Files
gitlab-instance-0a899031_cn…/app/[locale]/page.tsx
2026-03-09 05:07:59 -05:00

414 lines
20 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.
"use client";
import { 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 { cities, type City } from "../data/cities";
import { HOME_CONFIG } from "@/config";
const { memberPhotos, travelingNow, meetups: homeMeetups, hotTopics, routes, stats, communityStats } = HOME_CONFIG;
function safetyOrder(s: string): number {
const o: Record<string, number> = { 极好: 5, 很好: 4, : 3, 一般: 2, : 1 };
return o[s] || 0;
}
export default function Home() {
const [activeFilter, setActiveFilter] = useState("全部");
const [sortBy, setSortBy] = useState("score");
const [cityModal, setCityModal] = useState<City | null>(null);
const locale = useLocale();
const router = useRouter();
const { t } = useTranslation("home");
const { t: tNav } = useTranslation("nav");
const filteredCities = useMemo(() => {
let result = [...cities];
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]);
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">{stats.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">{stats.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">{stats.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">{stats.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">
<Link href="/join" className="right-item block">
<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">
<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">
<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)]">
<div 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">
🗓 {t("meetupDate")}
</div>
<div 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">
📍 {t("meetupLocation")}
</div>
<div 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">
👥 {t("meetupPeople")}
</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">
<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">
{routes.map((route) => (
<Link
key={route.titleZh}
href={`/dashboard?route=${encodeURIComponent(locale === "zh" ? route.titleZh : route.titleEn)}`}
className="community-card group cursor-pointer block"
>
<div className={`h-2 bg-gradient-to-r ${route.gradient}`} />
<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.titleZh : route.titleEn}
</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">
{locale === "zh" ? route.durationZh : route.durationEn}
</span>
</div>
<div className="flex items-center gap-1 mb-3">
{route.emojis.map((emoji, i) => (
<span key={i} className="flex items-center gap-0.5 text-sm">
<span>{emoji}</span>
<span className="font-medium text-gray-700 dark:text-gray-300">
{locale === "zh" ? route.citiesZh[i] : route.citiesEn[i]}
</span>
{i < route.emojis.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.descZh : route.descEn}
</p>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
{locale === "zh" ? route.costZh : route.costEn}
</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.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.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)}
/>
<Footer />
</div>
);
}