# 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 ```