diff --git a/docs/plans/2026-02-21-card-carousel-tap-detail-plan.md b/docs/plans/2026-02-21-card-carousel-tap-detail-plan.md new file mode 100644 index 0000000..e68d349 --- /dev/null +++ b/docs/plans/2026-02-21-card-carousel-tap-detail-plan.md @@ -0,0 +1,260 @@ +# Card Carousel and Tap-to-Detail Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace single thumbnail with 5-photo carousel on listing cards and fix tap behavior to open the detail bottom sheet. + +**Architecture:** Backend adds `additional_info` to streaming columns, extracts first 5 photo URLs into a `photos` property in GeoJSON output. Frontend adds a compact embla-carousel to PropertyCard and changes click handler to open the detail sheet instead of navigating to Rightmove. + +**Tech Stack:** Python (backend GeoJSON export), React + TypeScript + embla-carousel-react (frontend) + +--- + +### Task 1: Backend — Add photos to GeoJSON streaming + +**Files:** +- Modify: `repositories/listing_repository.py:19-25` (STREAMING_COLUMNS) +- Modify: `ui_exporter.py:12-85` (convert_row_to_geojson) +- Modify: `ui_exporter.py:88-134` (convert_to_geojson_feature) + +**Step 1: Add `additional_info` to STREAMING_COLUMNS** + +In `repositories/listing_repository.py`, add `'additional_info'` to the list: + +```python +STREAMING_COLUMNS = [ + 'id', 'price', 'number_of_bedrooms', 'square_meters', + 'longitude', 'latitude', 'photo_thumbnail', 'last_seen', + 'agency', 'price_history_json', 'available_from', + 'service_charge', 'lease_left', 'additional_info', +] +``` + +**Step 2: Extract photos in `convert_row_to_geojson`** + +In `ui_exporter.py`, after the `available_from` handling block (after line 47), add photo extraction: + +```python +# Extract first 5 photo URLs from additional_info +photos: list[str] = [] +additional_info = row.get('additional_info') +if additional_info: + if isinstance(additional_info, str): + import json as _json + additional_info = _json.loads(additional_info) + images = additional_info.get('property', {}).get('images', []) + photos = [img['url'] for img in images[:5] if isinstance(img, dict) and 'url' in img] +if not photos and row.get('photo_thumbnail'): + photos = [row['photo_thumbnail']] +``` + +Then add `"photos": photos,` to the properties dict (after line 66, the `photo_thumbnail` line). + +**Step 3: Extract photos in `convert_to_geojson_feature`** + +In `ui_exporter.py`, in `convert_to_geojson_feature`, `property_info` is already extracted at line 98. After line 98, add: + +```python +images = property_info.get('images', []) +photos = [img['url'] for img in images[:5] if isinstance(img, dict) and 'url' in img] +if not photos and listing.photo_thumbnail: + photos = [listing.photo_thumbnail] +``` + +Then add `"photos": photos,` to the properties dict (after the `photo_thumbnail` line). + +**Step 4: Run backend tests** + +Run: `pytest tests/ -v --tb=short -k "export or exporter or geojson"` +Expected: All pass (no tests directly assert the exact properties shape for photos) + +**Step 5: Commit** + +```bash +git add repositories/listing_repository.py ui_exporter.py +git commit -m "feat: include first 5 photo URLs in GeoJSON listing properties" +``` + +--- + +### Task 2: Frontend — Add photos to type definition + +**Files:** +- Modify: `frontend/src/types/index.ts:10-26` (PropertyProperties) + +**Step 1: Add `photos` field** + +In `PropertyProperties` interface, add after line 22 (`photo_thumbnail`): + +```typescript + photos?: string[]; +``` + +**Step 2: Commit** + +```bash +git add frontend/src/types/index.ts +git commit -m "feat: add photos array to PropertyProperties type" +``` + +--- + +### Task 3: Frontend — Add compact carousel to PropertyCard + +**Files:** +- Modify: `frontend/src/components/PropertyCard.tsx:1-3` (imports) +- Modify: `frontend/src/components/PropertyCard.tsx:117-120` (handleClick) +- Modify: `frontend/src/components/PropertyCard.tsx:130-139` (compact thumbnail) + +**Step 1: Add embla imports** + +Add at top of PropertyCard.tsx, after the existing imports: + +```typescript +import { useState, useCallback, useEffect } from 'react'; +import useEmblaCarousel from 'embla-carousel-react'; +``` + +**Step 2: Fix handleClick — remove window.open** + +Replace lines 117-120: + +```typescript +const handleClick = () => { + window.open(property.url, '_blank', 'noopener,noreferrer'); + onClick?.(); +}; +``` + +With: + +```typescript +const handleClick = () => { + onClick?.(); +}; +``` + +**Step 3: Add CardCarousel component** + +Add a small inline component before the `PropertyCard` function (after the `AllPOIDistances` component, before the `PropertyCardProps` interface). This keeps carousel logic isolated: + +```typescript +function CardCarousel({ photos }: { photos: string[] }) { + const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, dragFree: false }); + const [selectedIndex, setSelectedIndex] = useState(0); + + const onSelect = useCallback(() => { + if (!emblaApi) return; + setSelectedIndex(emblaApi.selectedScrollSnap()); + }, [emblaApi]); + + useEffect(() => { + if (!emblaApi) return; + emblaApi.on('select', onSelect); + return () => { emblaApi.off('select', onSelect); }; + }, [emblaApi, onSelect]); + + if (photos.length <= 1) { + return ( + Property + ); + } + + return ( +
e.stopPropagation()}> +
+
+ {photos.map((url, i) => ( +
+ {`Photo +
+ ))} +
+
+ {/* Dot indicators */} +
+ {photos.map((_, i) => ( +
+ ))} +
+
+ ); +} +``` + +Note: `onClick={e => e.stopPropagation()}` prevents carousel swipe from triggering the card's click handler (which opens the detail sheet). + +**Step 4: Replace thumbnail with carousel in compact variant** + +Replace the thumbnail div (lines 130-139): + +```html +
+ {property.photo_thumbnail && ( + Property + )} +
+``` + +With: + +```html +
+ {(property.photos?.length || property.photo_thumbnail) ? ( + + ) : null} +
+``` + +Note: slightly larger (w-24 h-24 vs w-20 h-20) to make the carousel more usable. + +**Step 5: Run frontend tests** + +Run: `cd frontend && npx vitest run --reporter=verbose` +Expected: All pass + +**Step 6: Commit** + +```bash +git add frontend/src/components/PropertyCard.tsx +git commit -m "feat: replace thumbnail with photo carousel and fix tap-to-detail" +``` + +--- + +### Task 4: Verify end-to-end + +**Step 1: Run all backend tests** + +Run: `pytest tests/ -v --tb=short` +Expected: All pass + +**Step 2: Run all frontend tests** + +Run: `cd frontend && npx vitest run --reporter=verbose` +Expected: All pass + +**Step 3: Commit all remaining changes and push** + +```bash +git push origin master +```