Skip to content

feat(spots): photos on add, one-tap save, flat My Spots, immersive map - #5

Merged
wbaxterh merged 2 commits into
v2-rebuildfrom
feature/spots-ux-overhaul
Jul 14, 2026
Merged

feat(spots): photos on add, one-tap save, flat My Spots, immersive map#5
wbaxterh merged 2 commits into
v2-rebuildfrom
feature/spots-ux-overhaul

Conversation

@wbaxterh

Copy link
Copy Markdown
Owner

Overview

Mobile Spots UX overhaul so adding, finding, and collecting spots is fast and intuitive. Backend support (owner edit/delete, one-tap save, /saved, private-spot gating) is already deployed to prod (32b7d0c).

What changed

Add a spot + photos — the add flow now supports photos from the gallery (multi-select) or the camera. On submit it creates the spot, uploads each photo, and sets the first as the main image. If the spot dedupes to one already owned by someone else, it's saved to My Spots and your photos aren't attached to the foreign spot. Failed uploads are surfaced, not silent.

"My Spots" is now a flat list = spots you created (badge "Mine") + spots you saved (badge "Saved"), merged and de-duped (Mine wins). The named Collections feature is preserved behind a secondary toggle.

One-tap save — on the map's selected-spot card and the spot detail, tap the bookmark to save/unsave to My Spots; long-press opens the named-list picker. Saved state is seeded once from the server and reconciled on focus.

Immersive full-screen map — the existing toggle now truly hides the chrome (header/tabs/search/pills), keeps the map controls + a filter button + the selected card, and respects the safe area. Filtering by sport/category (already present) is reachable in fullscreen.

Spot detail — photo gallery (user + Google photos), owner-only inline Edit and Delete.

Environment Variables

No environment variable changes.

Technical Impact

  • New native modules: none. expo-image-picker@17 was already installed and camera + photo-library permission strings already exist in app.config.js, so this ships as a JS change (no new EAS build required for the picker itself, though a build refresh is recommended).
  • Backend: already deployed — POST/DELETE /api/spots/:id/save, GET /api/spots/saved, owner-gated PUT/DELETE /:id, and GET /:id now hides non-approved spots from non-owners.
  • Constraint respected: no react-native-maps <Marker> was added (crashes under the New Architecture); markers stay on the projected-overlay pattern.

Testing Evidence

  • npx tsc --noEmit — 0 errors (whole project)
  • npx biome check . — 0 errors
  • bash scripts/check-prod-ready.sh — all checks pass
  • High-effort multi-agent code review run on the diff; the confirmed correctness findings (optimistic-save bounce, dedup photo upload to a foreign spot, un-seeded preview cards) were fixed.
  • Not yet device-tested — needs a run on a physical device (camera/gallery picker, map, save flow).

Deployment Notes

Merge to v2-rebuild. Recommend a dev-client/EAS build to exercise the camera + gallery picker and the map on-device before a store build.

Follow-ups (not blocking)

  • Add a save/unsave affordance directly on the My Spots list rows (currently the badge is display-only; unsave is on the map card + detail).
  • Migrate the add-spot location drop-pin off <Marker> to a center-crosshair (flagged in a code comment; left as-is to avoid destabilizing the working location step).
  • Optional: extract the shared optimistic save/unsave logic into a hook.

Mobile Spots UX overhaul (backend support already deployed).

Add Spot (add.tsx):
- Add photos from the gallery (multi-select) or camera before saving.
  On submit: createSpot -> upload each photo -> set the first as the main
  image. Photo upload failures are surfaced; if the spot deduped to one
  already owned by someone else, it's saved to My Spots and the user's
  photos are NOT attached to the foreign spot.

My Spots (index.tsx):
- Now a FLAT list of spots you CREATED ("Mine") + spots you SAVED
  ("Saved"), merged and de-duped (Mine wins). Named Collections preserved
  behind a secondary toggle.
- Map selected-spot card: one-tap bookmark save/unsave; long-press opens
  the named-list picker (AddToSpotListModal). Saved state is seeded once
  from the server (getSavedSpots) and reconciled on focus — fixes the
  optimistic-unsave bounce and un-seeded preview cards.
- Fullscreen map is now immersive: header/tabs/search/pills hide, controls
  (incl. a filter button) and the selected card stay, safe-area respected.

Spot detail ([spotId].tsx):
- Bookmark wired: tap = save/unsave to My Spots, long-press = named lists.
- Photo gallery (userPhotos + googlePhotos) via expo-image.
- Owner-only Edit (inline) + Delete.

API (src/lib/api/spots.ts):
- saveSpot / unsaveSpot / getSavedSpots / isSpotSaved / deleteSpot /
  uploadSpotPhoto (multipart via expo-file-system). No <Marker> added
  (New-Arch crash constraint respected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(spots+feed): tag a spot in posts, any-user photo contributions

Feed spot-tagging:
- Compose (media/upload.tsx): optional "Tag a spot" picker (debounced
  search) → sends spotId on the post. Posting without a spot is unchanged.
- The feed list and single-post views show a tappable spot chip that opens
  the spot. (Backend now enriches post.spot on lists too.)

Spot photo contributions (Google-Maps model):
- Any logged-in user can add photos to any spot from the detail screen
  (gallery multi-select or camera) → uploads then refreshes the gallery.
- Per-photo actions on user photos: Report (any user except the uploader)
  and Delete (uploader/admin). Google photos aren't reportable/deletable.
- Photos are public immediately; the backend auto-hides one after 3 distinct
  reports (App-Store-compliant UGC moderation).

API (src/lib/api): reportSpotPhoto / deleteSpotPhoto (photo key is
encodeURIComponent'd — the S3 key contains a "/", verified against prod
route matching); spotId on CreatePostData + spot on FeedPost.

Fixes from code review: encoded photo-key (report/delete were 404-ing);
gallery no longer double-renders the header photo; can't report your own
photo; refresh after report so an auto-hidden photo disappears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(spots map): stop the Fabric touch-registry crash + steadier markers

Fixes the hard crash on the spots map:
  NSInternalInconsistencyException: Inconsistency between local and UIKit
  touch registries  (RCTSurfaceTouchHandler, New Architecture / iOS 26)

Root cause (RN #53303): the projected-overlay marker Pressables were
UNMOUNTED mid-gesture — projectToScreen returned null for off-screen points
and the whole marker tree re-rendered on every onRegionChange frame — so a
marker with a live UIKit touch disappeared and desynced Fabric's touch
registry. "A lot of data" (many markers crossing the cull boundary) made it
near-certain.

Fixes:
- projectToScreenXY: never returns null for off-screen points; markers stay
  MOUNTED and are hidden via opacity + pointerEvents instead of unmounting.
- Stable, position-derived cluster keys (supercluster's cluster_id is
  reassigned across queries → key churn → remount).
- Marker onPress defers its animateToRegion to the next frame so re-projection
  can't move/unmount the pressed marker while the touch is finalizing.
- Safety net: patch-package neutralizes the fatal RCTAssert in
  RCTSurfaceTouchHandler (the guard right below it already skips the stray
  touch) — turns any residual desync into a no-op instead of a crash.

Deep on-device testing still required (see PR notes): iOS 26 physical device,
dense data, aggressive pan/pinch + tap-during-gesture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(spots map): Google-Maps-style search, drop the +/- zoom buttons

- Remove the +/- zoom controls (pinch-to-zoom covers it) and put a search
  (magnifying-glass) button in their place.
- Tapping it opens a full-screen search: as you type it queries Google Places
  (biased to the current viewport) AND our spots in parallel, grouped as
  "Spots" and "Places". Tap a spot to fly there and open its card; tap a place
  to fly the map to that location. Reachable in fullscreen where the top search
  bar is hidden.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(spots): show the top search bar in list view only

The map view now has its own Google-Maps-style in-map search (the
magnifying-glass control), so the search bar above the content is redundant
there. Gate it on viewMode === 'list' so it only appears in the list view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(spots map): show the bottom spot card only when a pin is selected

Previously the bottom of the map always showed a horizontal preview of up
to 5 spots when nothing was selected. Now the bottom card is hidden by
default and appears only when you tap a pin; tapping elsewhere on the map
clears the selection and hides it, and tapping another pin swaps it in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 14, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@wbaxterh
wbaxterh merged commit 82d7e3e into v2-rebuild Jul 14, 2026
2 checks passed
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.

1 participant