Expand swipe card to 50/50 photo/details split with all info

- Card now fills available height with photo carousel in top half
  and property details in bottom half
- Details section shows: price, beds/sqm/price-per-sqm, agency,
  available date, all POI distances, and price history summary
- Fix DialogTitle accessibility warning in ListingDetailSheet and
  MobileBottomSheet (add sr-only Drawer.Title)
This commit is contained in:
Viktor Barzin 2026-02-22 00:49:32 +00:00
parent 611449d328
commit e2c22f025f
No known key found for this signature in database
GPG key ID: 0EB088298288D958
3 changed files with 50 additions and 11 deletions

View file

@ -46,6 +46,7 @@ export function ListingDetailSheet({
<Drawer.Portal> <Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40 z-50" /> <Drawer.Overlay className="fixed inset-0 bg-black/40 z-50" />
<Drawer.Content className="fixed bottom-0 left-0 right-0 z-50 flex flex-col bg-background rounded-t-xl max-h-[90vh]"> <Drawer.Content className="fixed bottom-0 left-0 right-0 z-50 flex flex-col bg-background rounded-t-xl max-h-[90vh]">
<Drawer.Title className="sr-only">Listing Details</Drawer.Title>
<div className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-muted-foreground/20 my-3" /> <div className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-muted-foreground/20 my-3" />
<div className="overflow-y-auto flex-1"> <div className="overflow-y-auto flex-1">
{isLoading && ( {isLoading && (

View file

@ -77,6 +77,7 @@ export function MobileBottomSheet({
className="fixed inset-x-0 bottom-0 z-40 flex flex-col rounded-t-xl bg-background border-t shadow-lg" className="fixed inset-x-0 bottom-0 z-40 flex flex-col rounded-t-xl bg-background border-t shadow-lg"
style={{ maxHeight: '85vh' }} style={{ maxHeight: '85vh' }}
> >
<Drawer.Title className="sr-only">Property Listings</Drawer.Title>
{/* Drag handle */} {/* Drag handle */}
<div className="flex justify-center pt-2 pb-1"> <div className="flex justify-center pt-2 pb-1">
<div className="h-1.5 w-10 rounded-full bg-muted-foreground/30" /> <div className="h-1.5 w-10 rounded-full bg-muted-foreground/30" />

View file

@ -2,7 +2,7 @@ import { useRef, useState, useCallback, useEffect } from 'react';
import { animated, useSpring } from '@react-spring/web'; import { animated, useSpring } from '@react-spring/web';
import { useDrag } from '@use-gesture/react'; import { useDrag } from '@use-gesture/react';
import useEmblaCarousel from 'embla-carousel-react'; import useEmblaCarousel from 'embla-carousel-react';
import { Bed, Maximize2, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'; import { Bed, Maximize2, ExternalLink, ChevronLeft, ChevronRight, Building2, Calendar } from 'lucide-react';
import type { PropertyFeature } from '@/types'; import type { PropertyFeature } from '@/types';
interface SwipeCardProps { interface SwipeCardProps {
@ -98,12 +98,13 @@ export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeC
touchAction: 'none', touchAction: 'none',
position: 'absolute' as const, position: 'absolute' as const,
width: '100%', width: '100%',
height: '100%',
zIndex: 10 - stackIndex, zIndex: 10 - stackIndex,
top: stackIndex * 8, top: stackIndex * 8,
}} }}
className="cursor-grab active:cursor-grabbing" className="cursor-grab active:cursor-grabbing"
> >
<div className="relative bg-background rounded-2xl border shadow-lg overflow-hidden mx-4"> <div className="relative bg-background rounded-2xl border shadow-lg overflow-hidden mx-4 h-full flex flex-col">
{/* Color overlay */} {/* Color overlay */}
{isTop && ( {isTop && (
<animated.div <animated.div
@ -112,8 +113,8 @@ export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeC
/> />
)} )}
{/* Photo carousel */} {/* Photo carousel — top half */}
<div className="h-48 bg-muted relative" style={{ touchAction: 'pan-x' }}> <div className="flex-1 min-h-0 bg-muted relative" style={{ touchAction: 'pan-x' }}>
{photos.length > 1 ? ( {photos.length > 1 ? (
<> <>
<div className="overflow-hidden h-full" ref={emblaRef}> <div className="overflow-hidden h-full" ref={emblaRef}>
@ -159,16 +160,18 @@ export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeC
</button> </button>
</div> </div>
{/* Details */} {/* Details — bottom half */}
<div className="p-4"> <div className="flex-1 min-h-0 p-4 flex flex-col gap-3 overflow-y-auto">
<div className="text-xl font-semibold"> {/* Price */}
<div className="text-2xl font-semibold">
£{p.total_price.toLocaleString()} £{p.total_price.toLocaleString()}
{p.listing_type !== 'BUY' && ( {p.listing_type !== 'BUY' && (
<span className="text-muted-foreground font-normal text-sm">/mo</span> <span className="text-muted-foreground font-normal text-base">/mo</span>
)} )}
</div> </div>
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground"> {/* Key stats */}
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Bed className="h-4 w-4" /> {p.rooms} bed <Bed className="h-4 w-4" /> {p.rooms} bed
</span> </span>
@ -178,10 +181,24 @@ export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeC
<span>£{p.qmprice}/m²</span> <span>£{p.qmprice}/m²</span>
</div> </div>
{/* Agency & availability */}
<div className="flex flex-col gap-1.5 text-sm">
{p.agency && (
<span className="flex items-center gap-1.5 text-muted-foreground">
<Building2 className="h-3.5 w-3.5 flex-shrink-0" /> {p.agency}
</span>
)}
{p.available_from && (
<span className="flex items-center gap-1.5 text-muted-foreground">
<Calendar className="h-3.5 w-3.5 flex-shrink-0" /> Available {p.available_from}
</span>
)}
</div>
{/* POI distances */} {/* POI distances */}
{p.poi_distances && p.poi_distances.length > 0 && ( {p.poi_distances && p.poi_distances.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-3"> <div className="flex flex-wrap gap-1.5">
{p.poi_distances.slice(0, 4).map((d) => ( {p.poi_distances.map((d) => (
<span <span
key={`${d.poi_id}_${d.travel_mode}`} key={`${d.poi_id}_${d.travel_mode}`}
className="text-xs bg-muted px-2 py-0.5 rounded" className="text-xs bg-muted px-2 py-0.5 rounded"
@ -191,6 +208,26 @@ export function SwipeCard({ feature, onSwipe, onTap, isTop, stackIndex }: SwipeC
))} ))}
</div> </div>
)} )}
{/* Price history */}
{p.price_history && p.price_history.length > 1 && (
<div className="text-xs text-muted-foreground">
{p.price_history.length} price changes
{(() => {
const first = p.price_history[0]?.price;
const last = p.price_history[p.price_history.length - 1]?.price;
if (first && last && first !== last) {
const diff = last - first;
return (
<span className={diff < 0 ? 'text-green-600 ml-1' : 'text-red-500 ml-1'}>
({diff < 0 ? '' : '+'}£{diff.toLocaleString()})
</span>
);
}
return null;
})()}
</div>
)}
</div> </div>
</div> </div>
</animated.div> </animated.div>