"use client"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useAuth } from "@/lib/auth"; import { api } from "@/lib/api"; import type { Destination, Meetup, ProfileStats, TripItem } from "@/lib/types"; import SiteShell from "@/components/SiteShell"; import FavoriteButton from "@/components/FavoriteButton"; import RecentlyViewed from "@/components/RecentlyViewed"; import NomadDigest from "@/components/NomadDigest"; import RingNext from "@/components/RingNext"; import { useRingSteps } from "@/lib/rings"; import { useI18n } from "@/lib/i18n"; import type { Dict } from "@/lib/i18n"; import { loadTrip } from "@/lib/tripStorage"; import { loadPlanMeta, MOVE_CHECKLIST, type PlanMeta } from "@/lib/planMeta"; import { PLAN_SYNC_EVENT } from "@/lib/planSync"; import { mergeDestinationsIntoTrip } from "@/lib/tripActions"; import { useToast } from "@/lib/toast"; import { buildMeetupIcs, downloadMeetupIcs } from "@/lib/meetupIcs"; interface Badge { emoji: string; label: string; desc: string; earned: boolean; } function getBadges( p: Dict["profile"], stats: ProfileStats | null, trip: TripItem[], favCount: number, readiness: number ): Badge[] { const tripMonths = trip.reduce((s, item) => s + item.months, 0); return [ { emoji: "🌱", label: p.badgeNewbie, desc: p.badgeNewbieDesc, earned: true }, { emoji: "❤️", label: p.badgeCollector, desc: p.badgeCollectorDesc, earned: favCount >= 3 }, { emoji: "🗺️", label: p.badgePlanner, desc: p.badgePlannerDesc, earned: trip.length >= 3 }, { emoji: "📅", label: p.badgeLongStay, desc: p.badgeLongStayDesc, earned: tripMonths >= 6 }, { emoji: "✅", label: p.badgeReady, desc: p.badgeReadyDesc, earned: readiness >= 50 }, { emoji: "🌍", label: p.badgeGlobal, desc: p.badgeGlobalDesc, earned: (stats?.destinations_explored ?? 0) >= 5, }, ]; } export default function ProfilePage() { const { user, token, favorites, logout, planSyncStatus, refreshUser } = useAuth(); const router = useRouter(); const { toast } = useToast(); const { t } = useI18n(); const rings = useRingSteps(); const [stats, setStats] = useState(null); const [favoriteDests, setFavoriteDests] = useState([]); const [rsvps, setRsvps] = useState([]); const [connect, setConnect] = useState({ likes: 0, matches: 0, chats: 0 }); const [trip, setTrip] = useState([]); const [meta, setMeta] = useState(null); const [loading, setLoading] = useState(true); const [uploadingAvatar, setUploadingAvatar] = useState(false); const [vip, setVip] = useState(false); const onAvatarChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; e.target.value = ""; if (!file || !token) return; if (file.size > 5 * 1024 * 1024) { toast(t.profile.avatarTooBig, "error"); return; } setUploadingAvatar(true); try { const uploaded = await api.uploadMedia(token, file, "avatar"); await api.updateProfile(token, { avatar: uploaded.url }); await refreshUser?.(); toast(t.profile.avatarOk, "success"); } catch (err) { toast(err instanceof Error ? err.message : t.profile.avatarFail, "error"); } finally { setUploadingAvatar(false); } }; const refreshPlan = () => { setTrip(loadTrip()); setMeta(loadPlanMeta()); }; useEffect(() => { if (!token) { router.push("/login?next=/profile"); return; } setLoading(true); Promise.all([ api.getProfileStats(token), favorites.length > 0 ? api.getFavoriteDestinations(token) : Promise.resolve([]), api.getVipStatus(token).catch(() => ({ vip: false, expires_at: 0 })), Promise.all([ api.getMyRsvps(token).catch(() => ({ ids: [] as string[] })), api.getMeetups().catch(() => [] as Meetup[]), ]).then(([mine, all]) => { const idSet = new Set(mine.ids || []); return all.filter((m) => idSet.has(m.id)).slice(0, 6); }), Promise.all([ api.getMatchLikesReceived(token).catch(() => []), api.getMutualMatches(token).catch(() => ({ items: [] })), api.getConversations(token).catch(() => []), ]).then(([likes, matches, chats]) => ({ likes: likes.length, matches: matches.items?.length ?? 0, chats: chats.length, })), ]).then(([s, f, v, myMeetups, connectStats]) => { setStats(s); setFavoriteDests(f); setVip(Boolean(v.vip)); setRsvps(myMeetups); setConnect(connectStats); }).catch(() => {}).finally(() => setLoading(false)); refreshPlan(); window.addEventListener(PLAN_SYNC_EVENT, refreshPlan); return () => window.removeEventListener(PLAN_SYNC_EVENT, refreshPlan); }, [token, favorites, router]); const totalMonths = trip.reduce((s, item) => s + item.months, 0); const totalCost = trip.reduce((s, item) => s + item.cost * item.months, 0); const avgMonth = totalMonths > 0 ? Math.round(totalCost / totalMonths) : 0; const checked = MOVE_CHECKLIST.filter((c) => meta?.checklist[c.id]).length; const readiness = Math.round((checked / MOVE_CHECKLIST.length) * 100); const budgetOk = !meta || meta.monthlyBudget <= 0 || avgMonth <= 0 || avgMonth <= meta.monthlyBudget; const badges = useMemo( () => getBadges(t.profile, stats, trip, favoriteDests.length, readiness), [t.profile, stats, trip, favoriteDests.length, readiness] ); const earnedCount = badges.filter((b) => b.earned).length; const favsNotInTrip = favoriteDests.filter((d) => !trip.some((item) => item.slug === d.slug)); const addFavsToPlan = () => { if (favsNotInTrip.length === 0) { toast(t.profile.favsAlready, "info", { href: "/plan", label: t.profile.openPlan }); return; } const { added } = mergeDestinationsIntoTrip(favsNotInTrip, 1); refreshPlan(); toast(t.profile.favsAdded.replace("{n}", String(added)), "success", { href: "/plan", label: t.profile.openPlan, }); }; const planTitle = meta?.title && meta.title !== "我的旅居计划" ? meta.title : t.plan.defaultTitle; if (!user) return null; return (
{user.avatar?.startsWith("http") ? ( // eslint-disable-next-line @next/next/no-img-element ) : (
{user.avatar || "🧑‍💻"}
)}

{user.name}

{user.email}

{stats && {stats.nomad_level}} {vip && ✨ VIP}
{planSyncStatus === "syncing" && t.profile.syncing} {planSyncStatus === "synced" && t.profile.synced} {planSyncStatus === "offline" && t.profile.offline} {planSyncStatus === "idle" && t.profile.loggedIn}
{stats && (
❤️ {stats.favorites_count} {t.profile.statFavs}
🗓️ {trip.length} {t.profile.statCities}
✅ {readiness}% {t.profile.statReady}
🏅 {earnedCount}/{badges.length} {t.profile.statBadges}
)}

{t.profile.connectStrip}

💕 {connect.likes} {t.profile.statLikes} ✨ {connect.matches} {t.profile.statMatches} ✉️ {connect.chats} {t.profile.statChats} 🎉 {rsvps.length} {t.profile.statRsvps}

🗓️ {planTitle}

{t.profile.openPlan}
{trip.length === 0 ? (
🗺️

{t.profile.tripEmpty}

{t.profile.goPlan}
) : (
{(meta?.startMonth || meta?.monthlyBudget) && (
{meta?.startMonth && ( {t.profile.startLabel.replace("{month}", meta.startMonth)} )} {meta && meta.monthlyBudget > 0 && ( {t.profile.budgetLabel.replace("{budget}", meta.monthlyBudget.toLocaleString())} {avgMonth > 0 && t.profile.avgMonthLabel.replace("{avg}", avgMonth.toLocaleString())} )} {t.profile.readyCount .replace("{checked}", String(checked)) .replace("{total}", String(MOVE_CHECKLIST.length))}
)} {trip.map((item, i) => (
{i + 1} {item.emoji}
{item.name}, {item.country} {t.profile.monthsCost .replace("{months}", String(item.months)) .replace("{cost}", (item.cost * item.months).toLocaleString())}
))}
{t.profile.totalLine}{" "} {totalMonths} {t.profile.monthsUnit} {" "} · ¥{totalCost.toLocaleString()}
{trip.length >= 2 && ( item.slug).slice(0, 4).join(",")}`} className="btn btn-ghost btn-sm" > {t.profile.compareTrip} )} {t.profile.keepEditing}
)}

{t.profile.achievements}

{badges.map((b) => (
{b.earned ? b.emoji : "🔒"} {b.label} {b.desc}
))}

{t.profile.myFavs}

{favsNotInTrip.length > 0 && ( )}
{loading ? (

{t.profile.loading}

) : favoriteDests.length === 0 ? (
🌍

{t.profile.favsEmpty}

{t.profile.goExplore}
) : (
{favoriteDests.map((d) => (
{d.emoji}

{d.name}, {d.country}

💰 ¥{d.cost.toLocaleString()} {t.profile.perMonth} · ⭐ {d.rating}

))}
)}

🎉 {t.profile.myRsvps}

{rsvps.length > 0 && ( )} {t.nav.meetups}
{rsvps.length === 0 ? (

{t.profile.rsvpEmpty}

{t.profile.goMeetups}
) : (
{rsvps.map((m) => (
{m.emoji}
{m.title}

{m.city} · {m.date} {m.time}

{(m.mode === "online" || m.mode === "hybrid") && ( {t.profile.openLive} )}
))}
)}

{t.profile.quickLinks}

{t.profile.qDest} {t.profile.qPlan} {t.profile.qCompare} {t.profile.qNext} {t.profile.qMeetups} {t.profile.qDating} {t.profile.qChat} {t.profile.qNotif} {vip ? t.profile.qVipMember : t.profile.qVip} {t.profile.qDigital} {t.profile.qAi} {t.profile.qTools}
); }