"use client"; import { useEffect, useState, useMemo } from "react"; import { Link, useLocale, useRouter } from "@/i18n/navigation"; import { useTranslation } from "@/i18n/navigation"; import HeroSection from "../components/HeroSection"; import FilterBar from "../components/FilterBar"; import CityCard from "../components/CityCard"; import CityModal from "../components/CityModal"; import Footer from "@/app/components/Footer"; import ContentSubmissionModal from "@/app/components/ContentSubmissionModal"; import { cities, type City } from "../data/cities"; import { HOME_CONFIG } from "@/config"; import { apiFetch } from "@/app/lib/api-client"; const { memberPhotos: fallbackMemberPhotos, travelingNow: fallbackTravelingNow, meetups: fallbackHomeMeetups, hotTopics: fallbackHotTopics, routes: fallbackRoutes, stats: fallbackStats, communityStats: fallbackCommunityStats, } = HOME_CONFIG; interface ContentItem { slug: string; type: "ebook" | "video" | "app" | "guide"; title: string; titleEn?: string; subtitle: string; subtitleEn?: string; description?: string; coverImage: string; targetUrl: string; ctaLabel: string; ctaLabelEn?: string; durationMinutes?: number; } const FALLBACK_HOME_CONTENT: ContentItem[] = [ { slug: "nomad-china-playbook", type: "ebook", title: "《中国数字游民落地手册》", titleEn: "China Digital Nomad Playbook", subtitle: "选城、预算、租房和社群融入的一本实操指南", subtitleEn: "A practical guide for city choice, budget, housing, and community.", coverImage: "https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=1200", targetUrl: "/ebooks/nomad-china-playbook", ctaLabel: "查看电子书", ctaLabelEn: "View ebook", }, { slug: "lin-dali-documentary", type: "video", title: "林晓雨:在大理远程工作的现实一天", titleEn: "A remote work day in Dali", subtitle: "个人访谈 / 现实纪录片", subtitleEn: "Interview documentary", coverImage: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=1200", targetUrl: "/videos/lin-dali-documentary", ctaLabel: "观看访谈", ctaLabelEn: "Watch", }, { slug: "nomadcna-app", type: "app", title: "NomadCNA App", titleEn: "NomadCNA App", subtitle: "记录旅居路线、发现同城活动、保存城市清单", subtitleEn: "Track routes, discover meetups, and save city lists.", coverImage: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=1200", targetUrl: "/app", ctaLabel: "下载 App", ctaLabelEn: "Download app", }, { slug: "digital-nomad-guide", type: "guide", title: "数字游民指南", titleEn: "Digital Nomad Guide", subtitle: "完整方法论、工具、案例和长期更新", subtitleEn: "Methods, tools, cases, and long-term updates.", coverImage: "https://images.unsplash.com/photo-1499750310107-5fef28a66643?w=1200", targetUrl: "/digital", ctaLabel: "打开指南", ctaLabelEn: "Open guide", }, ]; interface ProfileItem { id: string; name: string; location?: string; photo: string; } interface MeetupItem { id: string; city: string; emoji?: string; date?: string; time?: string; rsvpCount?: number; venue?: string; } interface TopicItem { id: string; title: string; titleEn?: string; views: number; } interface RouteItem { id?: string; slug: string; title: string; titleEn?: string; durationDays: number; budget: number; description: string; descriptionEn?: string; stops?: string[]; cities?: City[]; } interface HomeStats { cities: string; nomads: string; homeMeetups: string; cost: string; } function safetyOrder(s: string): number { const o: Record = { 极好: 5, 很好: 4, 好: 3, 一般: 2, 差: 1 }; return o[s] || 0; } function shortDate(date?: string): string { if (!date) return "时间待定"; const month = Number(date.slice(5, 7)); const day = Number(date.slice(8, 10)); return `${month}月${day}日`; } export default function Home() { const [activeFilter, setActiveFilter] = useState("全部"); const [sortBy, setSortBy] = useState("score"); const [cityModal, setCityModal] = useState(null); const [liveCities, setLiveCities] = useState(cities); const [homeContent, setHomeContent] = useState(FALLBACK_HOME_CONTENT); const [homeStats, setHomeStats] = useState(fallbackStats); const [memberPhotos, setMemberPhotos] = useState(fallbackMemberPhotos); const [travelingNow, setTravelingNow] = useState(fallbackTravelingNow); const [homeMeetups, setHomeMeetups] = useState(fallbackHomeMeetups); const [hotTopics, setHotTopics] = useState(fallbackHotTopics); const [routes, setRoutes] = useState([]); const [communityStats, setCommunityStats] = useState(fallbackCommunityStats); const [submissionOpen, setSubmissionOpen] = useState(false); const locale = useLocale(); const router = useRouter(); const { t } = useTranslation("home"); useEffect(() => { apiFetch<{ ok: boolean; items: City[] }>("/api/cities") .then((data) => { if (data.items?.length) { setLiveCities(data.items.map((item) => ({ ...item, id: Number((item as unknown as { idNumber?: number }).idNumber || item.id) }))); } }) .catch(() => setLiveCities(cities)); apiFetch<{ ok: boolean; items: ContentItem[] }>("/api/content/home") .then((data) => setHomeContent(data.items?.length ? data.items : FALLBACK_HOME_CONTENT)) .catch(() => setHomeContent(FALLBACK_HOME_CONTENT)); apiFetch<{ stats: { cities: number; nomads: number; meetups: number; minCost: number; profiles: number; discussions: number } }>("/api/stats/overview") .then((data) => { setHomeStats({ cities: data.stats.cities.toLocaleString(), nomads: data.stats.nomads.toLocaleString(), homeMeetups: data.stats.meetups.toLocaleString(), cost: `¥${data.stats.minCost.toLocaleString()}`, }); setCommunityStats({ onlineMembers: `${data.stats.profiles.toLocaleString()}+`, messagesPerMonth: `${Math.max(data.stats.discussions * 180, 1).toLocaleString()}+`, }); }) .catch(() => { setHomeStats(fallbackStats); setCommunityStats(fallbackCommunityStats); }); apiFetch<{ items: ProfileItem[] }>("/api/profiles") .then((data) => { const profiles = (data.items || []).slice(0, 12); if (profiles.length) { setMemberPhotos(profiles.map((profile) => ({ name: profile.name, photo: profile.photo }))); setTravelingNow( profiles.map((profile) => ({ name: profile.name, photo: profile.photo, dest: profile.location || "远程", })) ); } }) .catch(() => { setMemberPhotos(fallbackMemberPhotos); setTravelingNow(fallbackTravelingNow); }); apiFetch<{ items: MeetupItem[] }>("/api/meetups") .then((data) => { const meetups = (data.items || []).slice(0, 6); if (meetups.length) { setHomeMeetups( meetups.map((meetup) => ({ city: meetup.city, emoji: meetup.emoji || "📍", date: shortDate(meetup.date), count: Number(meetup.rsvpCount || 0), })) ); } }) .catch(() => setHomeMeetups(fallbackHomeMeetups)); apiFetch<{ items: TopicItem[] }>("/api/discussions") .then((data) => { const topics = [...(data.items || [])] .sort((a, b) => Number(b.views || 0) - Number(a.views || 0)) .slice(0, 5); if (topics.length) { setHotTopics( topics.map((topic) => ({ titleZh: topic.title, titleEn: topic.titleEn || topic.title, views: Number(topic.views || 0), })) ); } }) .catch(() => setHotTopics(fallbackHotTopics)); apiFetch<{ items: RouteItem[] }>("/api/routes") .then((data) => setRoutes(data.items || [])) .catch(() => setRoutes([])); }, []); const filteredCities = useMemo(() => { let result = [...liveCities]; if (activeFilter !== "全部") { result = result.filter((c) => c.tags.includes(activeFilter)); } result.sort((a, b) => { switch (sortBy) { case "cost-asc": return a.costPerMonth - b.costPerMonth; case "cost-desc": return b.costPerMonth - a.costPerMonth; case "internet": return b.internetSpeed - a.internetSpeed; case "safety": return safetyOrder(b.safety) - safetyOrder(a.safety); case "nomads": return b.nomadsNow - a.nomadsNow; case "temperature": return b.temperature - a.temperature; default: return b.overallScore - a.overallScore; } }); return result; }, [activeFilter, sortBy, liveCities]); const displayRoutes = routes.length ? routes : fallbackRoutes.map((route, index) => ({ id: `fallback-${index}`, slug: route.titleEn.toLowerCase().replace(/[^a-z0-9]+/g, "-"), title: route.titleZh, titleEn: route.titleEn, durationDays: Number(route.durationZh.match(/\d+/)?.[0] || 30), budget: Number(route.costZh.replace(/[^\d]/g, "")) || 3000, description: route.descZh, descriptionEn: route.descEn, stops: route.citiesZh, cities: route.citiesZh.map((name, i) => ({ id: i, name, nameEn: route.citiesEn[i], icon: route.emojis[i], } as City)), })); const communityContent = useMemo( () => homeContent.filter((item) => item.type === "ebook" || item.type === "video"), [homeContent] ); const cityTags = [ { key: "dali", emoji: "🏯", zh: "大理", en: "Dali" }, { key: "chengdu", emoji: "🐼", zh: "成都", en: "Chengdu" }, { key: "shenzhen", emoji: "🌃", zh: "深圳", en: "Shenzhen" }, { key: "hangzhou", emoji: "⛲", zh: "杭州", en: "Hangzhou" }, ]; return (
{homeStats.cities}

{t("stats.cities")}

{homeStats.nomads}

{t("stats.nomads")}

{homeStats.homeMeetups}

{t("stats.meetups")}

{homeStats.cost}

{t("stats.cost")}

{homeContent.slice(0, 4).map((item, index) => { const title = locale === "zh" ? item.title : item.titleEn || item.title; const subtitle = locale === "zh" ? item.subtitle : item.subtitleEn || item.subtitle; const cta = locale === "zh" ? item.ctaLabel : item.ctaLabelEn || item.ctaLabel; const typeMeta: Record = { ebook: { header: "电子书", icon: "📘", href: `/ebooks/${item.slug}` }, video: { header: "访谈视频", icon: "▶️", href: `/videos/${item.slug}` }, app: { header: "App", icon: "📱", href: "/app" }, guide: { header: "数字游民指南", icon: "🧭", href: "/digital" }, }; const meta = typeMeta[item.type] || typeMeta.guide; const isExternal = meta.href.startsWith("http"); const content = ( <>
{meta.header}
{title}
{meta.icon}

{title}

{subtitle}

{cta}
); return isExternal ? ( {content} ) : ( {content} ); })}
{t("joinCommunity")}
💬

{t("joinCommunityDesc")}

{communityStats.onlineMembers} {t("membersOnline")}

{t("joinCommunity")} →
{t("newMembers")}
{memberPhotos.map((m) => ( {m.name} ))}
{t("joinedThisMonthShort")}
{t("latestEvents")}
{homeMeetups.slice(0, 3).map((meetup) => (
{meetup.emoji} {meetup.city} · {meetup.date} · {meetup.count}{t("people")}
))}
{memberPhotos.slice(0, 5).map((m) => ( ))}
{filteredCities.map((city, i) => ( setCityModal(city)} /> ))}
{filteredCities.length === 0 && (
🏜️

{t("noMatch")}

)}

电子书 / 访谈视频

{communityContent.slice(0, 8).map((item) => { const title = locale === "zh" ? item.title : item.titleEn || item.title; const subtitle = locale === "zh" ? item.subtitle : item.subtitleEn || item.subtitle; const cta = locale === "zh" ? item.ctaLabel : item.ctaLabelEn || item.ctaLabel; const href = item.type === "video" ? `/videos/${item.slug}` : `/ebooks/${item.slug}`; return (
{title} {item.type === "ebook" ? "电子书" : "访谈视频"}

{title}

{subtitle}

{cta}
); })}

{t("hotRoutes")}

{displayRoutes.map((route) => (

{locale === "zh" ? route.title : route.titleEn || route.title}

{route.durationDays} 天
{(route.cities || []).slice(0, 4).map((city, i) => ( {city.icon || "📍"} {locale === "zh" ? city.name : city.nameEn || city.name} {i < (route.cities || []).slice(0, 4).length - 1 && ( )} ))}

{locale === "zh" ? route.description : route.descriptionEn || route.description}

¥{route.budget.toLocaleString()} {t("routeDetails")}
))}

{t("communityDynamics")}

🍹 {t("recentMeetups")}

{homeMeetups.length}{t("perMonth")}
{homeMeetups.map((m) => (
{m.emoji}

{m.city}

{m.date}

{m.count}{t("people")}
))}
{t("viewMore")}

🛩️ {t("travelingNow")}

{travelingNow.length}{t("people")}
{travelingNow.slice(0, 16).map((m) => ( {m.name} ))}

🔥 {t("hotDiscussions")}

{hotTopics.slice(0, 5).map((t_item, i) => (
{i + 1}

{locale === "zh" ? t_item.titleZh : t_item.titleEn}

{t_item.views.toLocaleString()} {t("views")}

))}
{t("viewAllTopics")}
🛡️

{t("travelInsurance")}

{t("travelInsuranceDesc")}

{t("insuranceCoverage")}

{t("learnMore")}
💬

{t("communityChat")}

{locale === "zh" ? `本月 ${communityStats.messagesPerMonth} 条消息` : `${communityStats.messagesPerMonth} messages this month`}

{memberPhotos.slice(0, 6).map((m) => ( ))}
{t("onlineNow")}
{t("joinChat")}
🏛️

{t("landingGuide")}

{t("landingGuideDesc")}

{cityTags.map((tag) => ( ))}
{t("viewGuides")}
setCityModal(null)} /> setSubmissionOpen(false)} />
); }