Files
vngeoguessr/docs/project-structure.md
tiennm99 ceedf5dde9 feat(scripts): restore the leaderboards from a backup
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.
2026-10-02 13:12:52 +07:00

238 lines
13 KiB
Markdown

# 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/<province>/*.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