wrongmove: guard property cards against null backend fields (fix BUY crash)

BUY listings can come back from the API with null `total_price`, `qm`,
`qmprice`, `rooms`, and `last_seen` even though the TypeScript types
declare them non-nullable. The cards called `.toLocaleString()` and
`.split('T')` on those values, throwing TypeError and tripping the
ErrorBoundary — which is the "BUY part of the page crashes" symptom.

Coerce numeric fields via a `safeNum` helper (0 fallback), gate the
"Nd ago" line on a finite last_seen, and apply the same guards in
PropertyCard, PropertyCardCompact, SwipeCard, and the price-history
section of ListingDetail. Added regression tests asserting both card
variants render with all-null backend fields without throwing.
This commit is contained in:
Viktor Barzin 2026-05-10 21:17:41 +00:00
parent 73823bd381
commit 0b5308200e
6 changed files with 97 additions and 30 deletions

View file

@ -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) ? (
<CardCarousel
photos={property.photos?.length ? property.photos : [property.photo_thumbnail]}
altText={`${property.rooms}-bed, ${property.qm}m², £${property.total_price.toLocaleString()}`}
altText={`${safeRooms}-bed, ${safeQm}m², £${safeTotalPrice.toLocaleString()}`}
/>
) : null}
</div>
@ -185,7 +195,7 @@ export function PropertyCard({
{/* Price */}
<div className="flex items-center gap-1.5">
<span className="text-lg font-bold tracking-tight">
£{property.total_price.toLocaleString()}
£{safeTotalPrice.toLocaleString()}
{property.listing_type !== 'BUY' && (
<span className="text-muted-foreground font-normal text-sm">/mo</span>
)}
@ -200,18 +210,22 @@ export function PropertyCard({
{/* Key metrics on one line */}
<div className="flex items-center gap-1 text-sm text-muted-foreground mt-0.5">
<span>{property.rooms}</span><span>bed</span>
<span>{safeRooms}</span><span>bed</span>
<span>·</span>
<span>{property.qm} m²</span>
<span>{safeQm} m²</span>
<span>·</span>
<span>£{property.qmprice}/m²</span>
<span>£{safeQmprice}/m²</span>
</div>
{/* Agency + freshness */}
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-1">
<span>{property.agency}</span>
<span>·</span>
<span>{lastSeenDays}d ago</span>
{lastSeenDays !== null && (
<>
<span>·</span>
<span>{lastSeenDays}d ago</span>
</>
)}
</div>
{/* POI badges */}
@ -234,7 +248,7 @@ export function PropertyCard({
{(property.photos?.length || property.photo_thumbnail) ? (
<CardCarousel
photos={property.photos?.length ? property.photos : [property.photo_thumbnail]}
altText={`${property.rooms}-bed, ${property.qm}m², £${property.total_price.toLocaleString()}`}
altText={`${safeRooms}-bed, ${safeQm}m², £${safeTotalPrice.toLocaleString()}`}
/>
) : null}
@ -257,7 +271,7 @@ export function PropertyCard({
{/* Price as dominant element */}
<div className="flex items-center gap-1.5">
<span className="text-lg font-bold tracking-tight">
£{property.total_price.toLocaleString()}
£{safeTotalPrice.toLocaleString()}
{property.listing_type !== 'BUY' && (
<span className="text-muted-foreground font-normal text-sm">/mo</span>
)}
@ -272,11 +286,11 @@ export function PropertyCard({
{/* Key metrics on one line */}
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<span>{property.rooms}</span><span>bed</span>
<span>{safeRooms}</span><span>bed</span>
<span>·</span>
<span>{property.qm} m²</span>
<span>{safeQm} m²</span>
<span>·</span>
<span>£{property.qmprice}/m²</span>
<span>£{safeQmprice}/m²</span>
</div>
{/* Location */}
@ -294,8 +308,12 @@ export function PropertyCard({
{/* Agency + freshness */}
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<span>{property.agency}</span>
<span>·</span>
<span>{lastSeenDays}d ago</span>
{lastSeenDays !== null && (
<>
<span>·</span>
<span>{lastSeenDays}d ago</span>
</>
)}
</div>
</div>
</div>