Files
2026-03-07 12:33:14 -06:00

528 lines
23 KiB
TypeScript
Raw Permalink 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 "next/link";
import HeroSection from "./components/HeroSection";
import FilterBar from "./components/FilterBar";
import CityCard from "./components/CityCard";
import { cities } from "./data/cities";
/* ===== Avatar photos ===== */
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 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]">
<HeroSection />
{/* Stats Strip */}
<div className="border-y border-gray-100 bg-white">
<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">18</span>
<p className="text-xs text-gray-400 mt-0.5"></p>
</div>
<div className="w-px h-8 bg-gray-100 hidden sm:block" />
<div>
<span className="text-xl sm:text-2xl font-bold text-gray-900">5,000+</span>
<p className="text-xs text-gray-400 mt-0.5"></p>
</div>
<div className="w-px h-8 bg-gray-100 hidden sm:block" />
<div>
<span className="text-xl sm:text-2xl font-bold text-gray-900">8+</span>
<p className="text-xs text-gray-400 mt-0.5">/</p>
</div>
<div className="w-px h-8 bg-gray-100 hidden sm:block" />
<div>
<span className="text-xl sm:text-2xl font-bold text-gray-900">¥3,000</span>
<p className="text-xs text-gray-400 mt-0.5">/</p>
</div>
</div>
</div>
{/* Filter Bar */}
<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>
{/* Mixed Grid */}
<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 mb-0.5">
</p>
<p className="text-[9px] sm:text-xs text-gray-400 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 shadow-sm object-cover"
/>
))}
<div className="w-full mt-0.5 text-[9px] sm:text-xs text-gray-400 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 h-[calc(100%-38px)] sm:h-[calc(100%-42px)]">
<div className="flex items-center gap-1 bg-[#f5f6f7] rounded-lg px-1.5 py-1 sm:px-3 sm:py-2 font-medium text-[#333]">
🗓 315
</div>
<div className="flex items-center gap-1 bg-[#f5f6f7] rounded-lg px-1.5 py-1 sm:px-3 sm:py-2 font-medium text-[#333]">
📍
</div>
<div className="flex items-center gap-1 bg-[#f5f6f7] rounded-lg px-1.5 py-1 sm:px-3 sm:py-2 font-medium text-[#333]">
👥 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 shadow-sm object-cover"
/>
))}
</div>
</div>
</Link>
{filteredCities.map((city, i) => (
<CityCard key={city.id} city={city} rank={i + 1} />
))}
</div>
{/* Empty State */}
{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 text-base sm:text-lg">
</p>
<button
onClick={() => setActiveFilter("全部")}
className="mt-4 text-sm text-gray-700 bg-gray-200 px-5 py-2 rounded-full hover:bg-gray-300 transition-colors"
>
</button>
</div>
)}
{/* Popular Routes */}
<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 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 text-sm">
{route.title}
</h3>
<span className="text-[11px] text-gray-400 bg-gray-50 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">{city}</span>
{i < route.cities.length - 1 && (
<span className="text-gray-300 mx-1"></span>
)}
</span>
))}
</div>
<p className="text-xs text-gray-500 mb-3 leading-relaxed">
{route.desc}
</p>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-gray-900">
{route.cost}
</span>
<span className="text-xs text-[#ff4d4f] font-medium group-hover:translate-x-1 transition-transform">
</span>
</div>
</div>
</div>
))}
</div>
</section>
{/* Community 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 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 text-sm flex items-center gap-1.5">
🍹
</h3>
<span className="text-[11px] text-gray-400 bg-gray-50 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">{m.city}</p>
<p className="text-[11px] text-gray-400">{m.date}</p>
</div>
</div>
<span className="text-[11px] text-gray-400 bg-gray-50 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 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 text-sm flex items-center gap-1.5">
🛩
</h3>
<span className="text-[11px] text-gray-400 bg-gray-50 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 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"}`}>
{i + 1}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-700 group-hover:text-gray-900 transition-colors line-clamp-1 leading-snug">
{t.title}
</p>
<p className="text-[11px] text-gray-400 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 transition-colors">
</button>
</div>
</div>
{/* 全球旅行保险 */}
<div className="community-card bg-gradient-to-br from-emerald-50 to-teal-50">
<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 text-sm"></h3>
<p className="text-xs text-gray-500 mt-0.5"></p>
</div>
</div>
<p className="text-xs text-emerald-700 bg-emerald-100/50 rounded-lg px-3 py-2 mb-3">
+ ¥15/
</p>
<button className="w-full text-center text-sm font-semibold text-emerald-700 hover:text-emerald-800 bg-white/80 hover:bg-white border border-emerald-200 rounded-full py-2 transition-colors">
</button>
</div>
</div>
{/* 社区聊天 */}
<div className="community-card bg-gradient-to-br from-violet-50 to-purple-50">
<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 text-sm"></h3>
<p className="text-xs text-gray-500 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 shadow-sm object-cover"
/>
))}
</div>
<span className="text-xs text-gray-400">线</span>
</div>
<button className="w-full text-center text-sm font-semibold text-violet-700 hover:text-violet-800 bg-white/80 hover:bg-white border border-violet-200 rounded-full py-2 transition-colors">
</button>
</div>
</div>
{/* 签证/落户 */}
<div className="community-card bg-gradient-to-br from-amber-50 to-orange-50">
<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 text-sm"></h3>
<p className="text-xs text-gray-500 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 text-amber-700 rounded-full px-2 py-0.5"
>
{tag}
</span>
))}
</div>
<button className="w-full text-center text-sm font-semibold text-amber-700 hover:text-amber-800 bg-white/80 hover:bg-white border border-amber-200 rounded-full py-2 transition-colors">
</button>
</div>
</div>
</div>
</section>
{/* Footer */}
<footer className="border-t border-gray-200 bg-white">
<div className="max-w-[1400px] mx-auto px-5 sm:px-10 py-10 sm:py-12">
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 sm:gap-8 mb-8">
<div>
<h4 className="font-bold text-gray-900 text-sm mb-3"></h4>
<ul className="space-y-2 text-sm text-gray-500">
<li><Link href="/join" className="hover:text-gray-700"></Link></li>
<li><Link href="/meetups" className="hover:text-gray-700">线</Link></li>
<li><Link href="/dating" className="hover:text-gray-700"></Link></li>
<li><a href="#" className="hover:text-gray-700"></a></li>
</ul>
</div>
<div>
<h4 className="font-bold text-gray-900 text-sm mb-3"></h4>
<ul className="space-y-2 text-sm text-gray-500">
<li><a href="#" className="hover:text-gray-700"></a></li>
<li><a href="#" className="hover:text-gray-700"></a></li>
<li><a href="#" className="hover:text-gray-700"></a></li>
<li><a href="#" className="hover:text-gray-700"></a></li>
</ul>
</div>
<div>
<h4 className="font-bold text-gray-900 text-sm mb-3"></h4>
<ul className="space-y-2 text-sm text-gray-500">
<li><a href="#" className="hover:text-gray-700"></a></li>
<li><a href="#" className="hover:text-gray-700"></a></li>
<li><a href="#" className="hover:text-gray-700"></a></li>
<li><a href="#" className="hover:text-gray-700"></a></li>
</ul>
</div>
<div>
<h4 className="font-bold text-gray-900 text-sm mb-3"></h4>
<ul className="space-y-2 text-sm text-gray-500">
<li><a href="#" className="hover:text-gray-700"></a></li>
<li><a href="#" className="hover:text-gray-700">使</a></li>
<li><a href="#" className="hover:text-gray-700"></a></li>
<li><a href="#" className="hover:text-gray-700"></a></li>
</ul>
</div>
</div>
<div className="pt-8 border-t border-gray-200 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-2">
<span className="text-xl">🌏</span>
<span className="text-sm font-bold text-gray-900"></span>
</div>
<p className="text-xs text-gray-400">
© 2024-2026 NomadCNA. All rights reserved.
</p>
<div className="flex items-center gap-4 text-xs text-gray-400">
<span>CNY ¥</span>
<span>°C</span>
</div>
</div>
</div>
</footer>
</div>
);
}