Files
vngeoguessr/docs/tech-stack.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

89 lines
5.3 KiB
Markdown

# 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). `{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, tabs, 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 leaderboard migration
- **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)