wrongmove/frontend/src/components/SwipeablePropertyCard.tsx

123 lines
3.2 KiB
TypeScript
Raw Normal View History

import { useRef, useState } from 'react';
import { useDrag } from '@use-gesture/react';
import { useSpring, animated } from '@react-spring/web';
import { Heart, X } from 'lucide-react';
import { PropertyCard } from './PropertyCard';
import type { PropertyProperties, POI, DecisionType } from '@/types';
interface SwipeablePropertyCardProps {
property: PropertyProperties;
variant?: 'compact' | 'full';
isHighlighted?: boolean;
avgPricePerSqm?: number;
allPOIs?: POI[];
onClick?: () => void;
onSwipeRight?: () => void;
onSwipeLeft?: () => void;
decision?: DecisionType | null;
}
const SWIPE_THRESHOLD = 80;
export function SwipeablePropertyCard({
property,
variant = 'compact',
isHighlighted = false,
avgPricePerSqm,
allPOIs,
onClick,
onSwipeRight,
onSwipeLeft,
decision,
}: SwipeablePropertyCardProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [gone, setGone] = useState(false);
const [{ x, opacity }, api] = useSpring(() => ({
x: 0,
opacity: 1,
config: { tension: 200, friction: 20 },
}));
const bind = useDrag(
({ active, movement: [mx], direction: [dx] }) => {
if (gone) return;
// If dragging vertically more than horizontally, cancel (let scroll work)
if (active && Math.abs(mx) < 15) return;
if (!active) {
// Released
if (Math.abs(mx) > SWIPE_THRESHOLD) {
// Swipe confirmed
setGone(true);
const dir = dx > 0 ? 1 : -1;
api.start({
x: dir * 400,
opacity: 0,
onRest: () => {
if (dir > 0) onSwipeRight?.();
else onSwipeLeft?.();
},
});
} else {
// Spring back
api.start({ x: 0, opacity: 1 });
}
} else {
api.start({ x: mx, opacity: 1 - Math.abs(mx) / 400, immediate: true });
}
},
{
axis: 'x',
filterTaps: true,
from: () => [x.get(), 0],
},
);
if (gone) return null;
const likedBadge = decision === 'liked';
return (
<div className="relative overflow-hidden" ref={containerRef}>
{/* Background indicators */}
<div className="absolute inset-0 flex items-center justify-between px-6 pointer-events-none">
<div className="flex items-center gap-2 text-destructive">
<X className="h-8 w-8" />
</div>
<div className="flex items-center gap-2 text-green-600">
<Heart className="h-8 w-8" />
</div>
</div>
{/* Card */}
<animated.div
{...bind()}
style={{
x,
opacity,
touchAction: 'pan-y',
}}
className="relative"
>
<div className="relative">
{likedBadge && (
<div className="absolute top-1 right-1 z-10 bg-green-600 text-white rounded-full p-1">
<Heart className="h-3 w-3 fill-current" />
</div>
)}
<PropertyCard
property={property}
variant={variant}
isHighlighted={isHighlighted}
avgPricePerSqm={avgPricePerSqm}
allPOIs={allPOIs}
onClick={onClick}
/>
</div>
</animated.div>
</div>
);
}