feat(gallery): add map view with geo-tagged photo markers#83
feat(gallery): add map view with geo-tagged photo markers#83joaquimscosta merged 11 commits intomainfrom
Conversation
…y results Add optional hasGeo boolean parameter to GET /api/v1/gallery that filters results to only UserUploadedMedia items with non-null latitude and longitude. Combines with existing category, decade, and search filters via AND logic. Includes integration tests for all filter combinations.
Create BaseMap and useMapClustering as reusable shared primitives extracted from BravaMap's MapCanvas. Refactor MapCanvas to consume these primitives, preparing for GalleryMapCanvas reuse. - BaseMap: thin wrapper around react-map-gl with token, defaults, ref forwarding - useMapClustering: generic hook wrapping Supercluster with expandCluster helper - Shared types: ClusterPointProperties, GeoPointFeature, MapBounds - MapCanvas: replaced direct Map/useSupercluster with shared primitives
Wave 3: Build GalleryMapCanvas with markers, popup, and geo adapter. - GalleryMapCanvas: flat 2D map using shared BaseMap + useMapClustering - GalleryMapMarker: 48x48px photo thumbnail markers with pin nub - GalleryMapPopup: preview card with title, location, date, CTA - mediaItemToGeoFeature(): converts MediaItem to GeoJSON point - hasGeo parameter threaded through API client and query hook - Empty state for zero geo-tagged photos
… a11y Wave 4: Wire gallery map into the gallery page with full integration. - GalleryView type extended to "grid" | "timeline" | "map" - Map toggle button with MapPin icon and geo-tagged photo count tooltip - GalleryMapCanvas lazy-loaded via next/dynamic (ssr: false) - Photo selection from map opens lightbox - "Show on Map" button in lightbox desktop sidebar and mobile bottom sheet - Lightbox → map: closes lightbox, switches to map view, flies to location - Filtered empty state with "Clear Filters" action - aria-pressed on all view toggle buttons - Photo interface extended with latitude/longitude for lightbox
…ctedPhotoId Move flyToCoords handler from render body to useEffect to prevent DOM mutations during render. Wire selectedPhotoId prop so the map highlights the photo currently open in the lightbox.
- Fix deep-link navigation: ?view=map now correctly restores map view on page load
- Wire hasGeo=true to API: Map view now fetches only geo-tagged photos from backend
- Add auto-fit bounds: Maps with 1-3 markers auto-fit to show all locations in view
- Update error message: Change error text to match spec ("Map unavailable")
- Add frontend tests: 10 Vitest tests for mediaItemToGeoFeature (valid/null/edge coords)
Fixes all 5 gaps identified in verification report:
- page.tsx initialView parsing now recognizes 'map' value
- gallery-content.tsx filters include hasGeo when activeView === 'map'
- gallery-map-canvas.tsx uses fitBounds for sparse marker sets
- Tests achieve 100% coverage of GeoJSON conversion logic with null safety
…mbnail URL Add 14 Vitest component tests for GalleryMapCanvas covering populated state (markers, count badge, ARIA), empty states (zero photos, filtered), and callback wiring (onViewChange, onClearFilters). Fix stale YouTube thumbnail URL expectation (maxresdefault → hqdefault) in gallery-mappers tests.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
🚀 PR Validation Results📁 Components Changed:
🔍 Validation Results:⏭️ Backend CI: Skipped (no relevant changes) 🎉 Status: READY FOR REVIEWAll validation checks have passed! This PR is ready for code review. Updated: 2026-02-27T00:18:02.444Z | PR: #83 |
Code reviewFound 5 issues:
nosilha/apps/web/src/app/(main)/gallery/gallery-content.tsx Lines 120 to 126 in 5df75af nosilha/apps/web/src/components/gallery/gallery-map-canvas.tsx Lines 130 to 148 in 5df75af
nosilha/apps/web/src/lib/backend-api.ts Lines 2385 to 2389 in 5df75af nosilha/apps/web/src/lib/api-contracts.ts Lines 1067 to 1069 in 5df75af
nosilha/apps/web/src/components/gallery/gallery-map-popup.tsx Lines 21 to 23 in 5df75af
nosilha/apps/web/src/components/gallery/gallery-map-canvas.tsx Lines 129 to 148 in 5df75af nosilha/apps/web/src/components/gallery/gallery-map-canvas.tsx Lines 89 to 93 in 5df75af 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
- Cap in-memory page size in queryActiveMedia (was using raw uncapped size) - Clear flyToCoords state when switching away from map view (prevents stale flyTo on remount) - Guard flyTo effect with mapReady to avoid race condition on first map load - Use CacheConfig.MAP_DATA (no-store) for hasGeo requests instead of 30-min ISR - Use thumbnailUrl only in GalleryMapPopup (no fallback to url which could be a VIDEO embed)
|
All 5 issues addressed in commit
All 270 frontend unit tests and 7 backend geo-filter integration tests pass. TypeScript compilation clean. |
…wrappers, extract bounds logic Simplify gallery map view components and utilities: - Reorder imports (types before dynamic imports) - Remove unnecessary arrow wrapper around callback - Replace nested ternary with parseView() helper - Extract shared bounds-reading logic into syncBounds callback - Simplify cluster property casting - Remove redundant intermediate variables - Remove dead null fallbacks All 270 tests pass (56 gallery-specific). Update plan submodule reference.
…alidation
Replace legacy force-static/revalidate pattern with "use cache" + cacheLife()
for next-generation on-demand ISR with three cache profiles:
- max: infinite cache (static pages: about, contact, privacy, terms, contribute)
- content: 5min stale, 1hr revalidate, 24hr expire (directory, history, people slugs)
- entry: 1min stale, 30min revalidate, 24hr expire (homepage, gallery, directory detail)
- longLived: 10min stale, 2hr revalidate, 7d expire (history/people index)
Add tag-based cache invalidation for directory/gallery pages. Extend
/api/revalidate to support both path-based and tag-based revalidation:
- revalidateTag("gallery") for all gallery content
- revalidateTag("category:hotels") for category-specific content
- revalidateTag("entry:heritage:chiesa") for individual entries
Update .env.local.example to document REVALIDATE_SECRET configuration for
frontend cache invalidation authentication.
All pages verified with full Playwright sweep. ESLint and TypeScript checks pass.
References: #83
…1) (#85) * perf(web): homepage ISR fix and static asset cache headers - Replace force-dynamic with revalidate=1800 on homepage (T-02) - Add immutable Cache-Control header for /_next/static/ assets (T-03) - Add lightningcss-darwin-x64 for local build compatibility * perf(web): sharp image optimization with AVIF and Debian slim Docker - Add sharp as production dependency for native image processing (T-04) - Switch Dockerfile from node:20-alpine to node:22-slim for glibc compat (T-05) - Configure AVIF format with WebP fallback for ~50% smaller images (T-06) * perf(web): enable React Compiler for automatic memoization - Add babel-plugin-react-compiler as devDependency (T-07) - Set reactCompiler: true in next.config.ts - Healthcheck confirmed 439/439 components compatible * feat(infra): add Cloudflare Terraform provider with DNS and R2 resources Add Cloudflare provider v5 to manage DNS zone, 17 DNS records, and R2 media bucket as code. All resources imported from existing Cloudflare dashboard config. CI/CD workflow updated with TF_VAR_cloudflare_* env vars from GitHub secrets. * perf(web): migrate to Next.js 16 cacheLife API for granular cache invalidation Replace legacy force-static/revalidate pattern with "use cache" + cacheLife() for next-generation on-demand ISR with three cache profiles: - max: infinite cache (static pages: about, contact, privacy, terms, contribute) - content: 5min stale, 1hr revalidate, 24hr expire (directory, history, people slugs) - entry: 1min stale, 30min revalidate, 24hr expire (homepage, gallery, directory detail) - longLived: 10min stale, 2hr revalidate, 7d expire (history/people index) Add tag-based cache invalidation for directory/gallery pages. Extend /api/revalidate to support both path-based and tag-based revalidation: - revalidateTag("gallery") for all gallery content - revalidateTag("category:hotels") for category-specific content - revalidateTag("entry:heritage:chiesa") for individual entries Update .env.local.example to document REVALIDATE_SECRET configuration for frontend cache invalidation authentication. All pages verified with full Playwright sweep. ESLint and TypeScript checks pass. References: #83 * feat(infra): add revalidation secret infrastructure for frontend cache invalidation - Create GCP Secret Manager secret (revalidate_secret) - Grant IAM access to both backend and frontend Cloud Run services - Inject REVALIDATE_SECRET env var into backend and frontend Cloud Run services - Backend uses secret to call frontend revalidation endpoint with authentication - Frontend validates incoming revalidation requests using shared secret This enables backend services (e.g., gallery media uploads, content updates) to invalidate the frontend's Next.js cache without exposing the cache invalidation endpoint publicly. * fix(infra,web): add missing IAM depends_on and fix footer imports - Add missing depends_on for revalidation and resend_api_key IAM bindings on frontend Cloud Run service (prevents race condition during Terraform apply) - Move COPYRIGHT_YEAR constant after imports in footer component (fixes import ordering) - Update misleading 'Build-time constant' comment to 'Module-level constant' (reflects actual evaluation timing in use client component) Addresses code review findings from pragmatic-code-review. * fix(web,infra): address PR review findings — cacheTag, DNS proxy, generateStaticParams - Add missing cacheTag(`photo:${id}`) to photo detail page for on-demand cache invalidation, matching the pattern in directory entry detail pages - Fix _domainconnect DNS CNAME record to use proxied=false since Cloudflare does not support proxying underscore-prefixed service records - Restore generateStaticParams to history/[slug] page for build-time pre-rendering consistency with people/[slug] page - Update .claude/rules/frontend/app-router.md to reflect ISR → cacheLife migration Follow-up: #86 tracks backend tag-based revalidation support * fix(web): remove generateStaticParams from history/[slug] — no sub-pages exist The history category has no sub-pages in the Velite content (only _meta.yaml), so generateStaticParams returns an empty array which Next.js 16 rejects with EmptyGenerateStaticParamsError when using "use cache". The people category correctly has generateStaticParams because it has actual sub-pages.
Summary
hasGeoquery parameter toGET /api/v1/galleryendpoint for server-side geo-filteringBaseMapcomponent anduseMapClusteringhook from BravaMap, refactorMapCanvasto use themGalleryMapCanvaswith clustered photo markers, popup cards, empty states, and auto-fit boundsnext/dynamic, URL sync with?view=map, "Show on Map" button in lightboxSpec
plan/arkhe/specs/026-gallery-map-view/— 12 tasks across 4 waves, all completed. Verification report: 95% overall confidence (PASS).Key files
GalleryController.kt,GalleryService.kt,GalleryControllerGeoFilterTest.ktfeatures/map/shared/base-map.tsx,use-map-clustering.ts,types.tscomponents/gallery/gallery-map-canvas.tsx,gallery-map-marker.tsx,gallery-map-popup.tsxgallery-content.tsx,image-lightbox.tsx,gallery-mappers.tsgallery-map-canvas.test.tsx,gallery-mappers.test.tsTest plan
./gradlew test— 7 geo-filter integration tests passpnpm run test:unit— 270/270 tests pass (14 new component tests)npx tsc --noEmit— clean/gallery?view=map, verify markers, clustering, popup, empty states