Files
gitlab-instance-0a899031_cn…/app/[locale]/page.tsx
root a1daa040ef s
2026-06-07 18:06:52 +08:00

776 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { 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<string, number> = { 极好: 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<City | null>(null);
const [liveCities, setLiveCities] = useState<City[]>(cities);
const [homeContent, setHomeContent] = useState<ContentItem[]>(FALLBACK_HOME_CONTENT);
const [homeStats, setHomeStats] = useState<HomeStats>(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<RouteItem[]>([]);
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 (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
<HeroSection />
<div className="border-y border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-900">
<div className="max-w-[1400px] mx-auto px-5 sm:px-10 py-4 flex flex-wrap items-center justify-center gap-6 sm:gap-12 text-center">
<div>
<span className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-gray-100">{homeStats.cities}</span>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">{t("stats.cities")}</p>
</div>
<div className="w-px h-8 bg-gray-100 dark:bg-gray-700 hidden sm:block" />
<div>
<span className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-gray-100">{homeStats.nomads}</span>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">{t("stats.nomads")}</p>
</div>
<div className="w-px h-8 bg-gray-100 dark:bg-gray-700 hidden sm:block" />
<div>
<span className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-gray-100">{homeStats.homeMeetups}</span>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">{t("stats.meetups")}</p>
</div>
<div className="w-px h-8 bg-gray-100 dark:bg-gray-700 hidden sm:block" />
<div>
<span className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-gray-100">{homeStats.cost}</span>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">{t("stats.cost")}</p>
</div>
</div>
</div>
<div className="px-3 sm:px-5 md:px-10 pt-5">
<FilterBar
activeFilter={activeFilter}
onFilterChange={setActiveFilter}
sortBy={sortBy}
onSortChange={setSortBy}
resultCount={filteredCities.length}
/>
</div>
<div className="items-grid">
{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<string, { header: string; icon: string; href: string }> = {
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 = (
<>
<div className="right-item-header">{meta.header}</div>
<div className="relative h-[calc(100%-38px)] sm:h-[calc(100%-42px)] overflow-hidden">
<img src={item.coverImage} alt={title} className="absolute inset-0 h-full w-full object-cover" />
<div className="absolute inset-0 bg-gradient-to-t from-black/75 via-black/35 to-transparent" />
<div className="absolute inset-x-0 bottom-0 p-3 sm:p-4 text-white">
<div className="mb-1 text-xl sm:text-2xl">{meta.icon}</div>
<h3 className="line-clamp-2 text-xs sm:text-sm font-bold leading-snug">{title}</h3>
<p className="mt-1 line-clamp-2 text-[10px] sm:text-xs text-white/80">{subtitle}</p>
<span className="mt-2 inline-flex rounded-full bg-white/90 px-2.5 py-1 text-[10px] sm:text-xs font-semibold text-gray-900">
{cta}
</span>
</div>
</div>
</>
);
return isExternal ? (
<a
key={item.slug}
href={meta.href}
target="_blank"
rel="noopener noreferrer"
className="right-item block"
style={{ gridColumn: "-2 / -1", gridRow: index + 1 }}
>
{content}
</a>
) : (
<Link
key={item.slug}
href={meta.href}
className="right-item block"
style={{ gridColumn: "-2 / -1", gridRow: index + 1 }}
>
{content}
</Link>
);
})}
<button
type="button"
onClick={() => setSubmissionOpen(true)}
className="right-item block cursor-pointer text-left"
style={{ gridColumn: "-2 / -1", gridRow: 5 }}
>
<div className="right-item-header">稿</div>
<div className="flex h-[calc(100%-38px)] flex-col justify-center p-3 sm:h-[calc(100%-42px)] sm:p-5">
<span className="mb-2 inline-flex w-fit rounded-full bg-[#ff4d4f]/10 px-2.5 py-1 text-[10px] font-bold text-[#ff4d4f] sm:text-xs">
UGC
</span>
<p className="text-xs font-bold leading-snug text-gray-900 dark:text-gray-100 sm:text-sm">
稿 / 访
</p>
<p className="mt-2 line-clamp-3 text-[10px] leading-relaxed text-gray-500 dark:text-gray-400 sm:text-xs">
</p>
<span className="mt-3 inline-flex w-fit rounded-full bg-[#ff4d4f] px-3 py-1.5 text-[10px] font-semibold text-white sm:text-xs">
</span>
</div>
</button>
<Link href="/join" className="right-item block" style={{ gridColumn: "-2 / -1", gridRow: 6 }}>
<div className="right-item-header">{t("joinCommunity")}</div>
<div className="flex flex-col items-center justify-center text-center p-2 sm:p-5 h-[calc(100%-38px)] sm:h-[calc(100%-42px)]">
<span className="text-2xl sm:text-5xl mb-1 sm:mb-3">💬</span>
<p className="text-[10px] sm:text-sm text-gray-600 dark:text-gray-400 mb-0.5">
{t("joinCommunityDesc")}
</p>
<p className="text-[9px] sm:text-xs text-gray-400 dark:text-gray-500 mb-1.5 sm:mb-4">
{communityStats.onlineMembers} {t("membersOnline")}
</p>
<span className="bg-[#ff4d4f] text-white px-2.5 sm:px-6 py-1 sm:py-2 rounded-lg text-[10px] sm:text-sm font-semibold whitespace-nowrap">
{t("joinCommunity")}
</span>
</div>
</Link>
<Link href="/dating" className="right-item block" style={{ gridColumn: "-2 / -1", gridRow: 7 }}>
<div className="right-item-header">{t("newMembers")}</div>
<div className="p-2 sm:p-5 flex flex-wrap gap-1 sm:gap-2 content-start">
{memberPhotos.map((m) => (
<img
key={m.name}
src={m.photo}
alt={m.name}
title={m.name}
className="w-6 h-6 sm:w-10 sm:h-10 rounded-full border-2 border-white dark:border-gray-800 shadow-sm object-cover"
/>
))}
<div className="w-full mt-0.5 text-[9px] sm:text-xs text-gray-400 dark:text-gray-500 text-center">
{t("joinedThisMonthShort")}
</div>
</div>
</Link>
<Link href="/meetups" className="right-item block" style={{ gridColumn: "-2 / -1", gridRow: 8 }}>
<div className="right-item-header">{t("latestEvents")}</div>
<div className="p-1.5 sm:p-4 flex flex-col gap-0.5 sm:gap-2.5 text-[10px] sm:text-sm text-gray-700 dark:text-gray-300 h-[calc(100%-38px)] sm:h-[calc(100%-42px)]">
{homeMeetups.slice(0, 3).map((meetup) => (
<div
key={`${meetup.city}-${meetup.date}`}
className="flex items-center gap-1 bg-[#f5f6f7] dark:bg-gray-800 rounded-lg px-1.5 py-1 sm:px-3 sm:py-2 font-medium text-gray-900 dark:text-gray-100"
>
<span>{meetup.emoji}</span>
<span className="line-clamp-1">{meetup.city} · {meetup.date} · {meetup.count}{t("people")}</span>
</div>
))}
<div className="flex -space-x-1 mt-auto">
{memberPhotos.slice(0, 5).map((m) => (
<img
key={m.name}
src={m.photo}
alt=""
className="w-4 h-4 sm:w-7 sm:h-7 rounded-full border-2 border-white dark:border-gray-800 shadow-sm object-cover"
/>
))}
</div>
</div>
</Link>
{filteredCities.map((city, i) => (
<CityCard
key={city.id}
city={city}
rank={i + 1}
onClick={() => setCityModal(city)}
/>
))}
</div>
{filteredCities.length === 0 && (
<div className="text-center py-16 sm:py-20">
<span className="text-4xl sm:text-5xl mb-4 block">🏜</span>
<p className="text-gray-500 dark:text-gray-400 text-base sm:text-lg">
{t("noMatch")}
</p>
<button
onClick={() => setActiveFilter("全部")}
className="mt-4 text-sm text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-700 px-5 py-2 rounded-full hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
>
{t("showAll")}
</button>
</div>
)}
<section className="max-w-[1500px] mx-auto px-3 sm:px-6 md:px-10 pt-8 sm:pt-12 pb-4">
<div className="mb-5 flex flex-col gap-3 sm:mb-6 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg sm:text-xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
<span className="w-1 h-5 bg-[#ff4d4f] rounded-full" />
/ 访
</h2>
<button
type="button"
onClick={() => setSubmissionOpen(true)}
className="w-fit rounded-lg bg-[#ff4d4f] px-4 py-2 text-sm font-bold text-white transition-colors hover:bg-[#ff7a45]"
>
稿
</button>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{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 (
<Link
key={`${item.type}-${item.slug}`}
href={href}
className="group overflow-hidden rounded-lg bg-white shadow-sm ring-1 ring-gray-100 transition-transform hover:-translate-y-0.5 dark:bg-gray-900 dark:ring-gray-800"
>
<div className="relative aspect-[4/3] overflow-hidden bg-gray-100 dark:bg-gray-800">
<img src={item.coverImage} alt={title} className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" />
<span className="absolute left-3 top-3 rounded-full bg-white/90 px-2.5 py-1 text-[11px] font-bold text-gray-900">
{item.type === "ebook" ? "电子书" : "访谈视频"}
</span>
</div>
<div className="p-4">
<h3 className="line-clamp-2 text-sm font-bold leading-snug text-gray-900 dark:text-gray-100">{title}</h3>
<p className="mt-2 line-clamp-2 text-xs leading-5 text-gray-500 dark:text-gray-400">{subtitle}</p>
<span className="mt-4 inline-flex text-xs font-bold text-[#ff4d4f]">{cta}</span>
</div>
</Link>
);
})}
</div>
</section>
<section className="max-w-[1500px] mx-auto px-3 sm:px-6 md:px-10 pt-8 sm:pt-12 pb-4">
<h2 className="text-lg sm:text-xl font-bold text-gray-900 dark:text-gray-100 mb-5 sm:mb-6 flex items-center gap-2">
<span className="w-1 h-5 bg-[#ff4d4f] rounded-full" />
{t("hotRoutes")}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5">
{displayRoutes.map((route) => (
<Link
key={route.slug}
href={`/dashboard?route=${encodeURIComponent(route.slug)}`}
className="community-card group cursor-pointer block"
>
<div className="h-2 bg-gradient-to-r from-[#ff4d4f] to-[#ff7a45]" />
<div className="p-4 sm:p-5">
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-sm">
{locale === "zh" ? route.title : route.titleEn || route.title}
</h3>
<span className="text-[11px] text-gray-400 dark:text-gray-500 bg-gray-50 dark:bg-gray-800 rounded-full px-2 py-0.5">
{route.durationDays}
</span>
</div>
<div className="flex items-center gap-1 mb-3">
{(route.cities || []).slice(0, 4).map((city, i) => (
<span key={i} className="flex items-center gap-0.5 text-sm">
<span>{city.icon || "📍"}</span>
<span className="font-medium text-gray-700 dark:text-gray-300">
{locale === "zh" ? city.name : city.nameEn || city.name}
</span>
{i < (route.cities || []).slice(0, 4).length - 1 && (
<span className="text-gray-300 dark:text-gray-600 mx-1"></span>
)}
</span>
))}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3 leading-relaxed">
{locale === "zh" ? route.description : route.descriptionEn || route.description}
</p>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
¥{route.budget.toLocaleString()}
</span>
<span className="text-xs text-[#ff4d4f] font-medium group-hover:translate-x-1 transition-transform">
{t("routeDetails")}
</span>
</div>
</div>
</Link>
))}
</div>
</section>
<section id="community" className="max-w-[1500px] mx-auto px-3 sm:px-6 md:px-10 py-8 sm:py-12">
<h2 className="text-lg sm:text-xl font-bold text-gray-900 dark:text-gray-100 mb-5 sm:mb-6 flex items-center gap-2">
<span className="w-1 h-5 bg-[#ff4d4f] rounded-full" />
{t("communityDynamics")}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5">
<Link href="/meetups" className="community-card block">
<div className="p-4 sm:p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-sm flex items-center gap-1.5">
🍹 {t("recentMeetups")}
</h3>
<span className="text-[11px] text-gray-400 dark:text-gray-500 bg-gray-50 dark:bg-gray-800 rounded-full px-2 py-0.5">
{homeMeetups.length}{t("perMonth")}
</span>
</div>
<div className="space-y-2.5">
{homeMeetups.map((m) => (
<div key={m.city} className="flex items-center justify-between py-1.5">
<div className="flex items-center gap-2">
<span className="text-base">{m.emoji}</span>
<div>
<p className="text-sm font-medium text-gray-800 dark:text-gray-200">{m.city}</p>
<p className="text-[11px] text-gray-400 dark:text-gray-500">{m.date}</p>
</div>
</div>
<span className="text-[11px] text-gray-400 dark:text-gray-500 bg-gray-50 dark:bg-gray-800 rounded-full px-2 py-0.5">
{m.count}{t("people")}
</span>
</div>
))}
</div>
<div className="block w-full text-center text-xs font-medium text-[#ff4d4f] hover:text-[#ff7a45] mt-4 pt-3 border-t border-gray-100 dark:border-gray-700 transition-colors">
{t("viewMore")}
</div>
</div>
</Link>
<div className="community-card">
<div className="p-4 sm:p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-sm flex items-center gap-1.5">
🛩 {t("travelingNow")}
</h3>
<span className="text-[11px] text-gray-400 dark:text-gray-500 bg-gray-50 dark:bg-gray-800 rounded-full px-2 py-0.5">
{travelingNow.length}{t("people")}
</span>
</div>
<div className="flex flex-wrap gap-2">
{travelingNow.slice(0, 16).map((m) => (
<img
key={m.name}
src={m.photo}
alt={m.name}
title={`${m.name} · ${m.dest}`}
className="w-9 h-9 rounded-full shadow-sm cursor-pointer hover:scale-110 transition-transform object-cover"
/>
))}
</div>
</div>
</div>
<Link href="/discuss" className="community-card block">
<div className="p-4 sm:p-5">
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-sm flex items-center gap-1.5 mb-4">
🔥 {t("hotDiscussions")}
</h3>
<div className="space-y-3">
{hotTopics.slice(0, 5).map((t_item, i) => (
<div key={t_item.titleZh} className="flex items-start gap-2.5 group">
<span className={`text-xs font-bold mt-0.5 w-4 shrink-0 ${i < 3 ? "text-[#ff4d4f]" : "text-gray-300 dark:text-gray-600"}`}>
{i + 1}
</span>
<div className="flex-1">
<p className="min-w-0 text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100 transition-colors line-clamp-1 leading-snug">
{locale === "zh" ? t_item.titleZh : t_item.titleEn}
</p>
<p className="text-[11px] text-gray-400 dark:text-gray-500 mt-0.5">
{t_item.views.toLocaleString()} {t("views")}
</p>
</div>
</div>
))}
</div>
<div className="block w-full text-center text-xs font-medium text-[#ff4d4f] hover:text-[#ff7a45] mt-4 pt-3 border-t border-gray-100 dark:border-gray-700 transition-colors">
{t("viewAllTopics")}
</div>
</div>
</Link>
<Link href="/services" className="community-card bg-gradient-to-br from-emerald-50 to-teal-50 dark:from-emerald-950/30 dark:to-teal-950/30 block">
<div className="p-4 sm:p-5">
<div className="flex items-start gap-3 mb-3">
<span className="text-2xl">🛡</span>
<div>
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-sm">{t("travelInsurance")}</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{t("travelInsuranceDesc")}</p>
</div>
</div>
<p className="text-xs text-emerald-700 dark:text-emerald-400 bg-emerald-100/50 dark:bg-emerald-900/30 rounded-lg px-3 py-2 mb-3">
{t("insuranceCoverage")}
</p>
<div className="w-full text-center text-sm font-semibold text-emerald-700 dark:text-emerald-400 hover:text-emerald-800 dark:hover:text-emerald-300 bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-800 border border-emerald-200 dark:border-emerald-800 rounded-full py-2 transition-colors">
{t("learnMore")}
</div>
</div>
</Link>
<Link href="/join" className="community-card bg-gradient-to-br from-violet-50 to-purple-50 dark:from-violet-950/30 dark:to-purple-950/30 block">
<div className="p-4 sm:p-5">
<div className="flex items-start gap-3 mb-3">
<span className="text-2xl">💬</span>
<div>
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-sm">{t("communityChat")}</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{locale === "zh" ? `本月 ${communityStats.messagesPerMonth} 条消息` : `${communityStats.messagesPerMonth} messages this month`}</p>
</div>
</div>
<div className="flex items-center gap-2 mb-4">
<div className="flex -space-x-2">
{memberPhotos.slice(0, 6).map((m) => (
<img
key={m.name}
src={m.photo}
alt=""
className="w-7 h-7 rounded-full border-2 border-white dark:border-gray-800 shadow-sm object-cover"
/>
))}
</div>
<span className="text-xs text-gray-400 dark:text-gray-500">{t("onlineNow")}</span>
</div>
<div className="w-full text-center text-sm font-semibold text-violet-700 dark:text-violet-400 hover:text-violet-800 dark:hover:text-violet-300 bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-800 border border-violet-200 dark:border-violet-800 rounded-full py-2 transition-colors">
{t("joinChat")}
</div>
</div>
</Link>
<Link href="/tools" className="community-card bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-950/30 dark:to-orange-950/30 block">
<div className="p-4 sm:p-5">
<div className="flex items-start gap-3 mb-3">
<span className="text-2xl">🏛</span>
<div>
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-sm">{t("landingGuide")}</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{t("landingGuideDesc")}
</p>
</div>
</div>
<div className="flex flex-wrap gap-1.5 mb-3">
{cityTags.map((tag) => (
<button
key={tag.key}
onClick={() => router.push(`/city/${tag.key}`)}
className="text-[11px] bg-amber-100/60 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 rounded-full px-2 py-0.5 cursor-pointer hover:bg-amber-200 dark:hover:bg-amber-800 transition-colors"
>
{tag.emoji} {locale === "zh" ? tag.zh : tag.en}
</button>
))}
</div>
<div className="w-full text-center text-sm font-semibold text-amber-700 dark:text-amber-400 hover:text-amber-800 dark:hover:text-amber-300 bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-800 border border-amber-200 dark:border-amber-800 rounded-full py-2 transition-colors">
{t("viewGuides")}
</div>
</div>
</Link>
</div>
</section>
<CityModal
city={cityModal}
isOpen={!!cityModal}
onClose={() => setCityModal(null)}
/>
<ContentSubmissionModal open={submissionOpen} locale={locale} onClose={() => setSubmissionOpen(false)} />
<Footer />
</div>
);
}