Below 3 points the result dialog says whether the guess had the right district or province, located server-side from the boundaries. The reveal links the spot on OpenStreetMap, and a Share button builds a squares line with the region's URL. Region pages and the root carry Open Graph metadata for link previews. An expired round is worded as expired rather than as a failed save. A skipped round gets a fresh session id so the unawaited delete cannot kill the next round. Below sm the theme switch collapses to one cycling button and the tally hides, so the mute control stays on a 360px screen. The panorama viewer loads on demand, taking three.js out of the first load.
11 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 roundcredits/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 functionalitydebug/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- Main game client componentLeafletMap.js- Interactive map for guess placementPanoramaViewer.js- 360 degree street view display; owns the Mapillary attribution and atopBarSlotfor host chrome sharing that rowRegionPicker.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 switchSoundToggle.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 skeletonstabs.jsx- Tab navigation
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 elsepano-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 expirystats.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/*share.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)lib/assign-districts.mjs- District assignment shared by the two pano scriptslib/pano-schema.mjs- Panorama table DDL shared by the seed and the tests
Tests (tests/)
Vitest, mostly one file per src/lib/ module, plus a route test for
new-game, guess, skip, 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.
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