npm run leaderboard:import writes a decrypted backup back to Redis. It is a dry run until --apply, prints the destination prefix first, and accepts only score and distance board keys. By default each backed-up score is set and newer players are kept; --replace rewrites each board exactly. Members go in up to 1000 per ZADD through a new zAddMany helper. docs/leaderboard-backup.md covers what the backup holds, decrypting it and restoring it. The gitignore now covers the decrypted and encrypted file names the workflow uses, not only the dated export.
13 KiB
Project Structure
Root Directory
CLAUDE.md- Project instructions and guidelines for Claude Codecomponents.json- shadcn/ui configurationpackage.json- Dependencies and scriptsnext.config.mjs- Next.js configuration. Deliberately has noredirects(): a config redirect forwards the source query string to the destination, so the legacy/game?region=links are redirected fromsrc/app/game/page.jsinsteadeslint.config.mjs- ESLint configurationpostcss.config.mjs- PostCSS configurationjsconfig.json- JavaScript project configurationvitest.config.mjs/vitest.integration.config.mjs- The two vitest lanesplaywright.config.mjs- The browser smoke-test lane (tests/e2e/)docker-compose.yml- Local Redis + SRH for the integration lanedata-build/(gitignored) - Local pipeline artifacts awaitingdata:seedREADME.md- Public-facing project readme
Source Code (src/)
App Router (src/app/)
Next.js 16 App Router structure:
Pages
page.js- Homepage: region picker and leaderboard modallayout.js- Root layout componentglobals.css- Global stylesfavicon.ico- Site favicon
Game Pages
not-found.js- The app-wide 404 for any unmatched pathcomponents/NotFoundPanel.js- Shared body of both 404scomponents/InlineScript.js- The root layout's pre-paint theme script. Executable on the server, inert (text/plain) when React renders it on the client, where a script cannot run anyway — which is what stops React's "Encountered a script tag" console error on the region 404game/[region]/page.js- The game screen for one region (/game/tphcm). Server Component: validates the slug, prerenders one page per region, 404s an unknown onegame/[region]/not-found.js- The 404 for an unknown region code. A client component only so it can re-apply the theme: a thrownnotFound()is served from Next's error shell, so React renders the root layout on the client, where its inline theme script cannot executegame/page.js- Redirects the legacy?region=/?location=links to/game/{slug}; a region-less/gamegoes to the country rounddaily/page.js- Today's daily challenge, the game client in daily modecredits/page.js- Data sources, licenses, and open-source creditsdebug/page.js- Debug hub: lists every debug tool as a peerdebug/layout.js- Shared shell for all debug pages: app bar, DebugNav, themedebug/DebugNav.js- Segmented peer navigation shown on every debug pagedebug/coverage/page.js- Panorama coverage map, per regiondebug/coverage/CoverageMap.js- Leaflet layer for that page
API Routes (src/app/api/)
new-game/route.js- Creates new game sessions with Redis storageguess/route.js- Processes guess submissions, scores, and fans outleaderboard/route.js- Leaderboard data management with Redisskip/route.js- Skip current round functionalitydaily/route.js- Opens today's daily-challenge sessiondebug/pano/route.js- Resolve one panorama id to an image (closed in production without the debug key)debug/region-coverage/route.js- A region's outline and panorama points (same gate)
React Components (src/app/components/)
AppBackground.js- The key art (public/bg.png) on one fixed layer under every page, served throughnext/imageGameClient.js- The round lifecycle: fetch, epochs, prefetch, submit, daily replay. Layout lives in the components it rendersGameHeader.js- The game screen's app bar, presentationalPlaceName.js- A Vietnamese place name markedlang="vi"LeafletMap.js- Interactive map for guess placementPanoramaViewer.js- 360 degree street view display; owns the Mapillary attribution and atopBarSlotfor host chrome sharing that rowDailyCard.js- Homepage door to the daily challenge: play, or today's result and shareRegionPicker.js- Homepage province accordion, one row per playable regionRegionSelect.js- Level buttons plus a grouped select, used where a single region has to be chosen from 67LeaderboardList.js- Ranked rows for one boardUsernameModal.js- Set or change the leaderboard name; skip generates a randomPlayer-xxxxxxFirstRoundHint.js- One-time how-to-play banner, rendered in flow into the panorama pane's top row viaPanoramaViewer'stopBarSlotDonateQRModal.js- Donation QR code modalThemeToggle.js- Light/dark switch; only writes the stored choiceThemeSync.js- Applies the stored theme (and OS flips while it is 'system') to the document. Renders nothing; mounted once in the root layoutShareButton.js- Share-sheet/clipboard button with its outcome label and screen-reader status, used by the result dialog and the daily cardPlayLink.js- A game link whose plain click the home page can intercept for the name promptSoundToggle.js- Music and sound-effect switches;compactrenders one mute-everything button for the game header belowsmMusicPlayer.js- The background loop. Renders nothing and is mounted in the root layout, not in a page, so a client navigation between the menu and a round does not restart it
Reusable Components (src/components/)
shadcn/ui Components (src/components/ui/)
Only the primitives the app actually renders are vendored in. Add others with the shadcn CLI when a screen needs them, rather than keeping unused ones around.
accordion.jsx- Collapsible sections (province expansion)alert.jsx- Alert notificationsbadge.jsx- Badge componentsbutton.jsx- Button variantscard.jsx- Card layoutsdialog.jsx- Modal dialogsinput.jsx- Input fieldslabel.jsx- Form labelsselect.jsx- Grouped select (region picker)skeleton.jsx- Loading skeletons
Generated Data (src/data/)
Both directories are build output. Do not hand-edit; see Rebuilding the generated region data in development.md.
regions/index.js- The 85-node tree: code, name, parent, level, children, center, bbox, and coverage flagsregions/counts.js- Per-region panorama and cell tallies. The one panorama-derived file a client component may import - it carries counts only, never a coordinateboundaries/<province>/*.json- Simplified outlines, one file per region, behind a generatedboundaries/index.jsbarrel
The panorama index itself is not in the repo: the pipeline writes artifacts to
data-build/panos/ (gitignored) and scripts/seed-pano-db.mjs uploads them to
Neon Postgres, which is what the app queries at runtime.
Utility Libraries (src/lib/)
utils.js- Utility functions includingcn()for class name mergingregions.js- Client-safe region tree traversal. Imports nothing frompano-index.jsorpano-db.js;tests/regions.test.jsenforces thatmap-tiles.js- Client-safe tile provider choice: Geoapify whenNEXT_PUBLIC_GEOAPIFY_KEYis set at build time, OSM public server otherwisepano-index.js- Server-side only. Picks a panorama for a region from Postgres and reports which district it landed inpano-db.js- Server-side only. Neon HTTP adapter behind pano-index.jsregion-request.js- Resolves and validates a region code from a requestgame.js- Scoring ladder, distance, formattingusername.js- Player name in localStorage, the random-name generator, andvalidateUsername, the one rule the name prompt and/api/guesssharelast-region.js- Last-played region in localStorage (the home page's "Continue in ..." row)leaderboard.js- Leaderboard operations, including the district to province to country fan-outmapillary.js- Mapillary lookup by image idplayer-id.js- Server-side only. The anonymousvng_pidcookie that identifies a browser for repeat-avoidance, and nothing elsecookies.js- Cookie-header parsing for plainRequests, shared byplayer-id.jsanddebug-access.jspano-history.js- Server-side only. The last 50 panoramas a player was shown, in Redis with a rolling 3-day expirysession.js- Redis-based session management with 30-min expirydaily.js- Server-side only. Today's panorama, picked from the day and cached in Redisdaily-calendar.js- Which day a daily belongs to (Vietnam time) and its numberdaily-progress.js- The player's daily record and streak in localStoragestats.js- Server-side only. Daily round counts and distinct-player HyperLogLog in Redis, 90-day TTL; read byscripts/stats.mjsregion-locate.js- Server-side only. Which region a map point falls in, from the generated boundaries; feeds the result dialog's region-hit linedebug-access.js- The production gate on/api/debug/*errors.js-DryPoolErrorandUpstreamError, the two ways a draw failsstorage.js- The one place localStorage is touched: guarded read, write, and change notification across this tab and othersuse-stored-value.js- Client-side only. Renders a stored value via useSyncExternalStorefirst-round-hint.js- Whether the how-to-play hint has been seengeo-search.js- Client-safe region and street search for the guess mapshare.js- Client-safe share text for a round and the share-sheet callupstash.js- Upstash Redis REST client adapter with multi-tenant key prefixtheme.js,use-count-up.js- Theme persistence and a count-up hookaudio.js- Client-side only. The audio context, its first-gesture unlock, the decoded-buffer cache, one-shot playback, and the music/effects preferences
Build Scripts (scripts/)
Each carries a header comment with its flags and its cost.
build-region-boundaries.mjs- OSM/Nominatim to boundaries and the region treebuild-pano-index.mjs- Mapillary z14 tiles to per-province panorama artifactsassign-pano-districts.mjs- Clips and partitions panoramas by districtseed-pano-db.mjs- Validates the artifacts and uploads them to Neonbuild-check.mjs- Production build into.next-checkstats.mjs- Print the daily play statistics from Redis (npm run stats)export-leaderboards.mjs- Dump every board to JSON for backup (npm run leaderboard:export; run weekly by a GitHub Actions workflow)import-leaderboards.mjs- Write a backup back to Redis (npm run leaderboard:import; dry run unless--apply)lib/assign-districts.mjs- District assignment shared by the two pano scriptslib/pano-schema.mjs- Panorama table DDL shared by the seed and the testslib/env.mjs- The.envparser and loader every script shareslib/leaderboard-restore.mjs- Backup validation and the board writes behind the importlib/region-config.mjs- The hand-edited region configuration the boundary builder reads (input data, distinct from the generated tree)lib/barrel.mjs,lib/paths.mjs- Barrel writer and output paths
Tests (tests/)
Vitest, mostly one file per src/lib/ module, plus a route test for
new-game, guess, skip, daily, and debug/region-coverage. The leaderboard
and debug/pano routes have no dedicated test file; their underlying
src/lib/ logic (leaderboard.js, mapillary.js) is still covered. fake-upstash-redis.js, mock-upstash.js, redis-harness.js and
wait-for-srh.js are the shared harness that lets the same files run against
either the in-memory fake or a real Redis. fake-neon.js, mock-neon.js and
pano-fixtures.js are the equivalent for the panorama store: PGlite behind the
Neon SDK boundary, loaded with small synthetic rows. mapillary-stub.js
stubs the Mapillary Graph API around every test of a file that calls
stubMapillary().
tests/e2e/ holds the Playwright smoke specs (*.spec.js, so vitest never
collects them): the homepage picker, the username modal, one full round, and
routing.spec.js — the URL contract, which is most of the suite (region paths,
legacy ?region= redirects, both 404s, and the layout's inline theme script).
All run against browser-level stubs in tests/e2e/helpers.js with a fixture
panorama in tests/e2e/fixtures/. global-setup.js compiles the game routes
once before the workers start; without it a cold next dev makes every test
that navigates to /game/* time out together. Deeper UI behavior
(RegionSelect.js, the coverage page, real panoramas) remains manual testing
only.
Documentation (/docs/)
project-overview.md- Project overview, administrative basis, coverage notefeatures.md- Detailed game features documentationtech-stack.md- Technology stack and dependenciesdevelopment.md- Development guidelines, commands, and build sequencegame-flow.md- Complete gameplay flow documentationproject-structure.md- This file - project organization
Planning (/plans/)
- Project planning documents and implementation plans
Public Assets (public/)
zlp.jpg- Donation QR code imageaudio/- Nine sound effects plus the music loop (shipped as both WebM/Opus and MP3), withSOURCES.mdrecording each file's upstream source and licence- Static assets served directly by Next.js