2026-09-15 23:24:34 +03:30
|
|
|
|
"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");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 22:17:29 +03:30
|
|
|
|
const photo = profile.photos?.[0];
|
2026-09-15 23:24:34 +03:30
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|