Skip to content

Map media polish: bigger square markers, multi-photo browsing, download, per-day/photostream tile - #35

Merged
TomProkop merged 4 commits into
mainfrom
users/tomas.prokop/turbo-potato
Jul 23, 2026
Merged

TomProkop merged 4 commits into
mainfrom
users/tomas.prokop/turbo-potato

Conversation

@TomProkop

Copy link
Copy Markdown
Member

Follow-up round of map/photo polish after seeing the shipped thumbnails/clusters (#32, #34) live.

What changed

  • Bigger, square markers: individual media pins and cluster bubbles switch from small circles to bigger rounded squares (Apple Photos style), with a resized/repositioned count badge that no longer clips.
  • Shared reusable MediaLightbox: reworked from a single post prop to posts[] + 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.
  • Multi-photo browsing on the map: clicking a media cluster now opens the lightbox with that cluster's posts instead of the default zoom-to-split (new onClusterClick override in useClusterer). 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.
  • Cluster overlap fix: a small constant screen-space offset nudges media markers/clusters apart from waypoint markers/clusters sharing the same coordinate (e.g. a photo taken right at a trip stop), so neither fully covers the other.
  • Per-day photos button: DayStatsTable gets 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.
  • PhotoStreamTile: new compact recent-photos grid mounted next to the map (in the same sidebar as the tracker/day tables), with a "View all" link, using the same shared lightbox.

Files

  • MediaLightbox.tsx, PhotoStream.tsx
  • RouteMap/MediaMarker.tsx, mediaClusterRenderer.ts, MediaMarkers.tsx, useClusterer.ts, DayStatsTable.tsx, index.tsx
  • New: RouteMap/mediaProximity.ts, mediaByDay.ts, PhotoStreamTile.tsx

No backend changes.

Verification

  • npm run build and npm run lint both pass.
  • Manually reasoned through: cluster click opens the lightbox instead of zooming; 500m grouping is correct for lone pins; arrow nav wraps; download always uses 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

…, 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 MediaLightbox to support browsing posts[] 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 DayStatsTable and a new PhotoStreamTile in 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.

Comment thread src/components/RouteMap/useClusterer.ts Outdated
Comment on lines +43 to +56
{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>
</>
) : (

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment thread src/components/RouteMap/PhotoStreamTile.tsx Outdated
Comment on lines +23 to +46
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;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

TomProkop and others added 3 commits July 23, 2026 20:59
… 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>
@TomProkop
TomProkop force-pushed the users/tomas.prokop/turbo-potato branch from 9b7a6f7 to ac3857c Compare July 23, 2026 19:42
@TomProkop
TomProkop merged commit 6c732d9 into main Jul 23, 2026
TomProkop added a commit that referenced this pull request Jul 27, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants