mirror of
https://github.com/tiennm99/vngeoguessr.git
synced 2026-10-11 03:13:56 +00:00
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.
89 lines
5.3 KiB
Markdown
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) |