Map media polish: bigger square markers, multi-photo browsing, download, per-day/photostream tile - #35
Conversation
…, download, per-day/photostream tile - MediaMarker/mediaClusterRenderer: bigger rounded-square pins/clusters (was small circles), resized count badge, small screen-space offset so media markers/clusters don't sit exactly on top of waypoint markers/clusters at the same coordinate. - MediaLightbox: reworked into the one shared photo/video viewer used everywhere (posts[] + initialIndex instead of a single post) - left/ right chevron nav with arrow-key support and wrap-around, plus a download button (always the full-size blobUrl, for photos and videos). - MediaMarkers: clicking a media cluster now opens the lightbox with that cluster's posts (via a new onClusterClick override in useClusterer) instead of the default zoom-to-split; clicking an individual pin also pulls in any other geotagged posts within 500m (new mediaProximity.ts haversine helper) so lone pins aren't a dead end for browsing. - New mediaByDay.ts helper + DayStatsTable: added a "see photos from this day" button next to the existing "Show" button on each day row, filtering posts by capturedAt's date. - New PhotoStreamTile, mounted in the map sidebar: compact recent-photos grid with a "View all" link, opens the same shared lightbox. - PhotoStream: updated to the new MediaLightbox props. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Polishes the map + photo browsing UX by unifying all media viewing into a shared lightbox, improving marker/cluster visuals, and adding new entry points (map sidebar tile + per-day photos button) to browse uploaded media.
Changes:
- Reworks
MediaLightboxto support browsingposts[]with keyboard/button navigation plus an “original download” action, and updates callers to open by index. - Enhances RouteMap media interactions: larger square pins/clusters, cluster-click opens the lightbox, and single-pin click expands to nearby geotagged media via haversine proximity.
- Adds new UI surfaces for media discovery: per-day photos button in
DayStatsTableand a newPhotoStreamTilein the map sidebar.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/RouteMap/useClusterer.ts | Adds an optional onClusterClick override to customize cluster click behavior (e.g., open lightbox instead of zoom). |
| src/components/RouteMap/PhotoStreamTile.tsx | New sidebar tile showing latest uploads with quick lightbox entry and links to /photos. |
| src/components/RouteMap/mediaProximity.ts | New haversine distance + radius filtering helpers for “nearby media” browsing. |
| src/components/RouteMap/MediaMarkers.tsx | Updates map media markers to open the shared lightbox; cluster click opens cluster posts; pin click expands to nearby posts. |
| src/components/RouteMap/MediaMarker.tsx | Enlarges and squares media pins; adds a small screen-space offset to avoid overlap with waypoint markers. |
| src/components/RouteMap/mediaClusterRenderer.ts | Updates cluster bubble styling to match new marker style and adds the matching overlap offset; exports helpers to extract posts from clusters. |
| src/components/RouteMap/mediaByDay.ts | New helper for filtering posts by day using UTC date slice. |
| src/components/RouteMap/index.tsx | Mounts the new PhotoStreamTile in the RouteMap sidebar. |
| src/components/RouteMap/DayStatsTable.tsx | Adds a per-day “photos” button that opens the shared lightbox for that day’s posts. |
| src/components/PhotoStream.tsx | Switches stream lightbox opening from post to posts + initialIndex. |
| src/components/MediaLightbox.tsx | Implements multi-post navigation + download; becomes the single shared viewer used across the app. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {post.mediaType === 'video' ? ( | ||
| <> | ||
| <video | ||
| src={post.thumbUrl ?? post.blobUrl} | ||
| muted | ||
| preload="metadata" | ||
| playsInline | ||
| className="w-full h-full object-cover" | ||
| /> | ||
| <span className="absolute bottom-1 right-1 w-4 h-4 rounded-full bg-black/60 flex items-center justify-center"> | ||
| <Video size={9} strokeWidth={2} color="#fff" /> | ||
| </span> | ||
| </> | ||
| ) : ( |
There was a problem hiding this comment.
Fixed - now renders an <img> for the thumbnail (it's always a JPEG frame) and only falls back to <video src={post.blobUrl}> when there's no thumbUrl.
| const [index, setIndex] = useState(initialIndex); | ||
| const post = posts[index]; | ||
| const canNavigate = posts.length > 1; | ||
|
|
||
| function goPrev() { | ||
| setIndex((i) => (i - 1 + posts.length) % posts.length); | ||
| } | ||
|
|
||
| function goNext() { | ||
| setIndex((i) => (i + 1) % posts.length); | ||
| } | ||
|
|
||
| useEffect(() => { | ||
| function handleKey(e: KeyboardEvent) { | ||
| if (e.key === 'Escape') onClose(); | ||
| else if (canNavigate && e.key === 'ArrowLeft') goPrev(); | ||
| else if (canNavigate && e.key === 'ArrowRight') goNext(); | ||
| } | ||
| window.addEventListener('keydown', handleKey); | ||
| return () => window.removeEventListener('keydown', handleKey); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [canNavigate, posts.length, onClose]); | ||
|
|
||
| if (!post) return null; |
There was a problem hiding this comment.
Fixed - added a safeIndex clamped to posts.length - 1 (and used it for rendering, the counter, and prev/next), so a shrinking posts array can't leave the index pointing past the end and blanking the overlay.
… cluster badge - MediaLightbox: portal to document.body via createPortal. When opened from a map pin/cluster, the lightbox was a DOM descendant of Google Maps' container, which creates its own CSS stacking context for GPU compositing. That trapped the lightbox's z-[1000] below the site header's z-50, since z-index only compares within the same stacking context - the whole map subtree was compared to the header as one unit. Rendering at the body level escapes it, so close/download/nav buttons are now visible and clickable regardless of call site. - mediaClusterRenderer: move the count badge to be a sibling of the thumbnail div instead of a child of it. The thumbnail div has overflow:hidden to clip the square photo to rounded corners, which was also clipping the badge's negative top/right offset (intended to make it overhang the bubble's corner) - so the badge count was being cut off/obscured. The wrapper div (already used for the anti-overlap nudge) is now the positioned ancestor for both. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
postsForDay compared MediaPost.capturedAt's YYYY-MM-DD slice directly
against DayStat.date, but DayStat/STOPS dates are human-readable
strings ('Sat, 18 Jul 2026'), not ISO - so the comparison never
matched and the per-day Photos button in DayStatsTable never appeared
for any day, regardless of viewport. Added toIsoDate() to convert the
human format via regex (avoiding a Date/toISOString timezone-shift
bug) before comparing.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- useClusterer: also depend on 'map' when assigning onClusterClick,
not just the handler itself - previously if the map instance became
available after the handler was passed in, the effect wouldn't
re-run and the clusterer would keep the default zoom-to-split
behavior instead of the custom click handler.
- PhotoStreamTile: render video posts' thumbUrl as an <img> (it's
always a JPEG frame), only falling back to <video src={blobUrl}>
when no thumbnail exists - previously thumbUrl was passed straight
into a <video src>, which fails to load/play for most posts.
- PhotoStreamTile: fixed a doc-comment typo (missing space before 'the').
- MediaLightbox: clamp the displayed index to the current posts length
instead of indexing directly with stale state - if a caller swaps
posts while the lightbox stays open and the previous index is now
out of range, post would become undefined and the whole overlay
would silently disappear.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
9b7a6f7 to
ac3857c
Compare
main had independently added multi-photo browsing/navigation and a download button to MediaLightbox (#35), plus client-side capturedAt sorting in useMediaPosts (#36), overlapping with this branch's own displayUrl-fetching and download-button work. Resolved by keeping main's posts/initialIndex-based MediaLightbox (portal rendering, prev/next nav, single download button) and layering this branch's displayUrl fetch-on-open logic on top, keyed to the currently navigated post - so browsing between photos in the lightbox still uses the compressed display copy per-post, not just on initial open. PhotoStream.tsx: kept main's selectedIndex-based version, re-applied the 12-post preview cap on top. useMediaPosts.ts: both branches sort by capturedAt (main client-side, this branch server-side in media.ts) - harmless redundancy, kept both and corrected a comment that had gone stale. Verified with npm run build (frontend) and cd api && npm run build - both clean.
Follow-up round of map/photo polish after seeing the shipped thumbnails/clusters (#32, #34) live.
What changed
MediaLightbox: reworked from a singlepostprop toposts[] + initialIndex, with left/right chevron nav (buttons + arrow keys, wrap-around) and a download button (always the full-size original, for both photos and videos). This is now the one viewer used everywhere - the main stream, the per-day button, the map-side tile, and map pins/clusters.onClusterClickoverride inuseClusterer). Clicking an individual pin also pulls in any other geotagged posts within 500m (new haversine helper), so a lone pin isn't a browsing dead end.DayStatsTablegets a camera-icon button next to the existing "Show" button on each day row, filtering posts to that day and opening the shared lightbox. Hidden for days with no photos.Files
MediaLightbox.tsx,PhotoStream.tsxRouteMap/MediaMarker.tsx,mediaClusterRenderer.ts,MediaMarkers.tsx,useClusterer.ts,DayStatsTable.tsx,index.tsxRouteMap/mediaProximity.ts,mediaByDay.ts,PhotoStreamTile.tsxNo backend changes.
Verification
npm run buildandnpm run lintboth pass.blobUrl; badge is legible at the new size; the nudge offset doesn't visually break isolated (non-overlapping) clusters; per-day button only shows for days with matching posts.Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com