Files
chat_room/src/components/discover/SwipeCard.tsx
T
mobiniqow 93f6ea2f07 Initial commit: dating app web frontend (Next.js)
Phone+OTP auth, staged onboarding, admin-approval-gated home screen,
and the full discovery/match/chat UI (swipe deck with drag gestures,
match modal, profile detail, filters, messages/chat) wired to the
real backend, styled after a Figma dating-app UI kit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 23:25:42 +03:30

119 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}