"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { api } from "@/lib/api"; import { useAuth } from "@/lib/auth"; import { useToast } from "@/lib/toast"; import { useI18n } from "@/lib/i18n"; import type { MatchIntent, MatchProfile, MatchQuota } from "@/lib/types"; import RingNext from "@/components/RingNext"; import { useRingSteps } from "@/lib/rings"; const SWIPE_THRESHOLD = 110; const SUPER_THRESHOLD = -120; export default function DatingClient() { const { user, token } = useAuth(); const router = useRouter(); const { toast } = useToast(); const { t } = useI18n(); const rings = useRingSteps(); const [intent, setIntent] = useState("friends"); const [moreIntents, setMoreIntents] = useState(false); const [deck, setDeck] = useState([]); const [quota, setQuota] = useState(null); const [loading, setLoading] = useState(true); const [joined, setJoined] = useState(false); const [error, setError] = useState(""); const [matchModal, setMatchModal] = useState<{ conversationId?: string; name: string } | null>(null); const [drag, setDrag] = useState({ x: 0, y: 0, active: false, flying: "" }); const startRef = useRef<{ x: number; y: number } | null>(null); const dragRef = useRef({ x: 0, y: 0 }); const swipingRef = useRef(false); const cardRef = useRef(null); const intents = useMemo( () => [ { key: "friends" as const, label: t.dating.intentFriends, emoji: "๐Ÿค" }, { key: "dating" as const, label: t.dating.intentDating, emoji: "๐Ÿ’•" }, { key: "partner" as const, label: t.dating.intentPartner, emoji: "๐Ÿ’‘" }, { key: "roommate" as const, label: t.dating.intentRoommate, emoji: "๐Ÿ " }, { key: "cofounder" as const, label: t.dating.intentCofounder, emoji: "๐Ÿš€" }, { key: "explore" as const, label: t.dating.intentExplore, emoji: "๐ŸŒ" }, ], [t] ); const primary = intents.slice(0, 3); const quotaBlocked = Boolean(quota && !quota.vip && quota.remaining <= 0); const ensureJoined = useCallback(async () => { if (!token) return; try { await api.socialJoin(token, { city: t.dating.defaultCity, lookingFor: [intent, "explore"], bio: t.dating.defaultBio, }); } catch { // Profile may already exist. } }, [token, intent, t]); const load = useCallback(async () => { if (!token) { setLoading(false); return; } setLoading(true); setError(""); try { await ensureJoined(); const [candidates, q] = await Promise.all([ api.getMatchCandidates(token, { intent }), api.getMatchQuota(token), ]); setDeck(Array.isArray(candidates) ? candidates : []); setQuota(q); setJoined(true); setDrag({ x: 0, y: 0, active: false, flying: "" }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); setError(msg); setJoined(false); setDeck([]); } finally { setLoading(false); } }, [token, intent, ensureJoined]); useEffect(() => { void load(); }, [load]); const quickJoin = async () => { if (!token) { router.push("/login?next=/dating"); return; } setLoading(true); setError(""); try { await api.socialJoin(token, { city: t.dating.defaultCity, lookingFor: [intent, "explore"], bio: t.dating.defaultBio, }); toast(t.dating.joinOk, "success", { href: "/meetups", label: t.dating.meetAtEvents, }); await load(); } catch (err) { const msg = err instanceof Error ? err.message : t.dating.joinFail; setError(msg); toast(t.dating.joinFail, "error"); setLoading(false); } }; const swipe = useCallback( async (action: "like" | "dislike" | "superlike") => { if (!token || !deck[0] || swipingRef.current || quotaBlocked) return; swipingRef.current = true; const current = deck[0]; const fly = action === "like" ? "right" : action === "dislike" ? "left" : "up"; setDrag({ x: 0, y: 0, active: false, flying: fly }); try { const res = await api.swipe(token, current.id, action, intent); if (res.matched && res.match?.conversationId) { setMatchModal({ conversationId: res.match.conversationId, name: current.name }); } window.setTimeout(() => { setDeck((d) => d.slice(1)); setDrag({ x: 0, y: 0, active: false, flying: "" }); swipingRef.current = false; }, 220); const q = await api.getMatchQuota(token); setQuota(q); } catch { setDrag({ x: 0, y: 0, active: false, flying: "" }); swipingRef.current = false; toast(t.dating.swipeFail, "error"); } }, [token, deck, intent, quotaBlocked, toast, t] ); const onPointerDown = (e: React.PointerEvent) => { if (quotaBlocked || swipingRef.current) return; const target = e.target as HTMLElement; if (target.closest("button,a,input")) return; startRef.current = { x: e.clientX, y: e.clientY }; dragRef.current = { x: 0, y: 0 }; setDrag({ x: 0, y: 0, active: true, flying: "" }); cardRef.current?.setPointerCapture(e.pointerId); }; const onPointerMove = (e: React.PointerEvent) => { if (!startRef.current || !drag.active) return; const x = e.clientX - startRef.current.x; const y = e.clientY - startRef.current.y; dragRef.current = { x, y }; setDrag({ x, y, active: true, flying: "" }); }; const endDrag = () => { if (!startRef.current) return; const { x, y } = dragRef.current; startRef.current = null; if (Math.abs(x) > SWIPE_THRESHOLD) { void swipe(x > 0 ? "like" : "dislike"); return; } if (y < SUPER_THRESHOLD) { void swipe("superlike"); return; } setDrag({ x: 0, y: 0, active: false, flying: "" }); }; useEffect(() => { const onKey = (e: KeyboardEvent) => { if (!deck[0] || matchModal || quotaBlocked) return; if (e.key === "ArrowLeft") void swipe("dislike"); if (e.key === "ArrowRight") void swipe("like"); if (e.key === "ArrowUp") void swipe("superlike"); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [deck, matchModal, quotaBlocked, swipe]); const top = deck[0]; const next = deck[1]; const rotate = drag.x * 0.06; const likeOpacity = Math.min(1, Math.max(0, drag.x / SWIPE_THRESHOLD)); const nopeOpacity = Math.min(1, Math.max(0, -drag.x / SWIPE_THRESHOLD)); const superOpacity = Math.min(1, Math.max(0, -drag.y / Math.abs(SUPER_THRESHOLD))); if (!user) { return (

{t.dating.loginTitle}

{t.dating.loginDesc}

{t.nav.login} {t.dating.meetAtEvents} {t.nav.community}
); } return (
{t.dating.tag}

{t.dating.title}

{t.dating.subtitle}

{(moreIntents ? intents : primary).map((i) => ( ))} {!moreIntents && ( )}
{quota && (

{quota.vip ? t.dating.vipUnlimited : t.dating.quotaLeft .replace("{remaining}", String(quota.remaining)) .replace("{limit}", String(quota.limit))} {!quota.vip && quota.remaining <= 0 && ( <> {" ยท "} {t.dating.vipUnlock} )}

)} {loading && (

{joined ? t.dating.loadingCandidates : t.dating.loadingProfile}

)} {!joined && !loading && (

{t.dating.joinFirst}

{error &&

{error}

}
)} {top && ( <>

{t.dating.swipeHint}

{next && (
{next.photo || "๐Ÿง‘โ€๐Ÿ’ป"}

{next.name}

)}
LIKE NOPE SUPER
{top.photo || "๐Ÿง‘โ€๐Ÿ’ป"}

{top.name}

{top.location} ยท {top.gender} {top.single ? `ยท ${top.single}` : ""}

{top.bio}

{(top.tags || []).map((tag) => ( {tag} ))}
{token && ( )}
)} {!loading && joined && !top && (

{t.dating.empty}

{t.dating.meetAtEvents} {t.nav.community} {t.dating.viewChats} {quota && !quota.vip && ( {t.dating.openVip} )}
)}
{matchModal && (

๐ŸŽ‰ {t.dating.matched} {matchModal.name}!

{matchModal.conversationId && ( {t.dating.chatNow} )} setMatchModal(null)}> {t.dating.meetAtEvents}
)}
); }