diff --git a/frontend/src/components/ListingDetail.tsx b/frontend/src/components/ListingDetail.tsx index 4a5d2d0..2b008f5 100644 --- a/frontend/src/components/ListingDetail.tsx +++ b/frontend/src/components/ListingDetail.tsx @@ -214,8 +214,12 @@ export function ListingDetail({ detail, onDecide, onClearDecision }: ListingDeta
{detail.price_history.map((entry) => (
- {entry.last_seen.split('T')[0]} - £{entry.price.toLocaleString()} + + {typeof entry.last_seen === 'string' ? entry.last_seen.split('T')[0] : '—'} + + + £{typeof entry.price === 'number' && Number.isFinite(entry.price) ? entry.price.toLocaleString() : '—'} +
))}
diff --git a/frontend/src/components/PropertyCard.tsx b/frontend/src/components/PropertyCard.tsx index dd4d9bf..663b088 100644 --- a/frontend/src/components/PropertyCard.tsx +++ b/frontend/src/components/PropertyCard.tsx @@ -145,8 +145,18 @@ export function PropertyCard({ 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)); + // BUY listings may have null numeric / date fields; coerce so renders don't throw. + const lastSeenRaw = property.last_seen; + const lastSeenDate = typeof lastSeenRaw === 'string' ? lastSeenRaw.split('T')[0] : null; + const lastSeenTime = lastSeenDate ? new Date(lastSeenDate).getTime() : NaN; + const lastSeenDays = Number.isFinite(lastSeenTime) + ? Math.round((Date.now() - lastSeenTime) / (1000 * 60 * 60 * 24)) + : null; + const safeNum = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0); + const safeTotalPrice = safeNum(property.total_price); + const safeQm = safeNum(property.qm); + const safeQmprice = safeNum(property.qmprice); + const safeRooms = safeNum(property.rooms); // Determine if this is a good deal const isGoodDeal = avgPricePerSqm && property.qmprice > 0 && property.qmprice < avgPricePerSqm * 0.9; @@ -175,7 +185,7 @@ export function PropertyCard({ {(property.photos?.length || property.photo_thumbnail) ? ( ) : null} @@ -185,7 +195,7 @@ export function PropertyCard({ {/* Price */}
- £{property.total_price.toLocaleString()} + £{safeTotalPrice.toLocaleString()} {property.listing_type !== 'BUY' && ( /mo )} @@ -200,18 +210,22 @@ export function PropertyCard({ {/* Key metrics on one line */}
- {property.rooms}bed + {safeRooms}bed · - {property.qm} m² + {safeQm} m² · - £{property.qmprice}/m² + £{safeQmprice}/m²
{/* Agency + freshness */}
{property.agency} - · - {lastSeenDays}d ago + {lastSeenDays !== null && ( + <> + · + {lastSeenDays}d ago + + )}
{/* POI badges */} @@ -234,7 +248,7 @@ export function PropertyCard({ {(property.photos?.length || property.photo_thumbnail) ? ( ) : null} @@ -257,7 +271,7 @@ export function PropertyCard({ {/* Price as dominant element */}
- £{property.total_price.toLocaleString()} + £{safeTotalPrice.toLocaleString()} {property.listing_type !== 'BUY' && ( /mo )} @@ -272,11 +286,11 @@ export function PropertyCard({ {/* Key metrics on one line */}
- {property.rooms}bed + {safeRooms}bed · - {property.qm} m² + {safeQm} m² · - £{property.qmprice}/m² + £{safeQmprice}/m²
{/* Location */} @@ -294,8 +308,12 @@ export function PropertyCard({ {/* Agency + freshness */}
{property.agency} - · - {lastSeenDays}d ago + {lastSeenDays !== null && ( + <> + · + {lastSeenDays}d ago + + )}
diff --git a/frontend/src/components/PropertyCardCompact.tsx b/frontend/src/components/PropertyCardCompact.tsx index 685734a..f6d0d99 100644 --- a/frontend/src/components/PropertyCardCompact.tsx +++ b/frontend/src/components/PropertyCardCompact.tsx @@ -16,8 +16,15 @@ export function PropertyCardCompact({ avgPricePerSqm, onClick, }: PropertyCardCompactProps) { - const isGoodDeal = avgPricePerSqm && property.qmprice > 0 && property.qmprice < avgPricePerSqm * 0.9; - const isExpensive = avgPricePerSqm && property.qmprice > avgPricePerSqm * 1.1; + // BUY listings may have null numeric fields; coerce so renders don't throw. + const safeNum = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0); + const safeTotalPrice = safeNum(property.total_price); + const safeQm = safeNum(property.qm); + const safeQmprice = safeNum(property.qmprice); + const safeRooms = safeNum(property.rooms); + + const isGoodDeal = avgPricePerSqm && safeQmprice > 0 && safeQmprice < avgPricePerSqm * 0.9; + const isExpensive = avgPricePerSqm && safeQmprice > avgPricePerSqm * 1.1; const priceIndicator = isGoodDeal ? { dotColor: 'bg-[var(--deal-good)]', label: 'Good deal' } @@ -37,7 +44,7 @@ export function PropertyCardCompact({ {property.photo_thumbnail && ( {`${property.rooms}-bed, )} @@ -48,7 +55,7 @@ export function PropertyCardCompact({ {/* Price bold */}
- £{property.total_price.toLocaleString()} + £{safeTotalPrice.toLocaleString()} {property.listing_type !== 'BUY' && ( /mo )} @@ -62,10 +69,10 @@ export function PropertyCardCompact({
- {property.rooms} bed + {safeRooms} bed · - {property.qm} m² + {safeQm} m²
{/* Location */} diff --git a/frontend/src/components/SwipeCard.tsx b/frontend/src/components/SwipeCard.tsx index c9883f6..e080fef 100644 --- a/frontend/src/components/SwipeCard.tsx +++ b/frontend/src/components/SwipeCard.tsx @@ -19,6 +19,12 @@ export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeC const hasSwiped = useRef(false); const p = feature.properties; const photos = p.photos?.length ? p.photos : p.photo_thumbnail ? [p.photo_thumbnail] : []; + // BUY listings may have null numeric fields; coerce so renders don't throw. + const safeNum = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0); + const safeTotalPrice = safeNum(p.total_price); + const safeQm = safeNum(p.qm); + const safeQmprice = safeNum(p.qmprice); + const safeRooms = safeNum(p.rooms); const prefersReducedMotion = useMemo( () => window.matchMedia('(prefers-reduced-motion: reduce)').matches, @@ -150,7 +156,7 @@ export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeC
) : photos.length === 1 ? ( - {`${p.rooms}-bed, + {`${safeRooms}-bed, ) : null}