Commit Graph
144 Commits
Author SHA1 Message Date
tiennm99 86eff7364d chore: add .env.example template
Lists only the variables the app actually reads, with placeholder values, so a
fresh clone does not have to grep for them. .gitignore keeps .env* ignored and
whitelists this one file.
2026-09-03 15:17:42 +07:00
tiennm99 eb4f272817 feat(ui): paint the key art behind every page
public/bg.png on one fixed layer under the whole app, through next/image so
the 2.4MB PNG is served as a ~167KB WebP resized to the viewport rather than
as a CSS background nobody can optimise.

.vn-surface -- the ground every page sits on -- becomes translucent so the art
reads through it; opaque panes (cards, the game header, the panorama surround)
still cover it. New --z-backdrop rung is the ladder's only negative value.
2026-09-03 12:19:44 +07:00
tiennm99 b15d199565 feat(scoring): score every region on one distance ladder
Region size no longer stretches the score thresholds. A guess is graded on
absolute precision, so a point means the same thing on the district, province
and country board, and the headline score matches what every level is credited.

Drops the per-round bands payload and its client plumbing: with one ladder the
result dialog reads the constant directly.
2026-09-03 11:46:17 +07:00
tiennm99 be448b3d97 fix(layout): size the game action bar with grid tracks
The Submit and Skip bar collapsed to zero height on an iPad Pro 10.5 in
landscape: the map card ran 60px past where it should stop, which is exactly
the bar plus its gap. From lg up the bar sat four indefinite-height containers
deep -- a min-h-dvh column, two flex-1 min-h-0 boxes, an implicit auto grid row
-- and was the shrinkable sibling of a flex-1 map. Safari before 18 resolves
that chain by squeezing the auto-sized item to nothing, and that device caps at
iPadOS 17.

Give the map and controls column explicit grid tracks instead, so a stretched
row sizes the map and an auto row sizes the bar, with no flex free-space maths
in between. The bar and the header are also marked shrink-0, and the content
grid declares its single row rather than growing an implicit one.

The result dialog is centred with a translate, so a viewport unit that
over-reports the visible area hides Next Round below the fold rather than
cropping the bottom edge. Clamp it with svh, which is never larger than what
is on screen.
2026-09-02 15:43:44 +07:00
tiennm99 23c55bdb75 chore(plans): add the game screen layout review reports
The overlap audit that drove the layout work, and the verification pass that
measured each fix live across six viewports.
2026-09-02 09:53:22 +07:00
tiennm99 1aa15f8969 docs: record the layering ladder and fixed-surface rules
Document the z-index tokens and the isolate-not-out-bid rule for third-party
ladders, the footer and action-bar height tokens, how the game screen's
vertical budget and safe areas constrain floating chrome, which Leaflet chrome
each phone map state shows, and why percentage heights collapse below the
sticky-footer column.
2026-09-02 09:53:22 +07:00
tiennm99 a3b3d9970e fix(credits): add the OpenMapTiles credit Geoapify requires
The free plan asks for all three credits for non-osm-carto styles -- Geoapify,
OpenMapTiles for the tile schema, OpenStreetMap for the data -- and the page
carried only two. It matters more now that the credit is suppressed on the
collapsed minimap thumbnail.
2026-09-02 09:53:22 +07:00
tiennm99 26dd5f384e refactor(debug): float the coverage inspector over a full-width map
The two-column grid held half the width empty until a point was picked, and
reflowing the map on selection moved the very dot the user had just clicked.
From lg the inspector now floats over the map's right edge; a phone has
neither the width to float into nor the height to split, so there it takes the
surface and its close button brings the map back.

The map keeps a 16rem floor below lg: the page chrome takes 225px, so a purely
flexed surface collapsed to 98px on a landscape phone. Escape closes the
inspector unless a select owns the keypress, a ResizeObserver re-measures the
map when it reappears, and the tile attribution moves to the corner the
inspector does not cover.
2026-09-02 09:53:22 +07:00
tiennm99 23ee9d47ff fix(layout): put game chrome in flow and contain third-party z-index
The how-to-play hint was a free-floating overlay centred on the whole game
box, which put it over the guess map search field on desktop and over the
Mapillary attribution -- required visible by their Terms of Use -- on phones.
It now rides PanoramaViewer's topBarSlot as a flex sibling of that credit, so
neither collision is representable.

Replace the ad-hoc z-index values and the two forced 9999 rules with one
ladder of tokens in globals.css. Leaflet and Photo Sphere Viewer ladders are
contained by isolate on the panes that host them rather than out-bid, which
lets the dialog scrim finally cover the action bar and the minimap.

Also: pad the header and content box with the left/right safe-area insets that
viewportFit cover requires, size the collapsed minimap against the viewport so
a landscape phone shrinks it instead of pushing it into the panorama chrome,
hide Leaflet's credit and zoom buttons on that thumbnail and lift the zoom
control clear of the wrapped credit when it expands, move the desktop
"Click to place your guess" badge off the tile credit, raise the hint dismiss
target to 44px, and move the sticky-footer column off body so Radix portals
are not flex items.
2026-09-02 09:53:06 +07:00
tiennm99 1efd2b3a8e fix(layout): keep the credit footer out of the game surface
Render the credit and build stamp as an in-flow footer row in a
sticky-footer body column instead of a fixed overlay, so it can no
longer sit on top of the panorama, mini-map, or action bar. Page roots
now fill the column with flex-1, and the elements that used to reserve
space for the overlay drop their clearance offsets.
2026-09-02 01:58:01 +07:00
tiennm99 67b9195d63 feat(username): prompt for a name on landing
Open the username modal on first visit when localStorage holds no name,
instead of waiting for the first Play click. Dismissing the prompt with
no saved name falls back to a generated Player-xxxxxx, so every exit
path leaves a leaderboard name behind.
2026-09-02 01:57:53 +07:00
tiennm99 3e6bff83da feat(ui): add maker credit beside the build stamp 2026-09-01 18:00:38 +07:00
tiennm99 7212b67feb chore: remove leaderboard migration scripts and close plan
Completed one-shot leaderboard backfill migration verified in production.
Removed migration scripts, test, unused export, and npm script. Updated
documentation and closed UI/UX flow polish plan with session journal.
2026-09-01 17:26:21 +07:00
tiennm99 0ce56737f3 chore(plans): add execution plan and review reports
Add UI/UX flow polish execution plan with phases and review reports from code audit and presentation audit.
2026-09-01 17:17:22 +07:00
tiennm99 3cc77e2869 docs: update styling tokens, username flow, and structure
Document semantic color token system, username flow changes, and project structure refinements.
2026-09-01 17:17:16 +07:00
tiennm99 eebe1b7d6f refactor(api): drop expired session cityCode fallbacks
Remove deprecated cityCode fallback logic from guess and new-game routes, simplify session handling, update tests.
2026-09-01 17:17:11 +07:00
tiennm99 bf67ae655d refactor(ui): semantic color tokens and Lucide icon unification
Centralize color token system in globals.css, update theme.js with semantic naming, replace icon library references with Lucide, rename vn-surface class, and update all component references.
2026-09-01 17:17:06 +07:00
tiennm99 0b8b60a2ab feat(ui): game onboarding and result dialog clarity
Add FirstRoundHint banner for onboarding, desktop map ghost label, session progress badge. Restructure result dialog with map above fold, labeled reveal, legend, and collapsible leaderboard section. Improve skip tooltip, region picker labels/tooltips, dialog accessibility, credits parity, and ThemeToggle sizing.
2026-09-01 17:17:00 +07:00
tiennm99 0876a9db2d feat(ui): username flow rework
Defer username prompt to first Play click, make username editable in header chip, generate random Player-xxxxxx name on skip or deep-link, update tests accordingly.
2026-09-01 17:16:53 +07:00
tiennm99 0f167192ef chore(journal): session journal for licensing compliance work
Documents tile attribution requirements and Mapillary licensing updates.
2026-09-01 16:16:45 +07:00
tiennm99 afee4b981f fix(maps): complete tile attribution requirements
Geoapify free plan requires three linked credits for osm-bright styles
(Geoapify, OpenMapTiles, OSM contributors); the OSM fallback credit now
links to openstreetmap.org/copyright per OSM guidelines.
2026-09-01 16:16:37 +07:00
tiennm99 d7d6effbd0 chore(plans): document licensing research and tile-provider migration
Adds licensing research reports (Mapillary ToU verification, tile-provider comparison) and a complete plan for the Geoapify tile-provider migration with two execution phases.
2026-09-01 09:43:32 +07:00
tiennm99 a5c9ef5876 feat(maps): centralize tile provider with Geoapify support
Introduces map-tiles.js module to centralize tile provider logic, supporting Geoapify (paid, high quality) with API key fallback to OSM public server. Updates LeafletMap, ResultMap, and CoverageMap to use the new abstraction, improves maintainability and consistency across the codebase.
2026-09-01 09:43:19 +07:00
tiennm99 bbe5987a9b feat(credits): add Mapillary attribution overlay and credits page
Adds an overlay to acknowledge Mapillary imagery copyright and a dedicated /credits page displaying all data sources and providers with proper attribution.
2026-09-01 09:43:07 +07:00
tiennm99 649247b3a1 fix(home): tune wrench debug button hover and layout
Add improved hover/active feedback to debug button, safe-area offset for
better mobile positioning, descriptive title, and z-index adjustment for
proper stacking context.
2026-09-01 00:11:50 +07:00
tiennm99 289986695a refactor(debug): reorganize debug pages with shared shell and nav
Extract shared app bar (Home, DebugNav, ThemeToggle) to debug/layout.js,
consolidate tools as cards in debug/page.js, move bbox/Mapillary tester
to debug/bbox/page.js, apply shared shell to debug/coverage/page.js with
RegionSelect title row, and update project-structure docs to reflect new
debug section organization.
2026-09-01 00:11:41 +07:00
tiennm99 2b57bf9526 feat(footer): add build-stamp footer with commit SHA
Expose NEXT_PUBLIC_COMMIT_SHA via next.config.mjs (resolves full commit
SHA from VERCEL_GIT_COMMIT_SHA or git), render copyable footer stamp in
DebugFooter component (bottom-center, underlined, a11y-tuned), and add
e2e test for footer visibility and clipboard interaction.
2026-09-01 00:11:29 +07:00
tiennm99 fe3a89b3a8 chore(memory): update code-reviewer agent notes on scoring and quality gates
Capture scoring ladder implementation details, regional scoring board crediting
mechanics, and quality gate findings from UI/UX quick wins review cycle.
2026-08-31 22:09:04 +07:00
tiennm99 71e77eae00 chore(plans): add plan, journals, and review reports for UI/UX quick wins
Document 260831-1906 plan phases, execution journal, and code review reports
covering scoring ladder, game state refactor, home region continuity, and
leaderboard UI fixes. Multiple review passes verified test coverage and
component contracts.
2026-08-31 22:08:41 +07:00
tiennm99 9516599948 fix(maps): use regional scoring ladder for leaderboard distance tint
Leaderboard distance colors now computed against each board's own region's
scoring ladder, matching how boards are credited. 2km distance shows green
on country board (2 points) and red on district board (0 points). Remove
dead cdnjs config from ResultMap. Update project-structure.md.
2026-08-31 22:08:26 +07:00
tiennm99 81765f4878 feat(home): add last-region persistence and continue button
Track last played region in localStorage and show \"Continue in [region]\"
button on home page. Add scoring table derived from SCORE_BANDS with caption
explaining region-relative ladder. New last-region.js utility module.
2026-08-31 22:08:10 +07:00
tiennm99 7123cdde76 fix(game): split loading states, add inline error retry, improve round handling
Separate initialLoading/roundLoading/submitting concerns to prevent premature
panorama unmount. Add roundKey viewer remount trigger and round-epoch watchdog
to prevent stale fetches from replacing the current round. Next-round prefetch
in result dialog. Replace alert() with inline error+retry. Last-region write
on successful load.
2026-08-31 22:07:56 +07:00
tiennm99 434409443c feat(scoring): add region-relative scoring ladder and per-level board crediting
Implement region-scaled scoring: base ladder (0-5 points) adjusts thresholds
proportionally to the picked region's bbox diagonal. Leaderboards credited
separately using their own region's ladder, preventing country-round guesses
from earning inflated district-board points. Score and per-level points
returned in gameResult.bands and gameResult.levels.
2026-08-31 22:07:41 +07:00
tiennm99 38fd5af60e style(debug): move the debug page onto the app design tokens
- replace the pre-redesign gradient, white/10 glassmorphism and text-white
  with vn-gradient-bg, bg-card surfaces and foreground/muted tokens, so the
  page follows the light/dark theme like every other screen
- document the styling conventions and the deliberate raw-color exceptions
  (score bands, podium, panorama surround) in docs/development.md
2026-08-31 10:15:49 +07:00
tiennm99 9f1cd69029 chore: agent memory notes 2026-08-31 09:44:13 +07:00
tiennm99 1be57831a0 test(e2e): add Playwright smoke lane with stubbed APIs
- 9 chromium specs: region picker, username modal, one full round to the
  reveal and next-round reset
- every /api/* call, the panorama image, and OSM tiles stubbed via
  page.route, so the lane runs offline with no services or env
- webServer starts or reuses next dev (never reuses in CI)
2026-08-31 09:44:13 +07:00
tiennm99 e308638dbe chore(deps): upgrade to Next 16 and refresh dependencies
- next 15.5->16.3.3, react 19.2.8, eslint-config-next 16, turf 7.4,
  upstash 1.38.3, photo-sphere-viewer 5.15, lucide-react 1.x
- eslint.config.mjs rewritten to native flat config (FlatCompat and
  @eslint/eslintrc dropped); eslint stays on 9, v10 breaks
  eslint-config-next; new react-hooks compiler rules downgraded to warn
  for deliberate hydration and imperative-ref patterns
- add @neondatabase/serverless, @electric-sql/pglite, @playwright/test;
  drop uuid; data:seed and test:e2e scripts; --turbopack flags removed
  (default in 16)
2026-08-31 09:44:13 +07:00
tiennm99 6569f095c9 feat(game): serve panorama index from Neon Postgres
- panoramas + pano_provinces tables replace 28MB bundled JSON; pano-index.js
  now draws via cached COUNT + ORDER BY id OFFSET with rejection sampling,
  composite (province,id)/(district,id) indexes support the skip
- scripts/seed-pano-db.mjs validates pipeline artifacts (the old real-data
  vitest invariants, extracted to scripts/lib/pano-artifacts.mjs), stages
  into panoramas_next, verifies, renames into place in one transaction,
  keeps panoramas_old as backup; --province reseeds in place; --check
  validates only
- pipeline writes gitignored data-build/panos/; pano barrel removed
- tests run against PGlite mocked in at the @neondatabase/serverless
  boundary, mirroring the fake-upstash pattern; fixtures replace real data
- infrastructure errors rethrow instead of reading as missing coverage;
  session ids via crypto.randomUUID (uuid package dropped)
2026-08-31 09:44:03 +07:00
tiennm99 da7a715478 chore(plans): archive completed plans and stale reports
Journal entries preserve the outcomes; plan and report files remain
in git history.
2026-08-30 21:48:12 +07:00
tiennm99 75c77e9ab9 docs(plans): journal entry for guess map search 2026-08-30 21:42:44 +07:00
tiennm99 2768c0176a feat(game): add district and street search to the guess map
Adds offline region-tree matching with diacritic folding and Vietnamese
aliases (quận/q7 for District N), plus Photon geocoder for streets and
places bounded to the played region. Selection pans/zooms only; guess
placement remains click-only on the map. LeafletMap now supports zoomPosition
and proper teardown. Includes client-safety tests to prevent server-data
imports in this module.
2026-08-30 21:42:09 +07:00
tiennm99 5518676170 chore: session plan and agent memory
Record structure refactor plan and quality-gates analysis for future reference.
2026-08-30 20:52:41 +07:00
tiennm99 bed486c742 docs: refresh README and docs for the region tree and pipeline scripts
Update project overview to describe region tree refactor, document data
pipeline scripts, clarify project structure, and align with current practices.
2026-08-30 20:52:36 +07:00
tiennm99 997695659b build: add data pipeline scripts and migrate lint to eslint
Add build scripts for region data pipeline, migrate from eslint-config-next
to flat eslint config, and ensure node globals are available in all files.
2026-08-30 20:52:32 +07:00
tiennm99 3a965b7ede refactor(scripts): share data paths and barrel emitter across generators
Extract common path resolution and event-emitter barrel into shared modules
to reduce duplication and enable consistent data pipeline configuration.
2026-08-30 20:52:28 +07:00
tiennm99 fd64e9239b test(regions): guard every client module against server-only imports
Add comprehensive tests to ensure client-side code never imports from
pano-index or other server-only modules, maintaining security boundary.
2026-08-30 20:52:24 +07:00
tiennm99 10c828776b refactor(ui): extract round result, guess map, and leaderboard components
Move result display, map panels, and leaderboard into dedicated components,
reducing GameClient complexity. Removes unused tabs component and cleans
up unused parameters.
2026-08-30 20:52:20 +07:00
tiennm99 9133a3efc2 refactor(lib): split game.js into scoring and username modules
Extract username generation into a dedicated module, simplify game.js to
focus on round scoring logic, and update consumers accordingly.
2026-08-30 20:52:15 +07:00
tiennm99 3177e70e67 fix(api): stop sending the panorama id to the client
Prevents exposure of internal panorama IDs to the client, maintaining
security boundaries and avoiding client-side dependency on server state.
2026-08-30 20:52:11 +07:00
tiennm99 c9a35dca9a docs: describe the region tree, and finish the coverage page migration
Reconcile every doc with the shipped code. All six described a flat
five-city model, and features.md, tech-stack.md, game-flow.md and
project-structure.md still documented the dart-throw over /images?bbox=
that the prebuilt panorama indexes replaced.

project-overview.md gains a Coverage note that classifies absent coverage
into its three causes -- not yet added, no street imagery, missing from
the boundary -- because a note that only says "partial" teaches
maintainers to ignore real gaps. Cu Chi is named as the one instance of
the third, which is the only one that is a defect.

A context hook had been denying access to src/app/debug/coverage/page.js,
so this also lands the two items earlier phases recorded as
undeliverable: the api/debug/city-coverage -> region-coverage rename, and
the page's migration from a flat city list to RegionSelect. With its last
caller gone, game.js drops CITIES, cities, cityNames, cityCenters and
cityBboxes; getCityIndex, indexedCities and fetchCityPanorama take names
that match what they now take.

Selecting a district with no boundary returned a 400 and left the
previous region's panorama count and outline on screen beside the error,
so Ho Chi Minh's 184,938 read as Cu Chi's. The error path now clears
everything derived from the previous region, and a null boundary removes
the outline rather than skipping the redraw.

Two plan-time claims did not survive contact with the code and the docs
follow the code: VN scores like any other node rather than being a
zero-scoring exploration mode, and the fan-out credits two levels when a
panorama falls outside every district outline.
2026-08-30 19:46:13 +07:00