Add photo carousel to listing cards and fix tap-to-detail

Backend: include first 5 photo URLs from additional_info in GeoJSON
streaming response, with fallback to photo_thumbnail.

Frontend: replace single thumbnail with swipeable embla-carousel on
compact cards. Remove window.open on card tap so clicking opens the
detail bottom sheet instead of navigating to Rightmove.
This commit is contained in:
Viktor Barzin 2026-02-21 19:19:32 +00:00
parent f2e8d7d9f9
commit 4deed9911c
No known key found for this signature in database
GPG key ID: 0EB088298288D958
5 changed files with 87 additions and 14 deletions

View file

@ -1,3 +1,5 @@
import { useState, useCallback, useEffect } from 'react';
import useEmblaCarousel from 'embla-carousel-react';
import { ExternalLink, Bed, Maximize2, PoundSterling, Clock, Building, Footprints, Bike, Train } from 'lucide-react';
import { Button } from './ui/button';
import type { PropertyProperties, POIDistanceInfo, POI } from '@/types';
@ -84,6 +86,62 @@ function AllPOIDistances({ pois, distances }: { pois: POI[]; distances?: POIDist
);
}
function CardCarousel({ photos }: { photos: string[] }) {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
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>
<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>
);
}
interface PropertyCardProps {
property: PropertyProperties;
variant?: 'compact' | 'full';
@ -115,7 +173,6 @@ export function PropertyCard({
: null;
const handleClick = () => {
window.open(property.url, '_blank', 'noopener,noreferrer');
onClick?.();
};
@ -127,15 +184,11 @@ export function PropertyCard({
}`}
onClick={handleClick}
>
{/* Thumbnail */}
<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"
/>
)}
{/* Photo carousel */}
<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>
{/* Details */}