docs: add card carousel and tap-to-detail implementation plan [ci skip]

This commit is contained in:
Viktor Barzin 2026-02-21 19:10:06 +00:00
parent 089ee88728
commit f2e8d7d9f9
No known key found for this signature in database
GPG key ID: 0EB088298288D958

View file

@ -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 (
<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):
```html
<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:
```html
<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**
```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
```