Files
vngeoguessr/docs/project-structure.md
T
tiennm99 11bfa59377 docs: codify the conventions the code follows, drop the unused tabs dependency
docs/development.md states the rules the code now enforces or keeps:
server-only modules and the import-graph test (which now also forbids
the daily pick and the region locator from client bundles), route
decides best-effort and the library never swallows, typed failure kinds
mapped to statuses, claim before write, one module per storage concern
through the guarded store, useEffectEvent for imperative callbacks,
unprefixed logical keys, three fixed region levels, and the response
contract. The parameter rule now says what it always meant: React
components destructure props. @radix-ui/react-tabs had no component
using it. Stale lines about tabs, pagination, a leaderboard migration
and a sub-second suite are corrected.
2026-09-21 16:25:53 +07:00

12 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
  • 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
  • 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)
  • 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/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.

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