# Project Structure ## Root Directory - `CLAUDE.md` - Project instructions and guidelines for Claude Code - `components.json` - shadcn/ui configuration - `package.json` - Dependencies and scripts - `next.config.mjs` - Next.js configuration. Deliberately has no `redirects()`: a config redirect forwards the source query string to the destination, so the legacy `/game?region=` links are redirected from `src/app/game/page.js` instead - `eslint.config.mjs` - ESLint configuration - `postcss.config.mjs` - PostCSS configuration - `jsconfig.json` - JavaScript project configuration - `vitest.config.mjs` / `vitest.integration.config.mjs` - The two vitest lanes - `playwright.config.mjs` - The browser smoke-test lane (`tests/e2e/`) - `docker-compose.yml` - Local Redis + SRH for the integration lane - `data-build/` (gitignored) - Local pipeline artifacts awaiting `data:seed` - `README.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 modal - `layout.js` - Root layout component - `globals.css` - Global styles - `favicon.ico` - Site favicon #### Game Pages - `not-found.js` - The app-wide 404 for any unmatched path - `components/NotFoundPanel.js` - Shared body of both 404s - `components/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 404 - `game/[region]/page.js` - The game screen for one region (`/game/tphcm`). Server Component: validates the slug, prerenders one page per region, 404s an unknown one - `game/[region]/not-found.js` - The 404 for an unknown region code. A client component only so it can re-apply the theme: a thrown `notFound()` is served from Next's error shell, so React renders the root layout on the client, where its inline theme script cannot execute - `game/page.js` - Redirects the legacy `?region=` / `?location=` links to `/game/{slug}`; a region-less `/game` goes to the country round - `daily/page.js` - Today's daily challenge, the game client in daily mode - `credits/page.js` - Data sources, licenses, and open-source credits - `debug/page.js` - Debug hub: lists every debug tool as a peer - `debug/layout.js` - Shared shell for all debug pages: app bar, DebugNav, theme - `debug/DebugNav.js` - Segmented peer navigation shown on every debug page - `debug/coverage/page.js` - Panorama coverage map, per region - `debug/coverage/CoverageMap.js` - Leaflet layer for that page #### API Routes (`src/app/api/`) - `new-game/route.js` - Creates new game sessions with Redis storage - `guess/route.js` - Processes guess submissions, scores, and fans out - `leaderboard/route.js` - Leaderboard data management with Redis - `skip/route.js` - Skip current round functionality - `daily/route.js` - Opens today's daily-challenge session - `debug/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 through `next/image` - `GameClient.js` - The round lifecycle: fetch, epochs, prefetch, submit, daily replay. Layout lives in the components it renders - `GameHeader.js` - The game screen's app bar, presentational - `PlaceName.js` - A Vietnamese place name marked `lang="vi"` - `LeafletMap.js` - Interactive map for guess placement - `PanoramaViewer.js` - 360 degree street view display; owns the Mapillary attribution and a `topBarSlot` for host chrome sharing that row - `DailyCard.js` - Homepage door to the daily challenge: play, or today's result and share - `RegionPicker.js` - Homepage province accordion, one row per playable region - `RegionSelect.js` - Level buttons plus a grouped select, used where a single region has to be chosen from 67 - `LeaderboardList.js` - Ranked rows for one board - `UsernameModal.js` - Set or change the leaderboard name; skip generates a random `Player-xxxxxx` - `FirstRoundHint.js` - One-time how-to-play banner, rendered in flow into the panorama pane's top row via `PanoramaViewer`'s `topBarSlot` - `DonateQRModal.js` - Donation QR code modal - `ThemeToggle.js` - Light/dark switch; only writes the stored choice - `ThemeSync.js` - Applies the stored theme (and OS flips while it is 'system') to the document. Renders nothing; mounted once in the root layout - `ShareButton.js` - Share-sheet/clipboard button with its outcome label and screen-reader status, used by the result dialog and the daily card - `PlayLink.js` - A game link whose plain click the home page can intercept for the name prompt - `SoundToggle.js` - Music and sound-effect switches; `compact` renders one mute-everything button for the game header below `sm` - `MusicPlayer.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 notifications - `badge.jsx` - Badge components - `button.jsx` - Button variants - `card.jsx` - Card layouts - `dialog.jsx` - Modal dialogs - `input.jsx` - Input fields - `label.jsx` - Form labels - `select.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](development.md). - `regions/index.js` - The 85-node tree: code, name, parent, level, children, center, bbox, and coverage flags - `regions/counts.js` - Per-region panorama and cell tallies. **The one panorama-derived file a client component may import** - it carries counts only, never a coordinate - `boundaries//*.json` - Simplified outlines, one file per region, behind a generated `boundaries/index.js` barrel 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 including `cn()` for class name merging - `regions.js` - Client-safe region tree traversal. Imports nothing from `pano-index.js` or `pano-db.js`; `tests/regions.test.js` enforces that - `map-tiles.js` - Client-safe tile provider choice: Geoapify when `NEXT_PUBLIC_GEOAPIFY_KEY` is set at build time, OSM public server otherwise - `pano-index.js` - **Server-side only.** Picks a panorama for a region from Postgres and reports which district it landed in - `pano-db.js` - **Server-side only.** Neon HTTP adapter behind pano-index.js - `region-request.js` - Resolves and validates a region code from a request - `game.js` - Scoring ladder, distance, formatting - `username.js` - Player name in localStorage, the random-name generator, and `validateUsername`, the one rule the name prompt and `/api/guess` share - `last-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-out - `mapillary.js` - Mapillary lookup by image id - `player-id.js` - **Server-side only.** The anonymous `vng_pid` cookie that identifies a browser for repeat-avoidance, and nothing else - `cookies.js` - Cookie-header parsing for plain `Request`s, shared by `player-id.js` and `debug-access.js` - `pano-history.js` - **Server-side only.** The last 50 panoramas a player was shown, in Redis with a rolling 3-day expiry - `session.js` - Redis-based session management with 30-min expiry - `daily.js` - **Server-side only.** Today's panorama, picked from the day and cached in Redis - `daily-calendar.js` - Which day a daily belongs to (Vietnam time) and its number - `daily-progress.js` - The player's daily record and streak in localStorage - `stats.js` - **Server-side only.** Daily round counts and distinct-player HyperLogLog in Redis, 90-day TTL; read by `scripts/stats.mjs` - `region-locate.js` - **Server-side only.** Which region a map point falls in, from the generated boundaries; feeds the result dialog's region-hit line - `debug-access.js` - The production gate on `/api/debug/*` - `errors.js` - `DryPoolError` and `UpstreamError`, the two ways a draw fails - `storage.js` - The one place localStorage is touched: guarded read, write, and change notification across this tab and others - `use-stored-value.js` - **Client-side only.** Renders a stored value via useSyncExternalStore - `first-round-hint.js` - Whether the how-to-play hint has been seen - `geo-search.js` - Client-safe region and street search for the guess map - `share.js` - Client-safe share text for a round and the share-sheet call - `upstash.js` - Upstash Redis REST client adapter with multi-tenant key prefix - `theme.js`, `use-count-up.js` - Theme persistence and a count-up hook - `audio.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 tree - `build-pano-index.mjs` - Mapillary z14 tiles to per-province panorama artifacts - `assign-pano-districts.mjs` - Clips and partitions panoramas by district - `seed-pano-db.mjs` - Validates the artifacts and uploads them to Neon - `build-check.mjs` - Production build into `.next-check` - `stats.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 scripts - `lib/pano-schema.mjs` - Panorama table DDL shared by the seed and the tests - `lib/env.mjs` - The `.env` parser and loader every script shares - `lib/leaderboard-restore.mjs` - Backup validation and the board writes behind the import - `lib/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 note - `features.md` - Detailed game features documentation - `tech-stack.md` - Technology stack and dependencies - `development.md` - Development guidelines, commands, and build sequence - `game-flow.md` - Complete gameplay flow documentation - `project-structure.md` - This file - project organization ## Planning (`/plans/`) - Project planning documents and implementation plans ## Public Assets (`public/`) - `zlp.jpg` - Donation QR code image - `audio/` - Nine sound effects plus the music loop (shipped as both WebM/Opus and MP3), with `SOURCES.md` recording each file's upstream source and licence - Static assets served directly by Next.js