Multi-trip storage scoping, "My uploads", and Apple Photos-style map thumbnails - #32
Merged
Merged
Conversation
Three follow-ups from live use on the trip: 1. The shareable link required ?token=... - now the shared secret is embedded server-side into the rendered page (same pattern as the Maps API key) instead of being read from the URL, so crews can just be sent cicerallye.com/photos. Server-side validation is unchanged, so this is the same protection level as before. 2. Uploads were getting stuck forever on "Preparing upload for..." with no way to recover except reloading. Plain fetch() has no timeout, so a stalled mobile connection just hung indefinitely. Added an AbortController-based timeout (25s for the small JSON calls, 60s for the actual file PUT) with one automatic retry before surfacing a clear failure message. Confirmed via Azure CLI that the app settings and storage account config are otherwise healthy, so this was a client-side gap rather than a server regression. 3. Researched why the iOS gallery Location toggle still doesn't always work: confirmed this is a known WebKit/iOS inconsistency specific to HEIC (JPEG is far more reliable), not fixable client-side. Expanded the in-modal tip to mention switching Camera format to "Most Compatible" for anyone who wants reliable auto-geotagging. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…mas.prokop/turbo-potato
- api/src/tripId.ts: getCurrentTripId() as the API-side source of truth
for the active trip id, mirroring src/data/editions.ts (must be kept
in sync manually - documented in the file).
- mediaTable.ts/mediaBlob.ts: partition key and blob paths are now
trip-scoped ({tripId}/{day}/...); mediaComplete.ts's path regexes
updated to match.
- Client-side thumbnail generation (photos.ts): generates a small
(~200px, JPEG q0.7) thumbnail in-browser before upload (canvas draw
from an img element for photos, a seeked video frame for videos) so
map pins/clusters never download full-size originals. Best-effort -
on any decode failure the upload proceeds without a thumbnail.
- mediaSas.ts now issues a second SAS URL for the thumbnail blob
alongside the original; mediaComplete.ts stores thumbBlobPath/thumbUrl
when present; media.ts/mediaMine.ts DTOs expose thumbUrl.
- MediaMarker.tsx: individual map pins now render the real photo/video
thumbnail (circular, thumbUrl falling back to blobUrl) instead of a
generic icon.
- MediaMarkers.tsx/mediaClusterRenderer.ts: markers are tagged with
their post data on ref-set so cluster bubbles can show a random
photo from the cluster (Apple Photos map-view style) plus the count
badge, falling back to the amber camera badge if no thumbnail is
available.
- MediaLightbox.tsx: close button switched to a solid bg-black/70 chip
with a white ring and a larger (44px) tap target for visibility
against bright photos.
- Also includes the previously-drafted "My uploads" ownership work
(uploadedBy tracking, mediaMine.ts, mediaItem.ts delete/patch,
email-capture + uploads list UI in photos.ts).
Verified: npx tsc --noEmit (api), npm run build, npm run lint, and a
syntax check of the embedded photos.ts script. Not merged/shipped per
instructions - holding on this branch pending further feature work.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR enhances the live photo/video upload and map experience by introducing per-uploader management (“My uploads”), scoping media storage by trip/edition, and rendering map pins/clusters with real media thumbnails to avoid pulling full-size originals.
Changes:
- Added self-reported uploader ownership (
uploadedBy) plus new endpoints to list/manage a user’s own uploads (GET /api/media/mine,PATCH/DELETE /api/media/{id}). - Trip-scoped storage/partitioning via a current trip id (
{tripId}/{day}/...) to support multiple editions over time. - Added client-generated thumbnail blobs (
thumbUrl) used by map pins/clusters and other UI tweaks (lightbox close button).
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/hooks/useMediaPosts.ts | Extends frontend media model to include optional thumbUrl. |
| src/components/RouteMap/MediaMarkers.tsx | Tags marker instances with post data for cluster thumbnail selection. |
| src/components/RouteMap/MediaMarker.tsx | Updates individual map pin rendering to show media thumbnails. |
| src/components/RouteMap/mediaClusterRenderer.ts | Renders cluster bubbles using a representative thumbnail + count badge. |
| src/components/MediaLightbox.tsx | Improves close button visibility and tap target. |
| api/src/tripId.ts | Introduces current trip id configuration (app setting + fallback). |
| api/src/mediaTable.ts | Adds ownership + thumbnail fields; scopes Table partition key by trip id. |
| api/src/mediaEmail.ts | Adds normalization/validation for self-reported uploader emails. |
| api/src/mediaBlob.ts | Trip-scopes blob paths and adds thumbnail blob path generation. |
| api/src/functions/photos.ts | Adds email capture, thumbnail generation/upload, and “Your uploads” UI with edit/delete. |
| api/src/functions/mediaSas.ts | Extends SAS issuance to include an optional thumbnail upload SAS. |
| api/src/functions/mediaMine.ts | Adds endpoint to list posts by uploader email for the current trip. |
| api/src/functions/mediaItem.ts | Adds per-owner delete/patch endpoints gated by token + uploadedBy match. |
| api/src/functions/mediaComplete.ts | Records uploadedBy and optional thumbnail metadata on completion; validates new path shapes. |
| api/src/functions/media.ts | Includes thumbUrl in the public media listing payload. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+28
to
31
| <div className="relative w-9 h-9 rounded-full shadow-lg border-[3px] border-white overflow-hidden bg-gray-300"> | ||
| {post.mediaType === 'video' ? ( | ||
| <Video size={16} strokeWidth={2.25} color="#fff" /> | ||
| <video src={thumbSrc} muted preload="metadata" playsInline className="w-full h-full object-cover" /> | ||
| ) : ( |
Comment on lines
+69
to
+71
| const container = await getMediaContainer(); | ||
| await container.getBlockBlobClient(entity.blobPath).deleteIfExists(); | ||
| await table.deleteEntity(mediaPartitionKey(), id); |
Comment on lines
+7
to
+16
| function pickThumbnailPost(markers: readonly Marker[]): MediaPost | undefined { | ||
| const posts = markers.map((m) => (m as TaggedMarker).__mediaPost).filter((p): p is MediaPost => !!p); | ||
| if (posts.length === 0) return undefined; | ||
| // Apple Photos-style: prefer a random photo so the cluster bubble feels | ||
| // like "one of your photos", falling back to a random video only if the | ||
| // cluster has no photos at all. | ||
| const photos = posts.filter((p) => p.mediaType === 'photo'); | ||
| const pool = photos.length > 0 ? photos : posts; | ||
| return pool[Math.floor(Math.random() * pool.length)]; | ||
| } |
Comment on lines
+36
to
+46
| const post = pickThumbnailPost(markers ?? []); | ||
| const thumbSrc = post ? post.thumbUrl ?? post.blobUrl : undefined; | ||
| if (thumbSrc) { | ||
| const img = document.createElement('img'); | ||
| img.src = thumbSrc; | ||
| img.style.width = '100%'; | ||
| img.style.height = '100%'; | ||
| img.style.objectFit = 'cover'; | ||
| img.style.display = 'block'; | ||
| div.appendChild(img); | ||
| } else { |
Comment on lines
+783
to
+788
| const thumb = document.createElement('div'); | ||
| thumb.className = 'mineThumb'; | ||
| thumb.innerHTML = post.mediaType === 'video' | ||
| ? '<video src="' + post.blobUrl + '" muted></video>' | ||
| : '<img src="' + post.blobUrl + '" />'; | ||
|
|
Comment on lines
+48
to
+53
| if (!blobPath || !BLOB_PATH_PATTERN.test(blobPath)) { | ||
| return { status: 400, body: 'Missing or invalid blobPath' }; | ||
| } | ||
| if (thumbBlobPath !== undefined && !THUMB_BLOB_PATH_PATTERN.test(thumbBlobPath)) { | ||
| return { status: 400, body: 'Invalid thumbBlobPath' }; | ||
| } |
- MediaMarker.tsx: only render a <video> element when no thumbUrl is present; thumbUrl is always a JPEG frame, so it must go through <img>. - mediaItem.ts: DELETE now also removes the thumbnail blob (entity.thumbBlobPath) when present, instead of leaving it orphaned. - mediaClusterRenderer.ts: pick the cluster thumbnail deterministically (newest post by id) instead of Math.random(), so it no longer flickers between different photos on every pan/zoom re-render. Also stopped falling back to blobUrl for videos without a generated thumbnail (that would point an <img> at a video file). - photos.ts "Your uploads" list: prefer thumbUrl for the preview thumbnail, only falling back to a <video>/full-size <img> for older posts without one. - mediaComplete.ts: blobPath/thumbBlobPath validation is now pinned to the current trip id (same value mediaPartitionKey() uses) instead of accepting any trip prefix, closing a cross-trip tampering gap. Verified: npx tsc --noEmit (api), npm run build, npm run lint, and the embedded photos.ts script syntax check. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This was referenced Jul 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three features for the live photo/video upload page (
cicerallye.com/photos):1. My uploads (ownership)
uploadedByemail (captured once, saved inlocalStorage) — lays groundwork for real sign-in later.GET /api/media/mine?email=...lists a person's own uploads.DELETE/PATCH /api/media/{id}let someone delete their own post or edit its location/date — both gated on the shared upload token and a matchinguploadedBy.photos.tsgained a "Your uploads" section: thumbnail grid with edit-location (reuses the existing map picker), edit-date, and delete.2. Multi-trip storage scoping
{tripId}/{day}/...), driven byapi/src/tripId.ts, which mirrors the active trip id insrc/data/editions.ts(documented as needing manual sync when a new trip starts).3. Apple Photos-style map thumbnails
<img>for photos, a seeked<video>frame for videos) and uploaded as a second tiny blob via an extended SAS response — no server-side compute added to the upload path.blobUrlfor that post.4. Lightbox close button
bg-black/70chip with a white ring and a larger (44px) tap target so it's visible against any photo.Verification
npx tsc --noEmit(api) — cleannpm run build— cleannpm run lint— cleanphotos.tsscript (large inline template literal, no build step) vianew Function()Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com