7.7 KiB
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:
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:
# 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:
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
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):
photos?: string[];
Step 2: Commit
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:
import { useState, useCallback, useEffect } from 'react';
import useEmblaCarousel from 'embla-carousel-react';
Step 2: Fix handleClick — remove window.open
Replace lines 117-120:
const handleClick = () => {
window.open(property.url, '_blank', 'noopener,noreferrer');
onClick?.();
};
With:
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:
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 (
<img
src={photos[0]}
alt="Property"
className="w-full h-full object-cover"
loading="lazy"
/>
);
}
return (
<div className="relative w-full h-full" onClick={e => e.stopPropagation()}>
<div className="overflow-hidden h-full" ref={emblaRef}>
<div className="flex h-full">
{photos.map((url, i) => (
<div key={i} className="flex-[0_0_100%] min-w-0 h-full">
<img
src={url}
alt={`Photo ${i + 1}`}
className="w-full h-full object-cover"
loading="lazy"
/>
</div>
))}
</div>
</div>
{/* Dot indicators */}
<div className="absolute bottom-1 left-0 right-0 flex justify-center gap-1">
{photos.map((_, i) => (
<div
key={i}
className={`w-1 h-1 rounded-full ${
i === selectedIndex ? 'bg-white' : 'bg-white/40'
}`}
/>
))}
</div>
</div>
);
}
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):
<div className="w-20 h-20 rounded-md overflow-hidden flex-shrink-0 bg-muted">
{property.photo_thumbnail && (
<img
src={property.photo_thumbnail}
alt="Property"
className="w-full h-full object-cover"
/>
)}
</div>
With:
<div className="w-24 h-24 rounded-md overflow-hidden flex-shrink-0 bg-muted">
{(property.photos?.length || property.photo_thumbnail) ? (
<CardCarousel photos={property.photos?.length ? property.photos : [property.photo_thumbnail]} />
) : null}
</div>
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
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
git push origin master