s''
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "@/i18n/navigation";
|
||||
import Footer from "@/app/components/Footer";
|
||||
import { apiFetch } from "@/app/lib/api-client";
|
||||
|
||||
type TabType = "friends" | "dating" | "partner" | "roommate" | "cofounder" | "explore";
|
||||
|
||||
@@ -29,7 +30,17 @@ interface MatchCard {
|
||||
photo: string;
|
||||
}
|
||||
|
||||
const MOCK_PROFILES: Profile[] = [
|
||||
interface ProfileRecord {
|
||||
id: string;
|
||||
name?: string;
|
||||
age?: number;
|
||||
location?: string;
|
||||
tags?: string[];
|
||||
bio?: string;
|
||||
photo?: string;
|
||||
}
|
||||
|
||||
const FALLBACK_PROFILES: Profile[] = [
|
||||
{ id: "1", name: "林晓雨", age: 28, location: "大理", tags: ["本科学历", "远程工作者", "数字游民", "咖啡爱好者"], bio: "数字游民三年,目前在云南大理远程做产品设计。喜欢爬山、摄影,周末常去洱海边发呆。", gradient: "from-rose-400 via-pink-400 to-fuchsia-400", initial: "林", avatarColor: "bg-rose-500", photo: "https://i.pravatar.cc/150?img=1" },
|
||||
{ id: "2", name: "陈浩然", age: 32, location: "成都", tags: ["硕士学历", "独立开发者", "自由职业者", "素食主义"], bio: "在成都住了两年,做独立开发。喜欢冲浪和冥想,寻找志同道合的旅伴一起探索世界。", gradient: "from-amber-400 via-orange-400 to-red-400", initial: "陈", avatarColor: "bg-amber-500", photo: "https://i.pravatar.cc/150?img=3" },
|
||||
{ id: "3", name: "王思琪", age: 26, location: "深圳", tags: ["本科学历", "内容创作者", "数字游民", "环保主义者"], bio: "自由撰稿人,在深圳写写画画。热爱瑜伽和冥想,相信慢生活才是真正的奢侈。", gradient: "from-emerald-400 via-teal-400 to-cyan-400", initial: "王", avatarColor: "bg-emerald-500", photo: "https://i.pravatar.cc/150?img=5" },
|
||||
@@ -38,7 +49,7 @@ const MOCK_PROFILES: Profile[] = [
|
||||
{ id: "6", name: "刘子轩", age: 33, location: "昆明", tags: ["博士学历", "数据科学家", "远程工作者", "户外爱好者"], bio: "在昆明远程工作,热爱户外运动。正在探索云南各地,希望认识更多志同道合的朋友。", gradient: "from-blue-400 via-indigo-400 to-violet-500", initial: "刘", avatarColor: "bg-blue-500", photo: "https://i.pravatar.cc/150?img=11" },
|
||||
];
|
||||
|
||||
const MOCK_MATCHES: MatchCard[] = [
|
||||
const FALLBACK_MATCHES: MatchCard[] = [
|
||||
{ id: "m1", name: "周雨桐", location: "杭州", timeAgo: "2天前", initial: "周", color: "bg-pink-400", photo: "https://i.pravatar.cc/150?img=29" },
|
||||
{ id: "m2", name: "吴俊杰", location: "成都", timeAgo: "1周前", initial: "吴", color: "bg-amber-400", photo: "https://i.pravatar.cc/150?img=34" },
|
||||
{ id: "m3", name: "郑诗涵", location: "上海", timeAgo: "3周前", initial: "郑", color: "bg-emerald-400", photo: "https://i.pravatar.cc/150?img=36" },
|
||||
@@ -48,20 +59,123 @@ const MOCK_MATCHES: MatchCard[] = [
|
||||
|
||||
const TAB_ORDER: TabType[] = ["friends", "dating", "partner", "roommate", "cofounder", "explore"];
|
||||
|
||||
const PROFILE_GRADIENTS = [
|
||||
"from-rose-400 via-pink-400 to-fuchsia-400",
|
||||
"from-amber-400 via-orange-400 to-red-400",
|
||||
"from-emerald-400 via-teal-400 to-cyan-400",
|
||||
"from-violet-400 via-purple-400 to-indigo-400",
|
||||
"from-blue-400 via-indigo-400 to-violet-500",
|
||||
];
|
||||
|
||||
function mapProfile(record: ProfileRecord, index: number): Profile {
|
||||
const name = record.name || "数字游民";
|
||||
const initial = name.slice(0, 1);
|
||||
return {
|
||||
id: record.id,
|
||||
name,
|
||||
age: Number(record.age || 28 + index),
|
||||
location: record.location || "远程",
|
||||
tags: Array.isArray(record.tags) ? record.tags : ["数字游民", "远程工作者"],
|
||||
bio: record.bio || "正在完善个人资料,期待结识同城和同路的远程工作伙伴。",
|
||||
gradient: PROFILE_GRADIENTS[index % PROFILE_GRADIENTS.length],
|
||||
initial,
|
||||
avatarColor: "bg-[#ff4d4f]",
|
||||
photo: record.photo || `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(name)}`,
|
||||
};
|
||||
}
|
||||
|
||||
function toMatch(profile: Profile, index: number): MatchCard {
|
||||
return {
|
||||
id: `m-${profile.id}`,
|
||||
name: profile.name,
|
||||
location: profile.location,
|
||||
timeAgo: index < 2 ? "刚刚活跃" : "本周活跃",
|
||||
initial: profile.initial,
|
||||
color: profile.avatarColor,
|
||||
photo: profile.photo,
|
||||
};
|
||||
}
|
||||
|
||||
export default function DatingPage() {
|
||||
const { t } = useTranslation("match");
|
||||
const [activeTab, setActiveTab] = useState<TabType>("friends");
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [profiles, setProfiles] = useState<Profile[]>(FALLBACK_PROFILES);
|
||||
const [dragX, setDragX] = useState(0);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isAnimatingOut, setIsAnimatingOut] = useState<"left" | "right" | null>(null);
|
||||
const startXRef = useRef(0);
|
||||
|
||||
const currentProfile = MOCK_PROFILES[currentIndex];
|
||||
const hasMoreProfiles = currentIndex < MOCK_PROFILES.length - 1;
|
||||
useEffect(() => {
|
||||
apiFetch<{ items: ProfileRecord[] }>("/api/profiles")
|
||||
.then((data) => {
|
||||
if (data.items?.length) {
|
||||
setProfiles(data.items.map(mapProfile));
|
||||
setCurrentIndex(0);
|
||||
}
|
||||
})
|
||||
.catch(() => setProfiles(FALLBACK_PROFILES));
|
||||
}, []);
|
||||
|
||||
const matches = useMemo(
|
||||
() => (profiles.length ? profiles.slice(0, 5).map(toMatch) : FALLBACK_MATCHES),
|
||||
[profiles]
|
||||
);
|
||||
const currentProfile = profiles[currentIndex] || profiles[0] || FALLBACK_PROFILES[0];
|
||||
const hasMoreProfiles = currentIndex < profiles.length - 1;
|
||||
|
||||
const saveSwipe = (action: "like" | "dislike") => {
|
||||
if (!currentProfile) return;
|
||||
apiFetch("/api/matches/swipes", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ profileId: currentProfile.id, action }),
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const nextProfile = () => {
|
||||
window.setTimeout(() => {
|
||||
setCurrentIndex((i) => (hasMoreProfiles ? i + 1 : 0));
|
||||
setDragX(0);
|
||||
setIsAnimatingOut(null);
|
||||
}, 160);
|
||||
};
|
||||
|
||||
const handleLike = () => {
|
||||
setCurrentIndex((i) => (hasMoreProfiles ? i + 1 : 0));
|
||||
saveSwipe("like");
|
||||
setIsAnimatingOut("right");
|
||||
setDragX(260);
|
||||
nextProfile();
|
||||
};
|
||||
|
||||
const handleDislike = () => {
|
||||
setCurrentIndex((i) => (hasMoreProfiles ? i + 1 : 0));
|
||||
saveSwipe("dislike");
|
||||
setIsAnimatingOut("left");
|
||||
setDragX(-260);
|
||||
nextProfile();
|
||||
};
|
||||
|
||||
const beginDrag = (clientX: number) => {
|
||||
startXRef.current = clientX;
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const moveDrag = (clientX: number) => {
|
||||
if (!isDragging) return;
|
||||
setDragX(Math.max(-180, Math.min(180, clientX - startXRef.current)));
|
||||
};
|
||||
|
||||
const endDrag = () => {
|
||||
if (!isDragging) return;
|
||||
setIsDragging(false);
|
||||
if (dragX > 90) {
|
||||
handleLike();
|
||||
return;
|
||||
}
|
||||
if (dragX < -90) {
|
||||
handleDislike();
|
||||
return;
|
||||
}
|
||||
setDragX(0);
|
||||
};
|
||||
|
||||
const badgeMap: Record<TabType, string> = {
|
||||
@@ -100,7 +214,7 @@ export default function DatingPage() {
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">{t("title")} 💕</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">{t("likedYou")}</p>
|
||||
<div className="space-y-3">
|
||||
{MOCK_MATCHES.map((match) => (
|
||||
{matches.map((match) => (
|
||||
<div key={match.id} className="flex items-center gap-3 p-3 rounded-xl bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 shadow-sm hover:shadow-md transition-shadow cursor-pointer">
|
||||
<img src={match.photo} alt={match.name} className="w-12 h-12 rounded-full object-cover shadow-sm flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -116,7 +230,32 @@ export default function DatingPage() {
|
||||
|
||||
<main className="flex-1 min-w-0 flex flex-col items-center">
|
||||
<div className="w-full max-w-md mx-auto">
|
||||
<div className={`relative rounded-2xl overflow-hidden bg-gradient-to-br ${currentProfile.gradient} p-8 pb-6 shadow-xl border border-white/20`}>
|
||||
<div
|
||||
className={`relative rounded-2xl overflow-hidden bg-gradient-to-br ${currentProfile.gradient} p-8 pb-6 shadow-xl border border-white/20 touch-pan-y select-none ${isDragging ? "cursor-grabbing" : "cursor-grab"} transition-transform`}
|
||||
style={{
|
||||
transform: `translateX(${isAnimatingOut === "right" ? 420 : isAnimatingOut === "left" ? -420 : dragX}px) rotate(${dragX / 18}deg)`,
|
||||
opacity: isAnimatingOut ? 0.25 : 1,
|
||||
transitionDuration: isDragging ? "0ms" : "180ms",
|
||||
}}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowRight") handleLike();
|
||||
if (e.key === "ArrowLeft") handleDislike();
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
beginDrag(e.clientX);
|
||||
}}
|
||||
onPointerMove={(e) => moveDrag(e.clientX)}
|
||||
onPointerUp={endDrag}
|
||||
onPointerCancel={endDrag}
|
||||
>
|
||||
<div className={`pointer-events-none absolute left-5 top-5 rounded-full border-2 border-white/80 px-4 py-2 text-sm font-bold text-white transition-opacity ${dragX > 45 ? "opacity-100" : "opacity-0"}`}>
|
||||
喜欢
|
||||
</div>
|
||||
<div className={`pointer-events-none absolute right-5 top-5 rounded-full border-2 border-white/80 px-4 py-2 text-sm font-bold text-white transition-opacity ${dragX < -45 ? "opacity-100" : "opacity-0"}`}>
|
||||
跳过
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 flex flex-col items-end gap-1">
|
||||
<span className="px-3 py-1.5 rounded-full bg-white/90 text-gray-800 text-xs font-medium backdrop-blur-sm">{badgeText}</span>
|
||||
<a href="#" className="text-[#ff4d4f] hover:text-red-600 text-sm font-medium">{t("report")}</a>
|
||||
@@ -152,7 +291,7 @@ export default function DatingPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center text-sm text-gray-500 dark:text-gray-400 mt-4">{currentIndex + 1} / {MOCK_PROFILES.length}</p>
|
||||
<p className="text-center text-sm text-gray-500 dark:text-gray-400 mt-4">{currentIndex + 1} / {profiles.length}</p>
|
||||
</div>
|
||||
</main>
|
||||
<div className="hidden lg:block w-72 flex-shrink-0" />
|
||||
|
||||
Reference in New Issue
Block a user