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:
62
apps/web/components/comparison/add-to-compare-button.tsx
Normal file
62
apps/web/components/comparison/add-to-compare-button.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { BarChart3, Check } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Button, type ButtonProps } from '@/components/ui/button';
|
||||
import { useComparisonStore } from '@/lib/comparison-store';
|
||||
|
||||
interface AddToCompareButtonProps extends Omit<ButtonProps, 'onClick'> {
|
||||
listingId: string;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function AddToCompareButton({ listingId, compact, ...props }: AddToCompareButtonProps) {
|
||||
const t = useTranslations('compare');
|
||||
const isSelected = useComparisonStore((s) => s.isSelected(listingId));
|
||||
const addToCompare = useComparisonStore((s) => s.addToCompare);
|
||||
const removeFromCompare = useComparisonStore((s) => s.removeFromCompare);
|
||||
const canAdd = useComparisonStore((s) => s.canAdd());
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isSelected) {
|
||||
removeFromCompare(listingId);
|
||||
} else {
|
||||
addToCompare(listingId);
|
||||
}
|
||||
};
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={!isSelected && !canAdd}
|
||||
className={`inline-flex items-center justify-center rounded-full p-1.5 transition-colors ${
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-background/80 text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
} ${!isSelected && !canAdd ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
aria-label={isSelected ? t('removeFromCompare') : t('addToCompare')}
|
||||
title={isSelected ? t('removeFromCompare') : t('addToCompare')}
|
||||
>
|
||||
{isSelected ? <Check className="h-4 w-4" /> : <BarChart3 className="h-4 w-4" />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={handleClick}
|
||||
disabled={!isSelected && !canAdd}
|
||||
className="gap-1.5"
|
||||
aria-label={isSelected ? t('removeFromCompare') : t('addToCompare')}
|
||||
{...props}
|
||||
>
|
||||
{isSelected ? <Check className="h-4 w-4" /> : <BarChart3 className="h-4 w-4" />}
|
||||
{isSelected ? t('added') : t('addToCompare')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user