Files
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

5.3 KiB

Tech Stack

Frontend Framework

  • Next.js 16: React-based full-stack framework with App Router
  • React 19.2: Component architecture
  • Tailwind CSS 4: Utility-first CSS framework for styling

Street View & Mapping

  • Mapillary vector tiles: the z14 image layer is the index source. Read offline by scripts/build-pano-index.mjs with @mapbox/vector-tile + pbf, against a 50,000 requests/day cap
  • Mapillary Graph API: one call per round -- lookup by image id, ~230ms. Its /images?bbox= search is deliberately off the game path: it returns HTTP 500 in every dense district, because it counts images inside the box before applying the limit. See the header of src/lib/mapillary.js. The debug route that used to exercise that search was removed: it was an open proxy spending the API token on a call the game never makes
  • Leaflet: Interactive mapping library for guess placement
  • Map tiles: src/lib/map-tiles.js picks the provider at build time — Geoapify (NEXT_PUBLIC_GEOAPIFY_KEY set; free tier permits commercial use) or the OSM public server as the keyless fallback for dev, e2e, and forks
  • OpenStreetMap: Map data (© OpenStreetMap contributors, ODbL); Nominatim supplies the administrative boundaries via scripts/build-region-boundaries.mjs
  • @photo-sphere-viewer/core: 360° panorama viewer

Geographic Processing

  • @turf/turf: distance, point-in-polygon, union, simplify, and point-to-line distance -- used both at runtime and by the offline builds
  • Generated region tree: src/data/regions/ holds the nodes and src/data/boundaries/<province>/ the simplified outlines. The per-province panorama indexes live in Postgres (see below), seeded from local pipeline artifacts in data-build/panos/ (gitignored)
  • Client/server split: src/lib/regions.js is the client-safe view and imports nothing from src/lib/pano-index.js or src/lib/pano-db.js. That boundary is enforced by an import-graph walk in tests/regions.test.js -- the panorama rows are exact answers
  • Server-side Calculations: All geographic processing on backend

Data Storage & Session Management

  • Neon Postgres (HTTP): The panorama index -- 424k rows of id, province, district, lat, lng -- read via @neondatabase/serverless (DATABASE_URL or POSTGRES_URL). One random-draw query per round, count queries cached per process. Seeded by scripts/seed-pano-db.mjs, which stages into panoramas_next, verifies, then renames into place, keeping the previous generation as panoramas_old
  • Upstash Redis (REST): Session and leaderboard storage via @upstash/redis SDK (REST client, no sockets)
  • Credential Flexibility: Accepts UPSTASH_REDIS_REST_URL+UPSTASH_REDIS_REST_TOKEN (vanilla Upstash) or KV_REST_API_URL+KV_REST_API_TOKEN (Vercel Marketplace aliases)
  • Multi-tenant Key Prefix: All physical keys carry KEY_PREFIX (default vngeoguessr:) to safely share Upstash DB with other Vercel projects. Prefix applied centrally in src/lib/upstash.js; callers use logical keys only.
  • Key Namespaces: session:{id} (30-min TTL), leaderboard:{scope}, distance:{scope} (no expiry), stats:{day} and stats:players:{day} (90-day TTL), daily:{day} (48-hour TTL). {scope} is vietnam for the country and city:{regionCode} for every province and district -- the city: segment is kept so existing scores stay addressable
  • Sorted Sets: Leaderboard ranking; score boards untrimmed and served as a top-200 window, distance boards trimmed to 200. Hash and HyperLogLog for the daily statistics
  • UUID v4: Session identifier generation via built-in crypto.randomUUID()
  • 30-minute Session Expiry: Automatic TTL-based cleanup

UI Components & Styling

  • shadcn/ui: Complete component library with "new-york" style
  • Radix UI: Headless component primitives -- dialog, label, slot, plus accordion (province expansion) and select (region picker)
  • Lucide React: Icon library
  • class-variance-authority: Component variant management
  • tailwind-merge + clsx: Dynamic class name handling

Testing

  • Vitest: Test runner for the logic in src/lib/, the API routes, the generated region data, and the pipeline script helpers
  • In-memory Upstash fake: Default Redis backing store, no service required
  • PGlite: In-process Postgres (WASM) mocked in at the @neondatabase/serverless boundary, so the panorama queries run against real Postgres semantics with no service
  • SRH + Redis (Docker): Optional lane running the same tests against real Redis
  • Playwright: Chromium smoke tests for the UI (tests/e2e/), with every API call and the panorama image stubbed at the browser boundary -- no services, no env vars

Development & Analytics

  • ESLint: Code linting with Next.js configuration
  • Turbopack: Development server bundler
  • @vercel/analytics: User analytics tracking
  • @vercel/speed-insights: Performance monitoring

Key Dependencies

  • @upstash/redis: REST-based Upstash client; no socket pooling, works in edge compute and serverless
  • JavaScript Only: No TypeScript - pure JavaScript implementation
  • Individual Parameters: Functions use separate parameters instead of object destructuring
  • Note: redis (node-redis) package not used (replaced by Upstash REST SDK)