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

13 KiB

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.

  • 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 Requests, 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