From 812bfece4a4aaea72a2aed8d447f5bdf81c97517 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 28 Feb 2026 16:21:17 +0000 Subject: [PATCH] style: redesign PropertyCard with better visual hierarchy --- frontend/src/components/PropertyCard.tsx | 252 ++++++++---------- .../src/components/PropertyCardCompact.tsx | 45 ++-- 2 files changed, 132 insertions(+), 165 deletions(-) diff --git a/frontend/src/components/PropertyCard.tsx b/frontend/src/components/PropertyCard.tsx index db49492..7c0a5c6 100644 --- a/frontend/src/components/PropertyCard.tsx +++ b/frontend/src/components/PropertyCard.tsx @@ -1,9 +1,8 @@ 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 { ExternalLink, Heart, Bed, Maximize2, Clock, Footprints, Bike, Train } from 'lucide-react'; import type { PropertyProperties, POIDistanceInfo, POI } from '@/types'; -import { formatDate, formatDuration } from '@/utils/format'; +import { formatDuration } from '@/utils/format'; function TravelModeIcon({ mode }: { mode: string }) { switch (mode) { @@ -26,12 +25,12 @@ function POIDistanceBadges({ distances }: { distances: POIDistanceInfo[] }) { } return ( -
+
{Array.from(byPoi.entries()).map(([poiName, dists]) => ( -
+
{poiName}: {dists.map(d => ( - + {formatDuration(d.duration_seconds)} @@ -54,16 +53,16 @@ function AllPOIDistances({ pois, distances }: { pois: POI[]; distances?: POIDist } return ( -
+
{pois.map(poi => ( -
+
{poi.name}: {TRAVEL_MODES.map(mode => { const dist = distMap.get(`${poi.id}_${mode}`); return ( - + - {dist ? formatDuration(dist.duration_seconds) : '—'} + {dist ? formatDuration(dist.duration_seconds) : '\u2014'} ); })} @@ -154,9 +153,9 @@ export function PropertyCard({ const isExpensive = avgPricePerSqm && property.qmprice > avgPricePerSqm * 1.1; const priceIndicator = isGoodDeal - ? { color: 'text-green-600 bg-green-50', label: 'Good deal' } + ? { dotColor: 'bg-[var(--deal-good)]', label: 'Good deal' } : isExpensive - ? { color: 'text-red-600 bg-red-50', label: 'Above avg' } + ? { dotColor: 'bg-[var(--deal-above)]', label: 'Above avg' } : null; const handleClick = () => { @@ -166,7 +165,7 @@ export function PropertyCard({ if (variant === 'compact') { return (
-
-
+ {/* Price */} +
+ £{property.total_price.toLocaleString()} {property.listing_type !== 'BUY' && ( /mo )} -
+ {priceIndicator && ( - - {priceIndicator.label} - + + )} + {priceIndicator && ( + {priceIndicator.label} )}
-
- - - {property.rooms} - - - - {property.qm} m² - - - £{property.qmprice}/m² - + {/* Key metrics on one line */} +
+ {property.rooms}bed + · + {property.qm} m² + · + £{property.qmprice}/m²
-
- - - {lastSeenDays}d ago - - {property.agency} + {/* Agency + freshness */} +
+ {property.agency} + · + {lastSeenDays}d ago
+ + {/* POI badges */}
); } - // Full variant (for popup/detail view) + // Full variant return ( -
- {/* Header with image and price */} -
- - {property.photo_thumbnail && ( - {`${property.rooms}-bed, - )} - +
+ {/* Image section with 16:10 aspect ratio */} +
+ {(property.photos?.length || property.photo_thumbnail) ? ( + + ) : null} -
-
-
-
- £{property.total_price.toLocaleString()} - {property.listing_type !== 'BUY' && ( - /mo - )} -
- {priceIndicator && ( - - {priceIndicator.label} - - )} -
-
-
-
- - {/* Stats grid */} -
-
- - {property.rooms} bedrooms -
-
- - {property.qm} -
-
- - £{property.qmprice}/m² -
- {property.listing_type !== 'BUY' && property.available_from && ( -
- - Available {formatDate(property.available_from)} -
- )} - {property.listing_type === 'BUY' && ( -
- - Seen {lastSeenDays}d ago -
- )} -
- - {/* Agency and last seen */} -
- - {property.agency} - - Seen {lastSeenDays} days ago -
- - {/* POI Distances */} - {allPOIs && allPOIs.length > 0 ? ( -
-
Travel times
- -
- ) : property.poi_distances && property.poi_distances.length > 0 ? ( -
-
Travel times
- -
- ) : null} - - {/* Price history */} - {property.price_history.length > 1 && ( -
-
Price history
-
- {property.price_history.slice(0, 5).map((entry) => ( -
- {entry.last_seen.split('T')[0]} - £{entry.price.toLocaleString()} -
- ))} -
-
- )} - - {/* Actions */} - +
+ + {/* 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 +
); diff --git a/frontend/src/components/PropertyCardCompact.tsx b/frontend/src/components/PropertyCardCompact.tsx index 28e945c..685734a 100644 --- a/frontend/src/components/PropertyCardCompact.tsx +++ b/frontend/src/components/PropertyCardCompact.tsx @@ -1,4 +1,4 @@ -import { Bed, Maximize2 } from 'lucide-react'; +import { Bed, MapPin } from 'lucide-react'; import type { PropertyProperties } from '@/types'; interface PropertyCardCompactProps { @@ -20,20 +20,20 @@ export function PropertyCardCompact({ const isExpensive = avgPricePerSqm && property.qmprice > avgPricePerSqm * 1.1; const priceIndicator = isGoodDeal - ? { color: 'text-green-600 bg-green-50', label: 'Good deal' } + ? { dotColor: 'bg-[var(--deal-good)]', label: 'Good deal' } : isExpensive - ? { color: 'text-red-600 bg-red-50', label: 'Above avg' } + ? { dotColor: 'bg-[var(--deal-above)]', label: 'Above avg' } : null; return (
- {/* Thumbnail */} -
+ {/* Thumbnail with 4:3 aspect ratio */} +
{property.photo_thumbnail && ( {/* Details */} -
-
-
+
+ {/* Price bold */} +
+ £{property.total_price.toLocaleString()} {property.listing_type !== 'BUY' && ( /mo )} -
+ {priceIndicator && ( - - {priceIndicator.label} - + )}
-
+ {/* Beds and size */} +
- {property.rooms} + {property.rooms} bed - - - {property.qm} m² - - £{property.qmprice}/m² + · + {property.qm} m²
+ + {/* Location */} + {property.city && ( +
+ + {property.city} +
+ )}
);