Files
chat_room/src/components/discover/SwipeCard.tsx
T

119 lines
4.2 KiB
TypeScript
Raw Normal View History

"use client";
import { useRouter } from "next/navigation";
import { motion, useMotionValue, useTransform, PanInfo } from "framer-motion";
import { MapPin, User } from "lucide-react";
import { DiscoverCandidate, photoUrl } from "@/lib/api";
import { formatDistance } from "@/lib/utils";
const SWIPE_THRESHOLD = 120;
export function SwipeCard({
profile,
isTop,
stackIndex,
onDecide,
}: {
profile: DiscoverCandidate;
isTop: boolean;
stackIndex: number;
onDecide: (direction: "left" | "right") => void;
}) {
const router = useRouter();
const x = useMotionValue(0);
const rotate = useTransform(x, [-260, 260], [-16, 16]);
const likeOpacity = useTransform(x, [20, 120], [0, 1]);
const nopeOpacity = useTransform(x, [-120, -20], [1, 0]);
function handleDragEnd(_: unknown, info: PanInfo) {
if (info.offset.x > SWIPE_THRESHOLD) {
onDecide("right");
} else if (info.offset.x < -SWIPE_THRESHOLD) {
onDecide("left");
}
}
const photo = profile.photos?.[0];
return (
<motion.div
className="absolute inset-0"
style={
isTop
? { x, rotate }
: {
scale: 1 - stackIndex * 0.04,
y: stackIndex * 10,
}
}
drag={isTop ? "x" : false}
dragElastic={0.9}
onDragEnd={isTop ? handleDragEnd : undefined}
initial={{ scale: 1 - stackIndex * 0.04, y: stackIndex * 10, opacity: 0 }}
animate={{ scale: 1 - stackIndex * 0.04, y: stackIndex * 10, opacity: 1 }}
exit={{
x: x.get() > 0 ? 400 : x.get() < 0 ? -400 : 0,
opacity: 0,
transition: { duration: 0.3 },
}}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
<div
onClick={() => isTop && router.push(`/home/${profile.id}`)}
className="relative w-full h-full rounded-(--radius-xl) overflow-hidden shadow-(--shadow-cute-lg) bg-(--color-bg-tertiary) cursor-pointer select-none"
>
{photo ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={photoUrl(photo)}
alt={profile.first_name}
draggable={false}
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center bg-(--color-bg-tertiary) text-(--color-content-disabled) pointer-events-none">
<User size={64} strokeWidth={1.2} />
</div>
)}
<div className="absolute inset-x-0 bottom-0 h-2/3 bg-gradient-to-t from-black/85 via-black/30 to-transparent pointer-events-none" />
{isTop && (
<>
<motion.span
style={{ opacity: likeOpacity }}
className="absolute top-8 right-6 rotate-[-12deg] border-4 border-(--color-status-success) text-(--color-status-success) font-[var(--font-display)] font-extrabold text-[26px] px-3 py-1 rounded-(--radius-sm) bg-white/70"
>
لایک
</motion.span>
<motion.span
style={{ opacity: nopeOpacity }}
className="absolute top-8 left-6 rotate-[12deg] border-4 border-(--color-status-error) text-(--color-status-error) font-[var(--font-display)] font-extrabold text-[26px] px-3 py-1 rounded-(--radius-sm) bg-white/70"
>
ردّ
</motion.span>
</>
)}
<div className="absolute inset-x-0 bottom-0 p-5 pointer-events-none">
<div className="flex items-center gap-2">
<h2 className="font-[var(--font-display)] font-extrabold text-[24px] text-white">
{profile.first_name || "بدون نام"}
</h2>
<span className="font-[var(--font-display)] font-bold text-[20px] text-white/90">
{profile.age}
</span>
</div>
{profile.distance_km != null && (
<div className="flex items-center gap-1.5 mt-1.5">
<MapPin size={14} className="text-white/80" />
<span className="text-[13px] text-white/80">
{formatDistance(profile.distance_km)}
</span>
</div>
)}
</div>
</div>
</motion.div>
);
}