diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 75d5698..41d9c62 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,11 @@ jobs: NODE_OPTIONS: '--max-old-space-size=4096' VITE_AUTH0_DOMAIN: ${{ secrets.VITE_AUTH0_DOMAIN }} VITE_AUTH0_CLIENT_ID: ${{ secrets.VITE_AUTH0_CLIENT_ID }} + VITE_AUTH0_AUDIENCE: ${{ secrets.VITE_AUTH0_AUDIENCE }} AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} + VITE_SUPABASE_PROJECT: ${{ secrets.VITE_SUPABASE_PROJECT }} + VITE_SUPABASE_KEY: ${{ secrets.VITE_SUPABASE_KEY }} + - name: Extract hostname from package.json id: get-hostname diff --git a/CLAUDE.md b/CLAUDE.md index 9370101..65226ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -144,3 +144,4 @@ public/ - TypeScript target is ES6 with ESNext modules - Vite handles bundling and dev server (though dev mode is disabled per user preference) - Inspector can be toggled with 'i' key for debugging (only in development) +- https://dev.flatearthdefense.com is local development, it's proxied back to my localhost which is running npm run dev \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1c054f6..82a9f45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@babylonjs/procedural-textures": "8.36.1", "@babylonjs/serializers": "8.36.1", "@newrelic/browser-agent": "^1.302.0", + "@supabase/supabase-js": "^2.84.0", "loglevel": "^1.9.2", "openai": "4.52.3", "svelte-routing": "^2.13.0" @@ -986,6 +987,85 @@ "win32" ] }, + "node_modules/@supabase/auth-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.84.0.tgz", + "integrity": "sha512-J6XKbqqg1HQPMfYkAT9BrC8anPpAiifl7qoVLsYhQq5B/dnu/lxab1pabnxtJEsvYG5rwI5HEVEGXMjoQ6Wz2Q==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.84.0.tgz", + "integrity": "sha512-2oY5QBV4py/s64zMlhPEz+4RTdlwxzmfhM1k2xftD2v1DruRZKfoe7Yn9DCz1VondxX8evcvpc2udEIGzHI+VA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.84.0.tgz", + "integrity": "sha512-oplc/3jfJeVW4F0J8wqywHkjIZvOVHtqzF0RESijepDAv5Dn/LThlGW1ftysoP4+PXVIrnghAbzPHo88fNomPQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.84.0.tgz", + "integrity": "sha512-ThqjxiCwWiZAroHnYPmnNl6tZk6jxGcG2a7Hp/3kcolPcMj89kWjUTA3cHmhdIWYsP84fHp8MAQjYWMLf7HEUg==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.84.0.tgz", + "integrity": "sha512-vXvAJ1euCuhryOhC6j60dG8ky+lk0V06ubNo+CbhuoUv+sl39PyY0lc+k+qpQhTk/VcI6SiM0OECLN83+nyJ5A==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.84.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.84.0.tgz", + "integrity": "sha512-byMqYBvb91sx2jcZsdp0qLpmd4Dioe80e4OU/UexXftCkpTcgrkoENXHf5dO8FCSai8SgNeq16BKg10QiDI6xg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.84.0", + "@supabase/functions-js": "2.84.0", + "@supabase/postgrest-js": "2.84.0", + "@supabase/realtime-js": "2.84.0", + "@supabase/storage-js": "2.84.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@sveltejs/acorn-typescript": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.7.tgz", @@ -1068,6 +1148,12 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", @@ -1086,6 +1172,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@xstate/fsm": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz", @@ -1779,6 +1874,12 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.20.6", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", @@ -1937,6 +2038,27 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yaml": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", diff --git a/package.json b/package.json index 54a51cb..fb954d5 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@babylonjs/procedural-textures": "8.36.1", "@babylonjs/serializers": "8.36.1", "@newrelic/browser-agent": "^1.302.0", + "@supabase/supabase-js": "^2.84.0", "loglevel": "^1.9.2", "openai": "4.52.3", "svelte-routing": "^2.13.0" diff --git a/src/components/leaderboard/Leaderboard.svelte b/src/components/leaderboard/Leaderboard.svelte index 8d1006a..b15185e 100644 --- a/src/components/leaderboard/Leaderboard.svelte +++ b/src/components/leaderboard/Leaderboard.svelte @@ -3,11 +3,47 @@ import { Link } from 'svelte-routing'; import { gameResultsStore } from '../../stores/gameResults'; import type { GameResult } from '../../services/gameResultsService'; + import { CloudLeaderboardService, type CloudLeaderboardEntry } from '../../services/cloudLeaderboardService'; import { formatStars } from '../../game/scoreCalculator'; + // View toggle: 'local' or 'cloud' + let activeView: 'local' | 'cloud' = 'cloud'; + let cloudResults: CloudLeaderboardEntry[] = []; + let cloudLoading = false; + let cloudError = ''; + + // Check if cloud is available + const cloudService = CloudLeaderboardService.getInstance(); + const cloudAvailable = cloudService.isAvailable(); + + // Load cloud leaderboard + async function loadCloudLeaderboard() { + cloudLoading = true; + cloudError = ''; + try { + cloudResults = await cloudService.getGlobalLeaderboard(20); + } catch (error) { + cloudError = 'Failed to load cloud leaderboard'; + console.error('[Leaderboard] Cloud load error:', error); + } finally { + cloudLoading = false; + } + } + + // Switch view + function setView(view: 'local' | 'cloud') { + activeView = view; + if (view === 'cloud' && cloudResults.length === 0 && !cloudLoading) { + loadCloudLeaderboard(); + } + } + // Refresh data on mount onMount(() => { gameResultsStore.refresh(); + if (cloudAvailable && activeView === 'cloud') { + loadCloudLeaderboard(); + } }); // Format time as MM:SS @@ -28,18 +64,37 @@ } // Get color for end reason badge - function getEndReasonColor(result: GameResult): string { - if (result.endReason === 'victory') return '#4ade80'; - if (result.endReason === 'death') return '#ef4444'; + function getEndReasonColor(endReason: string): string { + if (endReason === 'victory') return '#4ade80'; + if (endReason === 'death') return '#ef4444'; return '#f59e0b'; // stranded } - // Get emoji for end reason - function getEndReasonEmoji(result: GameResult): string { - if (result.endReason === 'victory') return ''; - if (result.endReason === 'death') return ''; - return ''; + // Normalize cloud entry to match local result shape for display + function normalizeCloudEntry(entry: CloudLeaderboardEntry): GameResult { + return { + id: entry.id, + timestamp: new Date(entry.created_at).getTime(), + playerName: entry.player_name, + levelId: entry.level_id, + levelName: entry.level_name, + completed: entry.completed, + endReason: entry.end_reason as 'victory' | 'death' | 'stranded', + gameTimeSeconds: entry.game_time_seconds, + asteroidsDestroyed: entry.asteroids_destroyed, + totalAsteroids: entry.total_asteroids, + accuracy: entry.accuracy, + hullDamageTaken: entry.hull_damage_taken, + fuelConsumed: entry.fuel_consumed, + finalScore: entry.final_score, + starRating: entry.star_rating + }; } + + // Get current results based on active view + $: displayResults = activeView === 'cloud' + ? cloudResults.map(normalizeCloudEntry) + : $gameResultsStore;
Top 20 High Scores
+ + {#if cloudAvailable} +Loading global leaderboard...
+{cloudError}
+ +No game results yet!
Play a level to see your scores here.
@@ -69,7 +153,7 @@ - {#each $gameResultsStore as result, i} + {#each displayResults as result, i}