Files
vngeoguessr/docs/project-structure.md
T
tiennm99 e649259518 feat(game): explain a miss, share a round, and keep the phone header in reach
Below 3 points the result dialog says whether the guess had the right
district or province, located server-side from the boundaries. The
reveal links the spot on OpenStreetMap, and a Share button builds a
squares line with the region's URL. Region pages and the root carry
Open Graph metadata for link previews.

An expired round is worded as expired rather than as a failed save. A
skipped round gets a fresh session id so the unawaited delete cannot
kill the next round. Below sm the theme switch collapses to one cycling
button and the tally hides, so the mute control stays on a 360px
screen. The panorama viewer loads on demand, taking three.js out of
the first load.
2026-09-20 23:02:05 +07:00

11 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
  • 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
  • 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 - Main game client component
  • 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
  • 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
  • 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
  • tabs.jsx - Tab navigation

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

Tests (tests/)

Vitest, mostly one file per src/lib/ module, plus a route test for new-game, guess, skip, 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.

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