import { useState, useCallback, useEffect } from 'react'; import useEmblaCarousel from 'embla-carousel-react'; import { ExternalLink, Footprints, Bike, Train } from 'lucide-react'; import type { PropertyProperties, POIDistanceInfo, POI } from '@/types'; import { formatDuration } from '@/utils/format'; function TravelModeIcon({ mode }: { mode: string }) { switch (mode) { case 'WALK': return ; case 'BICYCLE': return ; case 'TRANSIT': return ; default: return null; } } function POIDistanceBadges({ distances }: { distances: POIDistanceInfo[] }) { if (!distances || distances.length === 0) return null; // Group by POI name const byPoi = new Map(); for (const d of distances) { const existing = byPoi.get(d.poi_name) || []; existing.push(d); byPoi.set(d.poi_name, existing); } return (
{Array.from(byPoi.entries()).map(([poiName, dists]) => (
{poiName}: {dists.map(d => ( {formatDuration(d.duration_seconds)} ))}
))}
); } const TRAVEL_MODES: Array<'WALK' | 'BICYCLE' | 'TRANSIT'> = ['WALK', 'BICYCLE', 'TRANSIT']; function AllPOIDistances({ pois, distances }: { pois: POI[]; distances?: POIDistanceInfo[] }) { // Index distances by poi_id + travel_mode for O(1) lookup const distMap = new Map(); if (distances) { for (const d of distances) { distMap.set(`${d.poi_id}_${d.travel_mode}`, d); } } return (
{pois.map(poi => (
{poi.name}: {TRAVEL_MODES.map(mode => { const dist = distMap.get(`${poi.id}_${mode}`); return ( {dist ? formatDuration(dist.duration_seconds) : '\u2014'} ); })}
))}
); } function CardCarousel({ photos, altText }: { photos: string[]; altText?: 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 ( {altText ); } return (
e.stopPropagation()}>
{photos.map((url, i) => (
{`Property
))}
{photos.map((_, i) => (
))}
); } interface PropertyCardProps { property: PropertyProperties; variant?: 'compact' | 'full'; isHighlighted?: boolean; avgPricePerSqm?: number; allPOIs?: POI[]; onClick?: () => void; } export function PropertyCard({ property, variant = 'compact', isHighlighted = false, avgPricePerSqm, allPOIs, onClick, }: PropertyCardProps) { const lastSeenDate = property.last_seen.split('T')[0]; const lastSeenDays = Math.round((Date.now() - new Date(lastSeenDate).getTime()) / (1000 * 60 * 60 * 24)); // Determine if this is a good deal const isGoodDeal = avgPricePerSqm && property.qmprice > 0 && property.qmprice < avgPricePerSqm * 0.9; const isExpensive = avgPricePerSqm && property.qmprice > avgPricePerSqm * 1.1; const priceIndicator = isGoodDeal ? { dotColor: 'bg-[var(--deal-good)]', label: 'Good deal' } : isExpensive ? { dotColor: 'bg-[var(--deal-above)]', label: 'Above avg' } : null; const handleClick = () => { onClick?.(); }; if (variant === 'compact') { return (
{/* Photo carousel */}
{(property.photos?.length || property.photo_thumbnail) ? ( ) : null}
{/* Details */}
{/* Price */}
£{property.total_price.toLocaleString()} {property.listing_type !== 'BUY' && ( /mo )} {priceIndicator && ( )} {priceIndicator && ( {priceIndicator.label} )}
{/* Key metrics on one line */}
{property.rooms}bed · {property.qm} m² · £{property.qmprice}/m²
{/* Agency + freshness */}
{property.agency} · {lastSeenDays}d ago
{/* POI badges */}
); } // Full variant return (
{/* Image section with 16:10 aspect ratio */}
{(property.photos?.length || property.photo_thumbnail) ? ( ) : null} {/* Overlay buttons: heart + external link */}
e.stopPropagation()}>
{/* Content below image */}
{/* Price as dominant element */}
£{property.total_price.toLocaleString()} {property.listing_type !== 'BUY' && ( /mo )} {priceIndicator && ( )} {priceIndicator && ( {priceIndicator.label} )}
{/* Key metrics on one line */}
{property.rooms}bed · {property.qm} m² · £{property.qmprice}/m²
{/* Location */} {property.city && (
{property.city}
)} {/* POI travel times */} {allPOIs && allPOIs.length > 0 ? ( ) : property.poi_distances && property.poi_distances.length > 0 ? ( ) : null} {/* Agency + freshness */}
{property.agency} · {lastSeenDays}d ago
); }