Skip to content

feat(map): map shell, clusters, trip page, upload, location correction and place albums - #17

Merged
kctong529 merged 25 commits into
mainfrom
feat/map
Oct 6, 2026
Merged

kctong529 merged 25 commits into
mainfrom
feat/map

Conversation

@KALalev18

Copy link
Copy Markdown
Contributor

Summary

This PR finishes the Map and trip navigation work package for Increment 1 and starts on Increment 2 (location correction, map filtering). All data still comes from the mock in src/api/mock.ts, through src/api/client.ts. When the Go endpoints are ready, only the functions in client.ts need to change.

It adds three screens: Trip (gallery, journey, upload, location correction), Map · My photos (clusters, filters, a place panel), and a reworked Dashboard. It also removes the old Figma Profile screen and drawers, as we agreed (the design doc has no Profile page).

What's included

Map · My photos (src/MapScreen.tsx)

  • Located photos are clustered pins (PhotoClusters.tsx). Our own clustering, so we don't have to add Leaflet.markercluster and rewrite the Linux lockfile from Windows. A cluster zooms in when selected; a spot that can't be split further opens in the side panel.
  • Filters: Trip, From / To capture date and Filter my photos (place, city, country, trip title or owner; debounced).
  • Find a place searches world places and flies the map there (PlaceSearch.tsx).
  • The first load and every filter change frame the result. Panning only refetches when the server says the result was truncated (2,000-marker cap, doc 5.2).
  • Notices explain when a trip has no located photos, or some unlocated ones, with a Correct locations button that opens the trip.
  • Basemap: Geoapify Positron when VITE_GEOAPIFY_KEY is set in .env.local (see .env.example), otherwise grey OSM tiles. The Dockerfile accepts the key as a build arg.

Place card (src/components/PlaceAlbums.tsx)

  • A pin opens every photo in that city, grouped into albums (one per trip).
  • Carousel: ‹ › buttons, a "1 / 2" counter and dots; the arrow keys work when the photo has focus.
  • Shared with: your own trips show recipient avatars and names, or a lock and "Only you". Someone else's album shows "Shared with you by …".
  • Album switcher at the bottom right of the card: ‹ Album 1 of 3 › moves between albums at the same place, including ones other people shared.
  • Other people's photos sit at city level (city centre), never exact coordinates (doc 3.3).

Trip page (src/TripScreen.tsx)

  • Trip header, plus every photo including ones without GPS, as a gallery or a day-by-day journey list.
  • Upload (UploadPanel.tsx): JPEG/PNG up to 25 MB, per-photo states (pending → uploading → processing → ready / needs-location / failed), retry, and an idempotent client_file_id.
  • EXIF (src/api/exif.ts) reads capture time, UTC offset and GPS (both byte orders, PNG eXIf). A stand-in for Kane's Go worker so the prototype shows real metadata. A GPS of exactly 0,0 counts as no fix.
  • Location correction (LocationPicker.tsx): search a place, click or drag a pin, or "same place as previous photo". A correction updates the Dashboard counts, the map pin and the place filter together; capture time never changes.

Dashboard

  • Totals (photos, countries, cities, locations to review), trip cards with Private/Public badges, recently uploaded, create trip, and empty states.
  • Open trip and Show on map (opens the Map filtered to that trip).

Other

  • Design-doc palette and type scale in src/tokens.css.
  • Sample photos are Wikimedia Commons images, each placed at the landmark it shows and credited in CREDITS.md (CC BY / CC BY-SA require it, and the repo is public).
  • README updated for the new screens, mock data, EXIF and basemap setup.

How to test

  1. npm install (if it rewrites package-lock.json, run git checkout package-lock.json), then npm run dev.
  2. Dashboard: check the totals and cards; click Show on map on "Europe by rail".
  3. Map: clear the trip filter, zoom into Paris and click a pin. Page through the photos with ‹ ›, then switch albums at the bottom right. Try Filter my photos (e.g. "Rome") and the date filters.
  4. Trip: open "Rome with family". The scanned St Peter's photo has no location: correct it with search or the map pin, then confirm the suggested Pantheon label. Go back to the Dashboard; "Locations to review" should go down.
  5. Upload: upload a phone JPEG with GPS, one without GPS, and a file over 25 MB or not JPEG/PNG. Check the per-photo states and that retry works.
  6. Keyboard: Tab to a map pin and open it with Enter, use the arrow keys on the carousel, and close the panel with Esc.

Open questions for the API (TBD after the design gala for @kctong529)

  • GET /api/map/photos returns two new fields: owner_name (null for your own photos) and shared_with (recipient names). They aren't in doc 5.2 yet. OK to add, or should they come from somewhere else?
  • Shared albums on the owner's map: albums other people share with you by name appear on Map · My photos (city-level only). That depends on named recipients (Increment 3). Is that how we want it?
  • Map count: "N photos on the map" now includes shared photos (15 instead of 11). Should it count only your own?
  • Still open from before: a cross-trip review list (e.g. GET /api/photos?review=required) so the Dashboard's "Locations to review" tile can open one global queue. For now, correction is per trip.

Checklist

  • npm run build passes (typecheck + bundle)
  • No package-lock.json changes
  • .env.local not committed (git-ignored)
  • New images credited in CREDITS.md
  • Status shown with an icon and a word, not colour alone; all controls work from the keyboard

@KALalev18 KALalev18 self-assigned this Oct 4, 2026

@kctong529 kctong529 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together ahead of the gala. The connected Dashboard → Trip → Map flow gives us a useful prototype. I cross-checked it with the design document and grouped the remaining work below.

I took the acceptance tests and straightforward fixes on a new branch based on feat/map. We can divide the larger tasks separately; not everything needs to be completed before Wednesday. Sections 4 and 5 are follow-up work and decisions to coordinate, rather than requirements for this PR before the gala.

Update at 3b99cd5: checked items below are implemented on test/gala-acceptance-fixes, pending merging into feat/map. The original reasoning is retained for later reference.

1. Acceptance tests

The build passes, but we also need automated checks for the keyboard and responsive-layout claims. The Playwright suite now covers:

  • Keyboard navigation across Dashboard, Map and Trip: controls are reachable in a sensible order and focus is visible.
  • Map pins and clusters: Enter/Space activate pins, and clusters zoom or show their photos as intended.
  • Photo navigation: previous/next buttons and left/right arrow keys change the displayed photo and update the counter.
  • Place search and correction: suggestions can be navigated and selected with the keyboard; Save, Cancel and Escape behave consistently.
  • Upload: states and errors are readable, and Retry / Set location are keyboard-accessible.
  • Responsive layouts: desktop, tablet and phone checks cover horizontal overflow, control bounds and overlapping controls, including expanded forms and long labels.

Validation: 181 checks passed, with two intentional skips because the exact upload byte-limit boundary runs only on desktop. Application build and test typecheck also pass.

The suite additionally covers creation validation/cancellation, empty collections, failed reads and saves, combined Map filters, mixed uploads and drag-and-drop. The coverage inventory is in tests/docs/acceptance-coverage.md.

These are checks of the current mock prototype in Chromium, not proof of real-device compatibility, backend integration or complete accessibility compliance. Tests that expect uploaded content or progress to disappear explicitly describe temporary mock limitations.

2. Small fixes and documentation

  • Use Trip consistently. The design defines a Trip as the user-created collection; city/country are photo attributes. Calling the place-grouped collections “Albums” suggests another content type. These now use “Trips at this place” and “Trip 1 of 3.”
  • Fix the place grouping key. Matching only city can combine different cities with the same name. The grouping now uses (country, city), with regression coverage for same-named cities in different countries.
  • Remove the implementation message from location review. “The capture time never changes; the map, place filter and Dashboard totals update together” describes internal behavior rather than helping someone review a place.
  • Match the upload limit exactly. The mock previously used 25 * 1024 * 1024, whereas the contract allows 25,000,000 bytes. Some files could therefore pass the mock but fail the API. The limit is now aligned, with checks at the boundary and one byte above it.
  • Remove shared sample albums and recipient rows from the owner flow. We have not finalized how owned private trips, owned shared selections and other people's collections should appear. For now, “My photos” and its count contain owned photos.
  • Document the mock limitations. EXIF extraction happens in the browser, geocoding uses a fixed city list and distance heuristics, and uploads disappear on reload. These are useful demo stand-ins but include known incorrect/incomplete behavior. The README now explicitly documents these limitations, including missing original-EXIF preservation, normalization, attribution and timezone lookup.
  • Explicitly defer large-dataset viewport loading. The current logic stops fetching when the latest response is not truncated. After loading a small viewport, panning elsewhere can therefore miss photos. This limitation is recorded rather than presenting the 2,000-marker behavior as finished.
  • Document the clustering choice. Clustering is already specified, but §5.1 names Leaflet.markercluster. Keeping our custom implementation for the gala is reasonable; the deviation is recorded, with performance/edge-case evaluation left for later.

Additional fixes made while testing:

  • Empty filtered results show feedback even when a trip is selected.
  • Place-search failures are distinguished from “No places found,” and new input clears stale suggestions.
  • Long place labels no longer push Close outside the viewport.
  • Dashboard upload controls have consistent heights, desktop alignment and mobile stacking. File restrictions are shown as helper text.

3. Mock upload: verify the demo and document limitations

Uploads currently exist only in memory and do not exercise the actual backend workflow. We do not need to implement production registration and recovery semantics in this temporary mock before the gala.

  • Check the intended demo attempts. Verify the chosen files, displayed progress, resulting photos and location correction.

    Automated fixture checks now cover valid GPS/time, missing metadata, malformed dates/images, unsupported files, mixed results, progress, retry and correction. The actual gala files still need a manual rehearsal on desktop and a real phone, with external images and tiles enabled.

  • Document the mock's identity limitations. Uploading the same file into another trip can return the original trip's photo, and concurrent repeated files can create duplicate records. These cases are not supported reliably by the current simulation.

  • Handle invalid metadata safely. An invalid capture date displays as unknown rather than breaking rendering.

When connecting the backend, implement the agreed batch-scoped identity and retry behavior, with tests for repeated requests and concurrent completion. Persistent progress recovery should also be handled during that integration.

The mock tests do not establish production idempotency, persistence or recovery. In particular, tests expecting uploads to disappear after reload must not become acceptance requirements for the real workflow.

4. Trip UI and photo ordering

  • Add title and private-notes editing. Trip editing is included in §3.2, and §5.2 lists PATCH /api/trips/{id}. The current UI only supports creation.

  • Add a normal full-photo viewer. Clicking a gallery thumbnail should open the photo for viewing, with a close control and keyboard support. Viewing should be separate from location correction.

  • Resolve the ordering mismatch between the API contract and frontend. The current API draft lists trip photos by upload time, while the mock returns them by capture time. Connecting the API could therefore change the gallery's order unexpectedly. Both orders are useful: I lean toward capture order for Trip browsing and upload order for recent additions. We should agree the gallery default, define missing-time and tie-breaking behavior, then align the contract and frontend—or explicitly sort in the frontend.

  • Follow the journey's specified date handling. List undated photos separately and show “time zone unknown” when the UTC offset is unavailable.

    Partially complete: undated photos are grouped separately and tested. The “time zone unknown” message remains missing.

  • Record capture-time editing as a later proposal. Capture timestamps may be incorrect because of camera-clock or timezone settings. A separate editing feature could preserve the imported value alongside a corrected timestamp; location correction itself should leave capture time untouched.

5. API integration, coordinate with backend work

The client.ts boundary is useful, but the current frontend types mix API data with presentation needs. Replacing mock calls with fetch will require some adaptation.

The README and client.ts now explicitly document that the mock uses frontend view models. This clarification does not implement the adapters or integration below.

  • Separate API responses from view models. For example, the UI wants a flat cover URL and trip summaries, while the contract returns structured image data with expiry information. Keep that transformation in an explicit adapter.
  • Use contract-shaped mocks for agreed endpoints. Run mock and real responses through the same adapter so the prototype exposes integration mismatches early.
  • Implement pagination. The screens currently ignore next_cursor; real responses may therefore show only the first page of trips or photos.
  • Handle temporary image URLs. Preserve expiry information and define how the UI obtains fresh URLs.
  • Connect the actual upload sequence. Registration, direct GCS upload, completion and batch-status recovery are currently simulated by one callback-driven function.
  • Complete map/search/correction contracts. Their endpoint purposes already appear in §5.2, but request/response details and additional fields still need agreement.
  • Coordinate authentication integration. The frontend should attach Firebase tokens and handle authentication failures; the backend should verify tokens and enforce ownership.

Before restoring shared-content UI, we should settle its access and presentation model. A share contains selected photos, so recipient information cannot simply be attached to every photo in a trip.

For Wednesday, I suggest prioritizing merging the completed fixes, checking the intended upload attempts, and one tested demo flow. The larger UI and backend integration work can follow after the gala.

@kctong529

Copy link
Copy Markdown
Contributor

Related issues for the changes in this follow-up branch:

These partially address the issues; live integration and remaining manual checks are recorded in their checklists.

Related follow-up work: #8 for authentication and #13 for real-device metadata preservation. Deployment is tracked in #15 and was implemented independently in 98b1d1d.

Local checks at 3b99cd5 pass: 181 passed, 2 intentionally skipped, plus build and test typechecking. The changes are on test/gala-acceptance-fixes, based on feat/map.

@kctong529
kctong529 self-requested a review October 6, 2026 00:03

@kctong529 kctong529 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fixes and acceptance tests are now included. Approving this for the gala prototype. Remaining UI decisions and backend integration work are tracked in the linked issues.

@kctong529
kctong529 merged commit 0ba6c55 into main Oct 6, 2026
2 checks passed
@kctong529
kctong529 deleted the feat/map branch October 6, 2026 00:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants