mirror of
https://github.com/tiennm99/vngeoguessr.git
synced 2026-10-11 12:29:01 +00:00
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.
238 lines
13 KiB
Markdown
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
|