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)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/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