feat(web): add property comparison page with side-by-side view
Build a complete property comparison feature at /compare: - Zustand store with localStorage persistence for selected listings (2-5) - Side-by-side comparison table (price, area, price/m², amenities, location, etc.) - Summary statistics banner (price range, area range, price/m² range) - "Add to Compare" button on property cards and detail pages - Floating comparison bar for quick access when listings are selected - Bilingual i18n support (Vietnamese + English) - 18 unit tests for store logic and comparison stats computation - Mobile-responsive layout with horizontal scroll on comparison table Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
68
apps/web/components/comparison/comparison-stats.tsx
Normal file
68
apps/web/components/comparison/comparison-stats.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import type { ComparisonStats } from '@/lib/comparison-store';
|
||||
import { formatPrice, formatPricePerM2 } from '@/lib/currency';
|
||||
|
||||
interface ComparisonStatsBannerProps {
|
||||
stats: ComparisonStats;
|
||||
}
|
||||
|
||||
export function ComparisonStatsBanner({ stats }: ComparisonStatsBannerProps) {
|
||||
const t = useTranslations('compare');
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{t('priceRange')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-lg font-bold text-primary">
|
||||
{formatPrice(stats.priceRange.min)} — {formatPrice(stats.priceRange.max)}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t('average')}: {formatPrice(stats.priceRange.avg)} VND
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{t('areaRange')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-lg font-bold">
|
||||
{stats.areaRange.min} — {stats.areaRange.max} m²
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t('average')}: {stats.areaRange.avg} m²
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{stats.pricePerM2Range && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{t('pricePerM2Range')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-lg font-bold">
|
||||
{formatPricePerM2(stats.pricePerM2Range.min)} — {formatPricePerM2(stats.pricePerM2Range.max)}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t('average')}: {formatPricePerM2(stats.pricePerM2Range.avg)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user