Files
gitlab-instance-0a899031_cn…/app/[locale]/page.tsx
2026-03-08 23:20:36 -05:00

473 lines
22 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 { useState, useMemo } from "react";
import { Link } 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 "../components/Footer";
import { cities, type City } from "../data/cities";
const memberPhotos = [
{ name: "林晓", photo: "https://i.pravatar.cc/150?img=5" },
{ name: "张浩", photo: "https://i.pravatar.cc/150?img=3" },
{ name: "陈悦", photo: "https://i.pravatar.cc/150?img=9" },
{ name: "周杰", photo: "https://i.pravatar.cc/150?img=7" },
{ name: "吴婷", photo: "https://i.pravatar.cc/150?img=1" },
{ name: "杨帅", photo: "https://i.pravatar.cc/150?img=12" },
{ name: "赵琪", photo: "https://i.pravatar.cc/150?img=16" },
{ name: "黄磊", photo: "https://i.pravatar.cc/150?img=11" },
{ name: "孙莉", photo: "https://i.pravatar.cc/150?img=20" },
];
const travelingNow = [
{ name: "小明", photo: "https://i.pravatar.cc/150?img=52", dest: "大理" },
{ name: "雨晴", photo: "https://i.pravatar.cc/150?img=44", dest: "成都" },
{ name: "阿杰", photo: "https://i.pravatar.cc/150?img=53", dest: "深圳" },
{ name: "小燕", photo: "https://i.pravatar.cc/150?img=47", dest: "杭州" },
{ name: "大伟", photo: "https://i.pravatar.cc/150?img=55", dest: "厦门" },
{ name: "佳琪", photo: "https://i.pravatar.cc/150?img=48", dest: "昆明" },
{ name: "志远", photo: "https://i.pravatar.cc/150?img=57", dest: "丽江" },
{ name: "心怡", photo: "https://i.pravatar.cc/150?img=49", dest: "三亚" },
{ name: "凯文", photo: "https://i.pravatar.cc/150?img=59", dest: "上海" },
{ name: "子豪", photo: "https://i.pravatar.cc/150?img=60", dest: "北京" },
{ name: "浩然", photo: "https://i.pravatar.cc/150?img=61", dest: "广州" },
{ name: "雅琪", photo: "https://i.pravatar.cc/150?img=21", dest: "南京" },
{ name: "天翔", photo: "https://i.pravatar.cc/150?img=67", dest: "长沙" },
{ name: "美玲", photo: "https://i.pravatar.cc/150?img=23", dest: "苏州" },
{ name: "俊杰", photo: "https://i.pravatar.cc/150?img=68", dest: "青岛" },
{ name: "晓华", photo: "https://i.pravatar.cc/150?img=26", dest: "珠海" },
];
const meetups = [
{ city: "大理", emoji: "🏯", date: "3月9日", count: 12 },
{ city: "成都", emoji: "🐼", date: "3月12日", count: 8 },
{ city: "深圳", emoji: "🌃", date: "3月14日", count: 15 },
{ city: "厦门", emoji: "🌊", date: "3月16日", count: 6 },
{ city: "杭州", emoji: "⛲", date: "3月20日", count: 23 },
];
const hotTopics = [
{ title: "大理最佳共享办公空间推荐 Top 10", views: 2340 },
{ title: "2026年数字游民友好城市排名", views: 1890 },
{ title: "远程工作者的税务规划指南", views: 1560 },
{ title: "成都 vs 大理:哪个更适合游民?", views: 1230 },
{ title: "新手游民必备工具与装备清单", views: 980 },
];
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 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]);
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">18</span>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">个城市</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">5,000+</span>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">位游民</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">8+</span>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">场聚会/月</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">¥3,000</span>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">起/月生活费</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">加入社群</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">
加入数字游民微信社群
</p>
<p className="text-[9px] sm:text-xs text-gray-400 dark:text-gray-500 mb-1.5 sm:mb-4">
3,200+ 在线成员
</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">
加入社群 →
</span>
</div>
</Link>
<Link href="/dating" className="right-item block">
<div className="right-item-header">新成员</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">
本月 86 人加入
</div>
</div>
</Link>
<Link href="/meetups" className="right-item block">
<div className="right-item-header">最新活动</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">
🗓 3月15日
</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">
📍 深圳南山
</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">
👥 12人
</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">
没有找到匹配的城市
</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"
>
显示全部城市
</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" />
热门路线
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5">
{[
{
title: "云南慢生活线",
cities: ["昆明", "大理", "丽江"],
emojis: ["🌸", "🏯", "🏔️"],
duration: "3-6个月",
cost: "¥3,200/月起",
desc: "从春城昆明出发,沿着滇西北一路感受最纯粹的慢生活",
gradient: "from-purple-500 to-pink-500",
},
{
title: "科技创业线",
cities: ["深圳", "杭州", "上海"],
emojis: ["🌃", "⛲", "🏙️"],
duration: "2-4个月",
cost: "¥7,000/月起",
desc: "穿越中国三大科技中心,连接最前沿的创业生态",
gradient: "from-blue-500 to-cyan-500",
},
{
title: "美食文化线",
cities: ["成都", "重庆", "长沙"],
emojis: ["🐼", "🌶️", "🎪"],
duration: "2-3个月",
cost: "¥4,000/月起",
desc: "巴蜀到湘楚,用味蕾丈量中国最火辣的城市",
gradient: "from-orange-500 to-red-500",
},
].map((route) => (
<div
key={route.title}
className="community-card group cursor-pointer"
>
<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">
{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.duration}
</span>
</div>
<div className="flex items-center gap-1 mb-3">
{route.cities.map((city, i) => (
<span key={city} className="flex items-center gap-0.5 text-sm">
<span>{route.emojis[i]}</span>
<span className="font-medium text-gray-700 dark:text-gray-300">{city}</span>
{i < route.cities.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">
{route.desc}
</p>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
{route.cost}
</span>
<span className="text-xs text-[#ff4d4f] font-medium group-hover:translate-x-1 transition-transform">
查看详情 →
</span>
</div>
</div>
</div>
))}
</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" />
社区动态
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5">
<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">
🍹 近期聚会
</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">
8场/月
</span>
</div>
<div className="space-y-2.5">
{meetups.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}人
</span>
</div>
))}
</div>
<Link
href="/meetups"
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"
>
查看全部聚会 →
</Link>
</div>
</div>
<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">
🛩️ 正在旅行
</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}人
</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>
<div className="community-card">
<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">
🔥 热门讨论
</h3>
<div className="space-y-3">
{hotTopics.map((t, i) => (
<a key={t.title} href="#" 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 min-w-0">
<p className="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">
{t.title}
</p>
<p className="text-[11px] text-gray-400 dark:text-gray-500 mt-0.5">
{t.views.toLocaleString()} 次浏览
</p>
</div>
</a>
))}
</div>
<button className="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">
查看更多话题 →
</button>
</div>
</div>
<div className="community-card bg-gradient-to-br from-emerald-50 to-teal-50 dark:from-emerald-950/30 dark:to-teal-950/30">
<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">旅行保险</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">数字游民定制方案</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">
覆盖全国 + 境外,远程工作专属条款,¥15/天起
</p>
<button 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">
了解更多 →
</button>
</div>
</div>
<div className="community-card bg-gradient-to-br from-violet-50 to-purple-50 dark:from-violet-950/30 dark:to-purple-950/30">
<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">社区聊天</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">本月 3,200+ 条消息</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">在线中</span>
</div>
<button 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">
加入聊天 →
</button>
</div>
</div>
<div className="community-card bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-950/30 dark:to-orange-950/30">
<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">落地指南</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
租房 · 共享办公 · 本地攻略
</p>
</div>
</div>
<div className="flex flex-wrap gap-1.5 mb-3">
{["🏯 大理", "🐼 成都", "🌃 深圳", "⛲ 杭州"].map((tag) => (
<span
key={tag}
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"
>
{tag}
</span>
))}
</div>
<button 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">
查看攻略 →
</button>
</div>
</div>
</div>
</section>
<CityModal
city={cityModal}
isOpen={!!cityModal}
onClose={() => setCityModal(null)}
/>
<Footer />
</div>
);
}