's'
This commit is contained in:
189
app/components/CityCard.tsx
Normal file
189
app/components/CityCard.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { City } from "../data/cities";
|
||||
|
||||
function ratingToPercent(rating: string): number {
|
||||
const m: Record<string, number> = {
|
||||
极好: 95,
|
||||
很好: 80,
|
||||
好: 60,
|
||||
一般: 40,
|
||||
差: 20,
|
||||
};
|
||||
return m[rating] || 50;
|
||||
}
|
||||
|
||||
function getBarColor(p: number): string {
|
||||
if (p >= 60) return "#22c55e";
|
||||
if (p >= 35) return "#eab308";
|
||||
return "#ef4444";
|
||||
}
|
||||
|
||||
function ScoreBar({
|
||||
icon,
|
||||
label,
|
||||
percent,
|
||||
animate,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
percent: number;
|
||||
animate: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<span className="text-white/80 text-xs sm:text-sm w-14 sm:w-16 shrink-0 flex items-center gap-1">
|
||||
<span>{icon}</span>
|
||||
<span className="hidden sm:inline">{label}</span>
|
||||
</span>
|
||||
<div className="flex-1 h-2 sm:h-2.5 bg-white/10 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-700 ease-out"
|
||||
style={{
|
||||
width: animate ? `${percent}%` : "0%",
|
||||
backgroundColor: getBarColor(percent),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CityCard({
|
||||
city,
|
||||
rank,
|
||||
}: {
|
||||
city: City;
|
||||
rank: number;
|
||||
}) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const scores = [
|
||||
{
|
||||
icon: "⭐",
|
||||
label: "综合",
|
||||
percent: (city.overallScore / 5) * 100,
|
||||
},
|
||||
{
|
||||
icon: "💵",
|
||||
label: "费用",
|
||||
percent: Math.max(15, 100 - city.costPerMonth / 200),
|
||||
},
|
||||
{
|
||||
icon: "📡",
|
||||
label: "网速",
|
||||
percent: Math.min(95, (city.internetSpeed / 120) * 100),
|
||||
},
|
||||
{
|
||||
icon: "👍",
|
||||
label: "好评",
|
||||
percent: ratingToPercent(city.liked),
|
||||
},
|
||||
{
|
||||
icon: "👮",
|
||||
label: "安全",
|
||||
percent: ratingToPercent(city.safety),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="city-card"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${city.gradientFrom}, ${city.gradientTo})`,
|
||||
}}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
{/* Icon watermark */}
|
||||
<span className="absolute bottom-8 right-2 text-[70px] sm:text-[90px] opacity-[0.12] select-none pointer-events-none leading-none">
|
||||
{city.icon}
|
||||
</span>
|
||||
|
||||
{/* ===== Default state ===== */}
|
||||
<div
|
||||
className={`absolute inset-0 z-10 flex flex-col justify-between p-3 sm:p-4 transition-opacity duration-300 ${hovered ? "opacity-0" : "opacity-100"}`}
|
||||
>
|
||||
{/* Top row */}
|
||||
<div className="flex items-start justify-between">
|
||||
<span className="text-white/70 text-base sm:text-xl font-bold">
|
||||
{rank}
|
||||
</span>
|
||||
<span className="text-[10px] sm:text-xs bg-white/20 backdrop-blur-sm rounded-full px-1.5 sm:px-2 py-0.5 sm:py-1 text-white flex items-center gap-0.5">
|
||||
📡 {city.internetSpeed}Mbps
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Center: city name + country */}
|
||||
<div className="text-center -mt-2">
|
||||
<h3 className="text-xl sm:text-[28px] font-bold text-white drop-shadow-md tracking-wide">
|
||||
{city.name}
|
||||
</h3>
|
||||
<p className="text-xs sm:text-sm text-white/60 mt-0.5">
|
||||
{city.countryEmoji} {city.country}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bottom: temp + cost + nomads */}
|
||||
<div>
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<span className="text-[9px] sm:text-[11px] bg-white/15 backdrop-blur-sm rounded-full px-1.5 py-0.5 text-white/80">
|
||||
👥 {city.nomadsNow}人在此
|
||||
</span>
|
||||
<span className="text-[9px] sm:text-[11px] bg-white/15 backdrop-blur-sm rounded-full px-1.5 py-0.5 text-white/80">
|
||||
🌡 {city.temperature}°C
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-end justify-between text-white text-xs sm:text-sm">
|
||||
<span className="text-white/80 text-[10px] sm:text-xs line-clamp-1 max-w-[60%]">
|
||||
{city.description}
|
||||
</span>
|
||||
<span className="font-semibold shrink-0">
|
||||
¥{city.costPerMonth.toLocaleString()}/月
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== Hover overlay (desktop only) ===== */}
|
||||
<div
|
||||
className={`absolute inset-0 z-20 bg-[#2a2a2a]/95 hidden sm:flex flex-col justify-between p-4 sm:p-5 transition-opacity duration-300 ${hovered ? "opacity-100" : "opacity-0 pointer-events-none"}`}
|
||||
>
|
||||
{/* Top icons */}
|
||||
<div className="flex items-center justify-between">
|
||||
<button className="text-white/50 hover:text-red-400 transition-colors text-base">
|
||||
❤️
|
||||
</button>
|
||||
<button
|
||||
className="text-white/30 hover:text-white text-sm transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setHovered(false);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Score bars */}
|
||||
<div className="space-y-2.5 sm:space-y-3">
|
||||
{scores.map((s) => (
|
||||
<ScoreBar
|
||||
key={s.label}
|
||||
icon={s.icon}
|
||||
label={s.label}
|
||||
percent={s.percent}
|
||||
animate={hovered}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom: city info small */}
|
||||
<div className="text-center text-white/40 text-xs">
|
||||
{city.name} · {city.countryEmoji} {city.country}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
97
app/components/FilterBar.tsx
Normal file
97
app/components/FilterBar.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { allTags } from "../data/cities";
|
||||
|
||||
interface FilterBarProps {
|
||||
activeFilter: string;
|
||||
onFilterChange: (filter: string) => void;
|
||||
sortBy: string;
|
||||
onSortChange: (sort: string) => void;
|
||||
resultCount: number;
|
||||
}
|
||||
|
||||
const sortOptions = [
|
||||
{ value: "score", label: "综合评分" },
|
||||
{ value: "cost-asc", label: "费用最低" },
|
||||
{ value: "cost-desc", label: "费用最高" },
|
||||
{ value: "internet", label: "网速最快" },
|
||||
{ value: "safety", label: "最安全" },
|
||||
{ value: "nomads", label: "游民最多" },
|
||||
{ value: "temperature", label: "最温暖" },
|
||||
];
|
||||
|
||||
const tagIcons: Record<string, string> = {
|
||||
全部: "🌍",
|
||||
热门: "🔥",
|
||||
便宜: "💰",
|
||||
高速网络: "📡",
|
||||
温暖: "☀️",
|
||||
安全: "🛡️",
|
||||
海滨: "🏖️",
|
||||
山城: "🏔️",
|
||||
都市: "🌆",
|
||||
美食: "🍜",
|
||||
文化: "🎨",
|
||||
一线城市: "🏙️",
|
||||
新一线: "⭐",
|
||||
宜居: "🏡",
|
||||
历史名城: "🏛️",
|
||||
友好社区: "🤝",
|
||||
户外运动: "🏄",
|
||||
};
|
||||
|
||||
export default function FilterBar({
|
||||
activeFilter,
|
||||
onFilterChange,
|
||||
sortBy,
|
||||
onSortChange,
|
||||
resultCount,
|
||||
}: FilterBarProps) {
|
||||
return (
|
||||
<div className="mb-6" id="cities">
|
||||
{/* Top bar: result count + sort */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-bold text-gray-900">
|
||||
发现目的地
|
||||
</h2>
|
||||
<span className="text-sm text-gray-400">
|
||||
{resultCount} 个城市
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-400 hidden sm:block">排序:</span>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => onSortChange(e.target.value)}
|
||||
className="text-sm bg-white border border-gray-200 rounded-lg px-3 py-1.5 text-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-200 cursor-pointer"
|
||||
>
|
||||
{sortOptions.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter tags */}
|
||||
<div className="flex gap-2 overflow-x-auto hide-scrollbar pb-2">
|
||||
{allTags.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => onFilterChange(tag)}
|
||||
className={`filter-btn shrink-0 flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium border transition-all ${
|
||||
activeFilter === tag
|
||||
? "active border-gray-900"
|
||||
: "border-gray-200 text-gray-600 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<span className="text-xs">{tagIcons[tag] || "🏷️"}</span>
|
||||
<span>{tag}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
189
app/components/HeroSection.tsx
Normal file
189
app/components/HeroSection.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
const avatarPhotos = [
|
||||
"https://i.pravatar.cc/150?img=32",
|
||||
"https://i.pravatar.cc/150?img=33",
|
||||
"https://i.pravatar.cc/150?img=25",
|
||||
"https://i.pravatar.cc/150?img=52",
|
||||
"https://i.pravatar.cc/150?img=44",
|
||||
"https://i.pravatar.cc/150?img=55",
|
||||
"https://i.pravatar.cc/150?img=47",
|
||||
"https://i.pravatar.cc/150?img=57",
|
||||
"https://i.pravatar.cc/150?img=48",
|
||||
"https://i.pravatar.cc/150?img=59",
|
||||
"https://i.pravatar.cc/150?img=49",
|
||||
];
|
||||
|
||||
const features = [
|
||||
{ icon: "🍹", text: "每年在 30+ 城市参加线下聚会" },
|
||||
{ icon: "❤️", text: "结识新朋友,约会交友" },
|
||||
{ icon: "🧪", text: "研究目的地,找到最适合居住和工作的地方" },
|
||||
{ icon: "🌎", text: "追踪你的旅行,记录你去过的地方" },
|
||||
{ icon: "💬", text: "加入社群聊天,在旅途中找到你的归属" },
|
||||
];
|
||||
|
||||
export default function HeroSection() {
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
const handleJoin = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email.trim()) {
|
||||
alert(`欢迎加入游牧中国!我们将发送确认邮件到 ${email}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-orange-50/70 via-white to-[#fafafa]" />
|
||||
<div
|
||||
className="absolute top-0 right-0 w-[500px] h-[500px] -translate-y-1/3 translate-x-1/4 opacity-40 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(249,115,22,0.25) 0%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 w-[350px] h-[350px] translate-y-1/3 -translate-x-1/4 opacity-30 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(139,92,246,0.2) 0%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative max-w-[1400px] mx-auto px-5 sm:px-10 py-10 sm:py-14 md:py-20">
|
||||
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-10 xl:gap-16">
|
||||
{/* Left Column */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-orange-700 bg-orange-100 px-3 py-1.5 rounded-full mb-5">
|
||||
🏆 中国数字游民社区 · 始于 2024
|
||||
</span>
|
||||
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight mb-5">
|
||||
<span className="hero-gradient-text">探索中国</span>
|
||||
<br />
|
||||
<span className="text-gray-900">远程工作,自由生活</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-base sm:text-lg text-gray-500 max-w-2xl mb-7 leading-relaxed">
|
||||
加入全球华人数字游民社区,发现中国最适合远程工作和生活的城市,
|
||||
从大理到成都,从深圳到杭州,开启你的游牧人生
|
||||
</p>
|
||||
|
||||
<div className="space-y-2.5 mb-8">
|
||||
{features.map((f) => (
|
||||
<div key={f.text} className="flex items-start gap-2.5">
|
||||
<span className="text-base sm:text-lg shrink-0 mt-0.5">
|
||||
{f.icon}
|
||||
</span>
|
||||
<span className="text-sm sm:text-base text-gray-600 underline decoration-gray-300 underline-offset-2">
|
||||
{f.text}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<div className="flex -space-x-2">
|
||||
{avatarPhotos.slice(0, 8).map((src, i) => (
|
||||
<img
|
||||
key={i}
|
||||
src={src}
|
||||
alt=""
|
||||
className="w-8 h-8 rounded-full border-2 border-white shadow-sm object-cover"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
<strong className="text-gray-900">5,000+</strong> 成员已加入
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-7 border-t border-gray-100">
|
||||
<p className="text-xs text-gray-400 uppercase tracking-wider mb-3">
|
||||
媒体报道
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-x-8 gap-y-2">
|
||||
{["36氪", "少数派", "极客公园", "虎嗅", "创业邦", "澎湃新闻"].map(
|
||||
(name) => (
|
||||
<span
|
||||
key={name}
|
||||
className="text-sm font-semibold text-gray-300 hover:text-gray-400 transition-colors cursor-default"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Video + Email */}
|
||||
<div className="lg:w-[360px] xl:w-[400px] shrink-0 mt-10 lg:mt-4">
|
||||
<div className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-100">
|
||||
<div className="relative h-48 sm:h-56 bg-gradient-to-br from-emerald-400 via-teal-500 to-cyan-600 flex items-center justify-center cursor-pointer group overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<div className="absolute top-4 left-4 w-20 h-16 rounded-lg bg-white/20" />
|
||||
<div className="absolute bottom-6 right-6 w-24 h-14 rounded-lg bg-white/15" />
|
||||
<div className="absolute top-1/2 left-1/3 w-16 h-16 rounded-full bg-white/10" />
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-1/3 bg-gradient-to-t from-black/20 to-transparent" />
|
||||
<button className="relative w-16 h-16 rounded-full bg-white/95 flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform">
|
||||
<svg
|
||||
className="w-7 h-7 text-emerald-600 ml-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleJoin} className="p-5">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="输入你的邮箱..."
|
||||
className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20 focus:border-[#ff4d4f] transition-colors mb-3"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-[#ff4d4f] hover:bg-[#ff3333] text-white py-3 rounded-lg text-sm font-semibold transition-colors active:scale-[0.98]"
|
||||
>
|
||||
加入游牧中国 →
|
||||
</button>
|
||||
<p className="text-xs text-gray-400 text-center mt-3">
|
||||
已有账号?输入邮箱即可自动登录
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2 justify-center">
|
||||
<Link
|
||||
href="/meetups"
|
||||
className="text-xs text-gray-400 hover:text-gray-600 bg-white rounded-full px-3 py-1.5 border border-gray-100 transition-colors"
|
||||
>
|
||||
🍹 线下聚会
|
||||
</Link>
|
||||
<Link
|
||||
href="/dating"
|
||||
className="text-xs text-gray-400 hover:text-gray-600 bg-white rounded-full px-3 py-1.5 border border-gray-100 transition-colors"
|
||||
>
|
||||
❤️ 交友约会
|
||||
</Link>
|
||||
<Link
|
||||
href="/join"
|
||||
className="text-xs text-gray-400 hover:text-gray-600 bg-white rounded-full px-3 py-1.5 border border-gray-100 transition-colors"
|
||||
>
|
||||
💬 加入社群
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
110
app/components/Navbar.tsx
Normal file
110
app/components/Navbar.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "城市", icon: "🏙️", href: "/" },
|
||||
{ label: "旅行", icon: "✈️", href: "/meetups" },
|
||||
{ label: "社区", icon: "💬", href: "/dating" },
|
||||
{ label: "探索", icon: "🎒", href: "/#community" },
|
||||
];
|
||||
|
||||
export default function Navbar() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
function isActive(href: string) {
|
||||
if (href.startsWith("/#")) return false;
|
||||
if (href === "/") return pathname === "/";
|
||||
return pathname.startsWith(href);
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-lg border-b border-gray-100">
|
||||
<div className="max-w-[1600px] mx-auto px-4 sm:px-6 h-16 flex items-center justify-between">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center gap-2 shrink-0">
|
||||
<span className="text-2xl">🌏</span>
|
||||
<span className="text-lg font-bold text-gray-900 tracking-tight">
|
||||
游牧中国
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Center Nav */}
|
||||
<div className="hidden md:flex items-center gap-1">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-full text-sm font-medium transition-colors ${
|
||||
isActive(link.href)
|
||||
? "text-gray-900 bg-gray-100"
|
||||
: "text-gray-500 hover:text-gray-900 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<span>{link.icon}</span>
|
||||
<span>{link.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right Side */}
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<button className="hidden sm:block text-sm text-gray-600 hover:text-gray-900 px-3 py-2 rounded-full hover:bg-gray-50 transition-colors">
|
||||
登录
|
||||
</button>
|
||||
<Link
|
||||
href="/join"
|
||||
className="text-sm bg-[#ff4d4f] text-white px-4 py-2 rounded-full hover:bg-[#ff7a45] transition-colors font-medium"
|
||||
>
|
||||
加入我们 →
|
||||
</Link>
|
||||
|
||||
{/* Mobile hamburger */}
|
||||
<button
|
||||
className="md:hidden ml-1 p-2 rounded-lg hover:bg-gray-100"
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
>
|
||||
{mobileOpen ? (
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileOpen && (
|
||||
<div className="md:hidden border-t border-gray-100 bg-white px-4 py-3 space-y-1">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className={`flex items-center gap-2 px-4 py-3 rounded-xl text-sm font-medium ${
|
||||
isActive(link.href)
|
||||
? "text-gray-900 bg-gray-50"
|
||||
: "text-gray-500 hover:bg-gray-50"
|
||||
}`}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
<span className="text-lg">{link.icon}</span>
|
||||
<span>{link.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
<div className="pt-2 border-t border-gray-100">
|
||||
<button className="w-full text-left px-4 py-3 text-sm text-gray-600 hover:bg-gray-50 rounded-xl">
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
245
app/components/Sidebar.tsx
Normal file
245
app/components/Sidebar.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
const meetups = [
|
||||
{ city: "清迈", emoji: "🇹🇭", date: "3月9日 周一", attendees: 12 },
|
||||
{ city: "曼谷", emoji: "🇹🇭", date: "3月12日 周四", attendees: 8 },
|
||||
{ city: "巴厘岛", emoji: "🇮🇩", date: "3月14日 周六", attendees: 15 },
|
||||
{ city: "大理", emoji: "🇨🇳", date: "3月16日 周一", attendees: 6 },
|
||||
{ city: "里斯本", emoji: "🇵🇹", date: "3月20日 周五", attendees: 23 },
|
||||
];
|
||||
|
||||
const travelingNow = [
|
||||
{ name: "小明", bg: "#f43f5e", destination: "清迈" },
|
||||
{ name: "雨晴", bg: "#8b5cf6", destination: "巴厘岛" },
|
||||
{ name: "阿杰", bg: "#3b82f6", destination: "曼谷" },
|
||||
{ name: "小燕", bg: "#10b981", destination: "东京" },
|
||||
{ name: "大伟", bg: "#f59e0b", destination: "里斯本" },
|
||||
{ name: "佳琪", bg: "#ec4899", destination: "墨西哥城" },
|
||||
{ name: "志远", bg: "#06b6d4", destination: "柏林" },
|
||||
{ name: "心怡", bg: "#ef4444", destination: "首尔" },
|
||||
{ name: "凯文", bg: "#84cc16", destination: "巴塞罗那" },
|
||||
{ name: "子豪", bg: "#7c3aed", destination: "成都" },
|
||||
{ name: "浩然", bg: "#0d9488", destination: "大理" },
|
||||
{ name: "雅琪", bg: "#ea580c", destination: "台北" },
|
||||
{ name: "天翔", bg: "#6366f1", destination: "布达佩斯" },
|
||||
{ name: "美玲", bg: "#d946ef", destination: "梅德林" },
|
||||
{ name: "俊杰", bg: "#14b8a6", destination: "普吉岛" },
|
||||
{ name: "晓华", bg: "#f97316", destination: "吉隆坡" },
|
||||
];
|
||||
|
||||
const newMembers = [
|
||||
{ name: "林", bg: "#3b82f6" },
|
||||
{ name: "张", bg: "#8b5cf6" },
|
||||
{ name: "陈", bg: "#f43f5e" },
|
||||
{ name: "周", bg: "#10b981" },
|
||||
{ name: "吴", bg: "#f59e0b" },
|
||||
{ name: "杨", bg: "#ec4899" },
|
||||
{ name: "赵", bg: "#06b6d4" },
|
||||
{ name: "黄", bg: "#ef4444" },
|
||||
{ name: "孙", bg: "#84cc16" },
|
||||
{ name: "马", bg: "#7c3aed" },
|
||||
{ name: "朱", bg: "#0d9488" },
|
||||
{ name: "胡", bg: "#ea580c" },
|
||||
{ name: "郭", bg: "#6366f1" },
|
||||
{ name: "何", bg: "#d946ef" },
|
||||
{ name: "高", bg: "#14b8a6" },
|
||||
{ name: "梁", bg: "#f97316" },
|
||||
{ name: "郑", bg: "#a855f7" },
|
||||
{ name: "罗", bg: "#22c55e" },
|
||||
{ name: "谢", bg: "#e11d48" },
|
||||
{ name: "韩", bg: "#0ea5e9" },
|
||||
];
|
||||
|
||||
const hotTopics = [
|
||||
{ title: "清迈最佳共享办公空间推荐 Top 10", views: 2340 },
|
||||
{ title: "2026年数字游民签证政策汇总", views: 1890 },
|
||||
{ title: "远程工作者的跨国税务规划指南", views: 1560 },
|
||||
{ title: "东南亚 vs 拉美:哪个更适合游民?", views: 1230 },
|
||||
{ title: "新手游民必备工具与装备清单", views: 980 },
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<aside className="w-[320px] shrink-0 hidden lg:block">
|
||||
<div className="sticky top-20 space-y-4 sidebar-scroll max-h-[calc(100vh-100px)] overflow-y-auto pr-1">
|
||||
{/* Insurance / Visa Promo */}
|
||||
<div className="rounded-2xl bg-gradient-to-br from-emerald-50 to-teal-50 border border-emerald-100 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">
|
||||
覆盖 195+ 国家和地区,远程工作专属条款,¥15/天起
|
||||
</p>
|
||||
<button className="w-full text-center text-sm font-semibold text-emerald-700 hover:text-emerald-800 bg-white border border-emerald-200 rounded-full py-2 transition-colors hover:bg-emerald-50">
|
||||
了解更多 →
|
||||
</button>
|
||||
<p className="text-[10px] text-gray-400 text-center mt-2">广告</p>
|
||||
</div>
|
||||
|
||||
{/* Upcoming Meetups */}
|
||||
<div className="rounded-2xl bg-white border border-gray-100 shadow-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">8场/月</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{meetups.map((m) => (
|
||||
<div
|
||||
key={m.city + m.date}
|
||||
className="flex items-center justify-between py-2 border-b border-gray-50 last:border-0 last:pb-0"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="text-lg">{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.attendees} 人报名
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button className="w-full text-center text-xs font-medium text-gray-500 hover:text-gray-700 mt-3 pt-3 border-t border-gray-100 transition-colors">
|
||||
查看全部聚会 →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Traveling Now */}
|
||||
<div className="rounded-2xl bg-white border border-gray-100 shadow-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">
|
||||
{travelingNow.length} 人
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{travelingNow.map((m) => (
|
||||
<div key={m.name} className="group relative">
|
||||
<div
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center text-xs text-white font-medium shadow-sm cursor-pointer hover:scale-110 transition-transform avatar-online"
|
||||
style={{ backgroundColor: m.bg }}
|
||||
title={`${m.name} · ${m.destination}`}
|
||||
>
|
||||
{m.name.charAt(0)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* New Members */}
|
||||
<div className="rounded-2xl bg-white border border-gray-100 shadow-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">86人/月</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{newMembers.map((m) => (
|
||||
<div
|
||||
key={m.name}
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center text-xs text-white font-medium shadow-sm cursor-pointer hover:scale-110 transition-transform"
|
||||
style={{ backgroundColor: m.bg }}
|
||||
>
|
||||
{m.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hot Topics */}
|
||||
<div className="rounded-2xl bg-white border border-gray-100 shadow-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 text-gray-300 mt-0.5 w-4 shrink-0">
|
||||
{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-gray-500 hover:text-gray-700 mt-3 pt-3 border-t border-gray-100 transition-colors">
|
||||
查看更多话题 →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Chat CTA */}
|
||||
<div className="rounded-2xl bg-gradient-to-br from-violet-50 to-purple-50 border border-violet-100 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>
|
||||
<button className="w-full text-center text-sm font-semibold text-violet-700 hover:text-violet-800 bg-white border border-violet-200 rounded-full py-2 transition-colors hover:bg-violet-50">
|
||||
加入聊天 →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Visa Service */}
|
||||
<div className="rounded-2xl bg-gradient-to-br from-amber-50 to-orange-50 border border-amber-100 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">
|
||||
<span className="text-[11px] bg-amber-100/60 text-amber-700 rounded-full px-2 py-0.5">
|
||||
🇹🇭 泰国
|
||||
</span>
|
||||
<span className="text-[11px] bg-amber-100/60 text-amber-700 rounded-full px-2 py-0.5">
|
||||
🇵🇹 葡萄牙
|
||||
</span>
|
||||
<span className="text-[11px] bg-amber-100/60 text-amber-700 rounded-full px-2 py-0.5">
|
||||
🇪🇸 西班牙
|
||||
</span>
|
||||
<span className="text-[11px] bg-amber-100/60 text-amber-700 rounded-full px-2 py-0.5">
|
||||
🇮🇩 印尼
|
||||
</span>
|
||||
</div>
|
||||
<button className="w-full text-center text-sm font-semibold text-amber-700 hover:text-amber-800 bg-white border border-amber-200 rounded-full py-2 transition-colors hover:bg-amber-50">
|
||||
了解详情 →
|
||||
</button>
|
||||
<p className="text-[10px] text-gray-400 text-center mt-2">广告</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
384
app/data/cities.ts
Normal file
384
app/data/cities.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
export interface City {
|
||||
id: number;
|
||||
name: string;
|
||||
country: string;
|
||||
countryEmoji: string;
|
||||
overallScore: number;
|
||||
costPerMonth: number;
|
||||
internetSpeed: number;
|
||||
safety: string;
|
||||
liked: string;
|
||||
temperature: number;
|
||||
humidity: number;
|
||||
nomadsNow: number;
|
||||
description: string;
|
||||
gradientFrom: string;
|
||||
gradientTo: string;
|
||||
icon: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export const cities: City[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "大理",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 4.5,
|
||||
costPerMonth: 3000,
|
||||
internetSpeed: 50,
|
||||
safety: "很好",
|
||||
liked: "极好",
|
||||
temperature: 16,
|
||||
humidity: 60,
|
||||
nomadsNow: 520,
|
||||
description: "苍山洱海,文艺气息浓厚的慢生活之城",
|
||||
gradientFrom: "#2563eb",
|
||||
gradientTo: "#0891b2",
|
||||
icon: "🏯",
|
||||
tags: ["热门", "便宜", "宜居", "山城", "文化", "友好社区"],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "成都",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 4.4,
|
||||
costPerMonth: 4500,
|
||||
internetSpeed: 80,
|
||||
safety: "很好",
|
||||
liked: "极好",
|
||||
temperature: 17,
|
||||
humidity: 75,
|
||||
nomadsNow: 680,
|
||||
description: "天府之国,美食与慢节奏的完美融合",
|
||||
gradientFrom: "#059669",
|
||||
gradientTo: "#65a30d",
|
||||
icon: "🐼",
|
||||
tags: ["热门", "美食", "都市", "新一线", "文化", "友好社区"],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "深圳",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 4.1,
|
||||
costPerMonth: 8000,
|
||||
internetSpeed: 120,
|
||||
safety: "极好",
|
||||
liked: "很好",
|
||||
temperature: 23,
|
||||
humidity: 70,
|
||||
nomadsNow: 850,
|
||||
description: "科技创新之城,创业者的乐园",
|
||||
gradientFrom: "#1d4ed8",
|
||||
gradientTo: "#0284c7",
|
||||
icon: "🌃",
|
||||
tags: ["热门", "高速网络", "都市", "一线城市", "安全"],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "上海",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 4.0,
|
||||
costPerMonth: 10000,
|
||||
internetSpeed: 110,
|
||||
safety: "极好",
|
||||
liked: "好",
|
||||
temperature: 17,
|
||||
humidity: 65,
|
||||
nomadsNow: 1200,
|
||||
description: "国际大都市,东西方文化交汇的中心",
|
||||
gradientFrom: "#b45309",
|
||||
gradientTo: "#78716c",
|
||||
icon: "🏙️",
|
||||
tags: ["热门", "高速网络", "都市", "一线城市", "文化"],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "杭州",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 4.3,
|
||||
costPerMonth: 6000,
|
||||
internetSpeed: 100,
|
||||
safety: "极好",
|
||||
liked: "很好",
|
||||
temperature: 17,
|
||||
humidity: 70,
|
||||
nomadsNow: 560,
|
||||
description: "西湖之畔,互联网重镇与诗画江南",
|
||||
gradientFrom: "#047857",
|
||||
gradientTo: "#0d9488",
|
||||
icon: "⛲",
|
||||
tags: ["热门", "高速网络", "都市", "新一线", "宜居", "文化"],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "厦门",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 4.1,
|
||||
costPerMonth: 5500,
|
||||
internetSpeed: 70,
|
||||
safety: "很好",
|
||||
liked: "很好",
|
||||
temperature: 21,
|
||||
humidity: 75,
|
||||
nomadsNow: 320,
|
||||
description: "海滨花园城市,鼓浪屿的浪漫与闽南风情",
|
||||
gradientFrom: "#ea580c",
|
||||
gradientTo: "#0284c7",
|
||||
icon: "🌊",
|
||||
tags: ["海滨", "温暖", "美食", "宜居", "文化"],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "北京",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 3.8,
|
||||
costPerMonth: 9000,
|
||||
internetSpeed: 100,
|
||||
safety: "极好",
|
||||
liked: "好",
|
||||
temperature: 14,
|
||||
humidity: 50,
|
||||
nomadsNow: 780,
|
||||
description: "千年古都,政治文化中心的深厚底蕴",
|
||||
gradientFrom: "#dc2626",
|
||||
gradientTo: "#b91c1c",
|
||||
icon: "🏛️",
|
||||
tags: ["都市", "一线城市", "文化", "高速网络", "历史名城"],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "广州",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 3.9,
|
||||
costPerMonth: 6500,
|
||||
internetSpeed: 90,
|
||||
safety: "很好",
|
||||
liked: "好",
|
||||
temperature: 23,
|
||||
humidity: 80,
|
||||
nomadsNow: 480,
|
||||
description: "食在广州,千年商都的烟火气",
|
||||
gradientFrom: "#f97316",
|
||||
gradientTo: "#ef4444",
|
||||
icon: "🌺",
|
||||
tags: ["热门", "美食", "都市", "一线城市", "温暖"],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "重庆",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 3.7,
|
||||
costPerMonth: 4000,
|
||||
internetSpeed: 75,
|
||||
safety: "很好",
|
||||
liked: "好",
|
||||
temperature: 18,
|
||||
humidity: 80,
|
||||
nomadsNow: 350,
|
||||
description: "8D魔幻山城,火锅与夜景的不夜城",
|
||||
gradientFrom: "#ef4444",
|
||||
gradientTo: "#c2410c",
|
||||
icon: "🌶️",
|
||||
tags: ["便宜", "美食", "山城", "都市", "文化"],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "昆明",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 4.2,
|
||||
costPerMonth: 3500,
|
||||
internetSpeed: 55,
|
||||
safety: "好",
|
||||
liked: "很好",
|
||||
temperature: 16,
|
||||
humidity: 55,
|
||||
nomadsNow: 380,
|
||||
description: "四季如春的春城,通往东南亚的门户",
|
||||
gradientFrom: "#d946ef",
|
||||
gradientTo: "#059669",
|
||||
icon: "🌸",
|
||||
tags: ["便宜", "温暖", "宜居", "友好社区", "户外运动"],
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "三亚",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 3.5,
|
||||
costPerMonth: 5000,
|
||||
internetSpeed: 60,
|
||||
safety: "好",
|
||||
liked: "好",
|
||||
temperature: 26,
|
||||
humidity: 85,
|
||||
nomadsNow: 250,
|
||||
description: "中国的热带天堂,椰风海韵的度假胜地",
|
||||
gradientFrom: "#0ea5e9",
|
||||
gradientTo: "#06b6d4",
|
||||
icon: "🏖️",
|
||||
tags: ["海滨", "温暖", "户外运动"],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "丽江",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 4.0,
|
||||
costPerMonth: 3200,
|
||||
internetSpeed: 45,
|
||||
safety: "好",
|
||||
liked: "好",
|
||||
temperature: 14,
|
||||
humidity: 50,
|
||||
nomadsNow: 280,
|
||||
description: "玉龙雪山下的纳西古城,灵魂栖息地",
|
||||
gradientFrom: "#6366f1",
|
||||
gradientTo: "#7c3aed",
|
||||
icon: "🏔️",
|
||||
tags: ["便宜", "文化", "山城", "宜居", "历史名城"],
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "西安",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 3.8,
|
||||
costPerMonth: 4200,
|
||||
internetSpeed: 70,
|
||||
safety: "很好",
|
||||
liked: "好",
|
||||
temperature: 15,
|
||||
humidity: 55,
|
||||
nomadsNow: 230,
|
||||
description: "十三朝古都,历史与美食交织的城市",
|
||||
gradientFrom: "#78716c",
|
||||
gradientTo: "#a16207",
|
||||
icon: "🗿",
|
||||
tags: ["便宜", "美食", "文化", "历史名城"],
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "南京",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 3.9,
|
||||
costPerMonth: 5500,
|
||||
internetSpeed: 85,
|
||||
safety: "很好",
|
||||
liked: "好",
|
||||
temperature: 16,
|
||||
humidity: 70,
|
||||
nomadsNow: 400,
|
||||
description: "六朝古都,紫金山下的文化名城",
|
||||
gradientFrom: "#7c3aed",
|
||||
gradientTo: "#059669",
|
||||
icon: "🍃",
|
||||
tags: ["文化", "都市", "新一线", "历史名城"],
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "长沙",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 3.6,
|
||||
costPerMonth: 4500,
|
||||
internetSpeed: 75,
|
||||
safety: "好",
|
||||
liked: "好",
|
||||
temperature: 17,
|
||||
humidity: 75,
|
||||
nomadsNow: 290,
|
||||
description: "娱乐之都,夜生活与美食的不眠城",
|
||||
gradientFrom: "#f59e0b",
|
||||
gradientTo: "#16a34a",
|
||||
icon: "🎪",
|
||||
tags: ["美食", "都市", "新一线", "友好社区"],
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "青岛",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 3.7,
|
||||
costPerMonth: 4800,
|
||||
internetSpeed: 65,
|
||||
safety: "很好",
|
||||
liked: "好",
|
||||
temperature: 13,
|
||||
humidity: 65,
|
||||
nomadsNow: 220,
|
||||
description: "红瓦绿树碧海蓝天,啤酒飘香的海滨城市",
|
||||
gradientFrom: "#2563eb",
|
||||
gradientTo: "#ca8a04",
|
||||
icon: "🍺",
|
||||
tags: ["海滨", "美食", "宜居", "户外运动"],
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "苏州",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 4.0,
|
||||
costPerMonth: 5000,
|
||||
internetSpeed: 80,
|
||||
safety: "极好",
|
||||
liked: "很好",
|
||||
temperature: 17,
|
||||
humidity: 70,
|
||||
nomadsNow: 260,
|
||||
description: "上有天堂下有苏杭,古典园林与现代新城",
|
||||
gradientFrom: "#0d9488",
|
||||
gradientTo: "#64748b",
|
||||
icon: "🏡",
|
||||
tags: ["文化", "宜居", "安全", "历史名城"],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "珠海",
|
||||
country: "中国",
|
||||
countryEmoji: "🇨🇳",
|
||||
overallScore: 3.9,
|
||||
costPerMonth: 5200,
|
||||
internetSpeed: 70,
|
||||
safety: "很好",
|
||||
liked: "好",
|
||||
temperature: 23,
|
||||
humidity: 75,
|
||||
nomadsNow: 180,
|
||||
description: "浪漫之城,毗邻澳门的海滨花园",
|
||||
gradientFrom: "#0891b2",
|
||||
gradientTo: "#6366f1",
|
||||
icon: "🎡",
|
||||
tags: ["海滨", "温暖", "宜居", "安全"],
|
||||
},
|
||||
];
|
||||
|
||||
export const allTags = [
|
||||
"全部",
|
||||
"热门",
|
||||
"便宜",
|
||||
"高速网络",
|
||||
"温暖",
|
||||
"安全",
|
||||
"海滨",
|
||||
"山城",
|
||||
"都市",
|
||||
"美食",
|
||||
"文化",
|
||||
"一线城市",
|
||||
"新一线",
|
||||
"宜居",
|
||||
"历史名城",
|
||||
"友好社区",
|
||||
"户外运动",
|
||||
];
|
||||
304
app/dating/page.tsx
Normal file
304
app/dating/page.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
type TabType = "friends" | "dating";
|
||||
|
||||
interface Profile {
|
||||
id: string;
|
||||
name: string;
|
||||
age: number;
|
||||
location: string;
|
||||
tags: string[];
|
||||
bio: string;
|
||||
gradient: string;
|
||||
initial: string;
|
||||
avatarColor: string;
|
||||
photo: string;
|
||||
}
|
||||
|
||||
interface MatchCard {
|
||||
id: string;
|
||||
name: string;
|
||||
location: string;
|
||||
timeAgo: string;
|
||||
initial: string;
|
||||
color: string;
|
||||
photo: string;
|
||||
}
|
||||
|
||||
const MOCK_PROFILES: Profile[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "林晓雨",
|
||||
age: 28,
|
||||
location: "大理",
|
||||
tags: ["本科学历", "远程工作者", "数字游民", "咖啡爱好者"],
|
||||
bio: "数字游民三年,目前在云南大理远程做产品设计。喜欢爬山、摄影,周末常去洱海边发呆。",
|
||||
gradient: "from-rose-400 via-pink-400 to-fuchsia-400",
|
||||
initial: "林",
|
||||
avatarColor: "bg-rose-500",
|
||||
photo: "https://i.pravatar.cc/150?img=1",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "陈浩然",
|
||||
age: 32,
|
||||
location: "成都",
|
||||
tags: ["硕士学历", "独立开发者", "自由职业者", "素食主义"],
|
||||
bio: "在成都住了两年,做独立开发。喜欢冲浪和冥想,寻找志同道合的旅伴一起探索世界。",
|
||||
gradient: "from-amber-400 via-orange-400 to-red-400",
|
||||
initial: "陈",
|
||||
avatarColor: "bg-amber-500",
|
||||
photo: "https://i.pravatar.cc/150?img=3",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "王思琪",
|
||||
age: 26,
|
||||
location: "深圳",
|
||||
tags: ["本科学历", "内容创作者", "数字游民", "环保主义者"],
|
||||
bio: "自由撰稿人,在深圳写写画画。热爱瑜伽和冥想,相信慢生活才是真正的奢侈。",
|
||||
gradient: "from-emerald-400 via-teal-400 to-cyan-400",
|
||||
initial: "王",
|
||||
avatarColor: "bg-emerald-500",
|
||||
photo: "https://i.pravatar.cc/150?img=5",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "张明远",
|
||||
age: 30,
|
||||
location: "杭州",
|
||||
tags: ["MBA学历", "创业中", "自由职业者", "徒步爱好者"],
|
||||
bio: "正在杭州创业,做远程团队协作工具。周末喜欢去西湖徒步,偶尔品鉴龙井茶。",
|
||||
gradient: "from-violet-400 via-purple-400 to-indigo-400",
|
||||
initial: "张",
|
||||
avatarColor: "bg-violet-500",
|
||||
photo: "https://i.pravatar.cc/150?img=7",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "李雅婷",
|
||||
age: 27,
|
||||
location: "厦门",
|
||||
tags: ["本科学历", "UI设计师", "数字游民", "摄影爱好者"],
|
||||
bio: "在厦门远程做UI设计,业余时间探索闽南美食。喜欢用相机记录生活,寻找一起探店的朋友。",
|
||||
gradient: "from-rose-500 via-red-400 to-orange-500",
|
||||
initial: "李",
|
||||
avatarColor: "bg-rose-500",
|
||||
photo: "https://i.pravatar.cc/150?img=9",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "刘子轩",
|
||||
age: 33,
|
||||
location: "昆明",
|
||||
tags: ["博士学历", "数据科学家", "远程工作者", "户外爱好者"],
|
||||
bio: "在昆明远程工作,热爱户外运动。正在探索云南各地,希望认识更多志同道合的朋友。",
|
||||
gradient: "from-blue-400 via-indigo-400 to-violet-500",
|
||||
initial: "刘",
|
||||
avatarColor: "bg-blue-500",
|
||||
photo: "https://i.pravatar.cc/150?img=11",
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_MATCHES: MatchCard[] = [
|
||||
{ id: "m1", name: "周雨桐", location: "杭州", timeAgo: "2天前", initial: "周", color: "bg-pink-400", photo: "https://i.pravatar.cc/150?img=29" },
|
||||
{ id: "m2", name: "吴俊杰", location: "成都", timeAgo: "1周前", initial: "吴", color: "bg-amber-400", photo: "https://i.pravatar.cc/150?img=34" },
|
||||
{ id: "m3", name: "郑诗涵", location: "上海", timeAgo: "3周前", initial: "郑", color: "bg-emerald-400", photo: "https://i.pravatar.cc/150?img=36" },
|
||||
{ id: "m4", name: "孙宇航", location: "深圳", timeAgo: "1月前", initial: "孙", color: "bg-violet-400", photo: "https://i.pravatar.cc/150?img=38" },
|
||||
{ id: "m5", name: "赵梦琪", location: "厦门", timeAgo: "1年前", initial: "赵", color: "bg-rose-400", photo: "https://i.pravatar.cc/150?img=41" },
|
||||
];
|
||||
|
||||
export default function DatingPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabType>("friends");
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
const currentProfile = MOCK_PROFILES[currentIndex];
|
||||
const hasMoreProfiles = currentIndex < MOCK_PROFILES.length - 1;
|
||||
|
||||
const handleLike = () => {
|
||||
if (hasMoreProfiles) {
|
||||
setCurrentIndex((i) => i + 1);
|
||||
} else {
|
||||
setCurrentIndex(0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDislike = () => {
|
||||
if (hasMoreProfiles) {
|
||||
setCurrentIndex((i) => i + 1);
|
||||
} else {
|
||||
setCurrentIndex(0);
|
||||
}
|
||||
};
|
||||
|
||||
const badgeText =
|
||||
activeTab === "dating"
|
||||
? "你们都在寻找 🌹 约会"
|
||||
: "你们都在寻找 🤝 交友";
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#fafafa]">
|
||||
{/* Tabs */}
|
||||
<div className="flex justify-center pt-6 pb-4 px-4">
|
||||
<div className="inline-flex rounded-full bg-white p-1 shadow-sm border border-gray-100">
|
||||
<button
|
||||
onClick={() => setActiveTab("friends")}
|
||||
className={`px-6 py-2.5 rounded-full text-sm font-medium transition-all duration-200 ${
|
||||
activeTab === "friends"
|
||||
? "bg-[#ff4d4f] text-white shadow-sm"
|
||||
: "text-gray-600 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
交友
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("dating")}
|
||||
className={`px-6 py-2.5 rounded-full text-sm font-medium transition-all duration-200 ${
|
||||
activeTab === "dating"
|
||||
? "bg-[#ff4d4f] text-white shadow-sm"
|
||||
: "text-gray-600 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
约会
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6 max-w-7xl mx-auto px-4 pb-12">
|
||||
{/* Left Sidebar - hidden on mobile */}
|
||||
<aside className="hidden md:block w-72 flex-shrink-0">
|
||||
<div className="sticky top-24">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-1">
|
||||
约会匹配 💕
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mb-4">这些人喜欢了你</p>
|
||||
<div className="space-y-3">
|
||||
{MOCK_MATCHES.map((match) => (
|
||||
<div
|
||||
key={match.id}
|
||||
className="flex items-center gap-3 p-3 rounded-xl bg-white border border-gray-100 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
|
||||
>
|
||||
<img
|
||||
src={match.photo}
|
||||
alt={match.name}
|
||||
className="w-12 h-12 rounded-full object-cover shadow-sm flex-shrink-0"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-gray-900 truncate">
|
||||
{match.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 truncate">
|
||||
{match.location}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 bg-gray-50 px-2 py-1 rounded-full flex-shrink-0">
|
||||
{match.timeAgo}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Center - Main Card */}
|
||||
<main className="flex-1 min-w-0 flex flex-col items-center">
|
||||
<div className="w-full max-w-md mx-auto">
|
||||
<div
|
||||
className={`relative rounded-2xl overflow-hidden bg-gradient-to-br ${currentProfile.gradient} p-8 pb-6 shadow-xl border border-white/20`}
|
||||
>
|
||||
{/* Top right: badge + report */}
|
||||
<div className="absolute top-4 right-4 flex flex-col items-end gap-1">
|
||||
<span className="px-3 py-1.5 rounded-full bg-white/90 text-gray-800 text-xs font-medium backdrop-blur-sm">
|
||||
{badgeText}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
className="text-[#ff4d4f] hover:text-red-600 text-sm font-medium"
|
||||
>
|
||||
举报资料
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="flex justify-center mt-12 mb-6">
|
||||
<img
|
||||
src={currentProfile.photo}
|
||||
alt={currentProfile.name}
|
||||
className="w-28 h-28 rounded-full shadow-lg border-4 border-white/50 object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Name, age, location */}
|
||||
<div className="text-center mb-4">
|
||||
<h3 className="text-xl font-bold text-white drop-shadow-sm">
|
||||
{currentProfile.name},{currentProfile.age}
|
||||
</h3>
|
||||
<p className="text-white/90 text-sm mt-1">
|
||||
📍 {currentProfile.location}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="mb-4">
|
||||
<p className="text-white/90 text-xs font-medium mb-2">
|
||||
匹配标签:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
{currentProfile.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-3 py-1 rounded-full bg-white/80 text-gray-800 text-xs font-medium"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bio */}
|
||||
<p className="text-white/95 text-sm leading-relaxed text-center mb-8 px-2">
|
||||
{currentProfile.bio}
|
||||
</p>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center justify-center gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<button
|
||||
onClick={handleDislike}
|
||||
className="w-16 h-16 rounded-full bg-white shadow-lg flex items-center justify-center text-[#ff4d4f] hover:bg-red-50 hover:scale-105 active:scale-95 transition-all"
|
||||
aria-label="不感兴趣"
|
||||
>
|
||||
<span className="text-2xl">✕</span>
|
||||
</button>
|
||||
<span className="text-xs text-white/80 font-medium">
|
||||
不感兴趣
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<button
|
||||
onClick={handleLike}
|
||||
className="w-16 h-16 rounded-full bg-[#22c55e] shadow-lg flex items-center justify-center text-white hover:bg-[#16a34a] hover:scale-105 active:scale-95 transition-all"
|
||||
aria-label="喜欢"
|
||||
>
|
||||
<span className="text-2xl">💚</span>
|
||||
</button>
|
||||
<span className="text-xs text-white/80 font-medium">喜欢</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress indicator */}
|
||||
<p className="text-center text-sm text-gray-500 mt-4">
|
||||
{currentIndex + 1} / {MOCK_PROFILES.length}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Right spacer for desktop balance */}
|
||||
<div className="hidden lg:block w-72 flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
227
app/globals.css
Normal file
227
app/globals.css
Normal file
@@ -0,0 +1,227 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #fafafa;
|
||||
--foreground: #111827;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-geist-sans, system-ui), "PingFang SC",
|
||||
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ==================== Items Grid (mobile-first) ==================== */
|
||||
.items-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-auto-rows: 180px;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.items-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-auto-rows: 260px;
|
||||
gap: 20px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.items-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-auto-rows: 280px;
|
||||
gap: 30px;
|
||||
padding: 20px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.items-grid {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pin right-items to the LAST explicit column */
|
||||
.items-grid .right-item:nth-child(1) {
|
||||
grid-column: -2 / -1;
|
||||
grid-row: 1;
|
||||
}
|
||||
.items-grid .right-item:nth-child(2) {
|
||||
grid-column: -2 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
.items-grid .right-item:nth-child(3) {
|
||||
grid-column: -2 / -1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
/* ==================== City Card ==================== */
|
||||
.city-card {
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.city-card:hover {
|
||||
animation: pulse 0.6s ease;
|
||||
}
|
||||
|
||||
/* ==================== Right Item ==================== */
|
||||
.right-item {
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
background: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.right-item:hover {
|
||||
animation: pulse 0.6s ease;
|
||||
}
|
||||
|
||||
.right-item-header {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(90deg, #ff4d4f, #ff7a45);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.right-item-header {
|
||||
height: 42px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== Hover / Pulse Animation ==================== */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== Hero Gradient Text ==================== */
|
||||
@keyframes gradient-shift {
|
||||
0%,
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-gradient-text {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#f97316,
|
||||
#ec4899,
|
||||
#8b5cf6,
|
||||
#3b82f6,
|
||||
#f97316
|
||||
);
|
||||
background-size: 300% 300%;
|
||||
animation: gradient-shift 6s ease infinite;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* ==================== Filter Buttons ==================== */
|
||||
.filter-btn {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.filter-btn:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.filter-btn.active {
|
||||
background: #111827;
|
||||
color: white;
|
||||
}
|
||||
.filter-btn.active:hover {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
/* ==================== Community Cards ==================== */
|
||||
.community-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.03);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.community-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* ==================== Utilities ==================== */
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.line-clamp-1 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
280
app/join/page.tsx
Normal file
280
app/join/page.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
type FormState = {
|
||||
nickname: string;
|
||||
gender: string;
|
||||
city: string;
|
||||
occupation: string;
|
||||
education: string;
|
||||
wechat: string;
|
||||
phone: string;
|
||||
intro: string;
|
||||
};
|
||||
|
||||
const OCCUPATIONS = [
|
||||
"自由职业者",
|
||||
"远程工程师",
|
||||
"设计师",
|
||||
"内容创作者",
|
||||
"创业者",
|
||||
"其他",
|
||||
] as const;
|
||||
|
||||
const EDUCATIONS = ["高中", "大专", "本科", "硕士", "博士"] as const;
|
||||
|
||||
const initialForm: FormState = {
|
||||
nickname: "",
|
||||
gender: "",
|
||||
city: "",
|
||||
occupation: "",
|
||||
education: "",
|
||||
wechat: "",
|
||||
phone: "",
|
||||
intro: "",
|
||||
};
|
||||
|
||||
export default function JoinPage() {
|
||||
const [form, setForm] = useState<FormState>(initialForm);
|
||||
|
||||
const updateField = (field: keyof FormState, value: string) => {
|
||||
setForm((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!form.nickname.trim()) {
|
||||
alert("请填写昵称");
|
||||
return;
|
||||
}
|
||||
if (!form.wechat.trim()) {
|
||||
alert("请填写微信号");
|
||||
return;
|
||||
}
|
||||
alert("提交成功!我们将通过微信联系你。");
|
||||
setForm(initialForm);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#fafafa]">
|
||||
{/* Top Section: Gradient Hero */}
|
||||
<section className="relative overflow-hidden bg-gradient-to-br from-orange-400 via-orange-500 to-rose-500 px-4 pt-12 pb-16 sm:pt-16 sm:pb-20">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-white drop-shadow-sm sm:text-4xl md:text-5xl">
|
||||
加入游牧中国社区
|
||||
</h1>
|
||||
<p className="mt-3 text-base text-white/95 sm:text-lg md:text-xl">
|
||||
填写信息加入全球华人数字游民社群
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Form Card */}
|
||||
<section className="relative z-10 mx-auto -mt-8 max-w-lg px-4 pb-12 sm:-mt-10 sm:px-6">
|
||||
<div className="rounded-2xl bg-white p-6 shadow-lg sm:p-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* 昵称 */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="nickname"
|
||||
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||
>
|
||||
昵称 <span className="text-[#ff4d4f]">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="nickname"
|
||||
type="text"
|
||||
value={form.nickname}
|
||||
onChange={(e) => updateField("nickname", e.target.value)}
|
||||
required
|
||||
placeholder="请输入你的昵称"
|
||||
className="w-full rounded-xl border border-gray-200 px-4 py-3 text-gray-900 placeholder-gray-400 transition-colors focus:border-[#ff4d4f] focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 性别 */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">
|
||||
性别
|
||||
</label>
|
||||
<div className="flex gap-6">
|
||||
{["男", "女", "其他"].map((g) => (
|
||||
<label
|
||||
key={g}
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="gender"
|
||||
value={g}
|
||||
checked={form.gender === g}
|
||||
onChange={(e) => updateField("gender", e.target.value)}
|
||||
className="h-4 w-4 border-gray-300 text-[#ff4d4f] focus:ring-[#ff4d4f]/20"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{g}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 当前所在城市 */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="city"
|
||||
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||
>
|
||||
当前所在城市
|
||||
</label>
|
||||
<input
|
||||
id="city"
|
||||
type="text"
|
||||
value={form.city}
|
||||
onChange={(e) => updateField("city", e.target.value)}
|
||||
placeholder="如:清迈、曼谷、大理"
|
||||
className="w-full rounded-xl border border-gray-200 px-4 py-3 text-gray-900 placeholder-gray-400 transition-colors focus:border-[#ff4d4f] focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 职业 */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="occupation"
|
||||
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||
>
|
||||
职业
|
||||
</label>
|
||||
<select
|
||||
id="occupation"
|
||||
value={form.occupation}
|
||||
onChange={(e) => updateField("occupation", e.target.value)}
|
||||
className="w-full rounded-xl border border-gray-200 px-4 py-3 text-gray-900 transition-colors focus:border-[#ff4d4f] focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20"
|
||||
>
|
||||
<option value="">请选择职业</option>
|
||||
{OCCUPATIONS.map((o) => (
|
||||
<option key={o} value={o}>
|
||||
{o}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 学历 */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="education"
|
||||
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||
>
|
||||
学历
|
||||
</label>
|
||||
<select
|
||||
id="education"
|
||||
value={form.education}
|
||||
onChange={(e) => updateField("education", e.target.value)}
|
||||
className="w-full rounded-xl border border-gray-200 px-4 py-3 text-gray-900 transition-colors focus:border-[#ff4d4f] focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20"
|
||||
>
|
||||
<option value="">请选择学历</option>
|
||||
{EDUCATIONS.map((e) => (
|
||||
<option key={e} value={e}>
|
||||
{e}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 微信号 */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="wechat"
|
||||
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||
>
|
||||
微信号 <span className="text-[#ff4d4f]">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="wechat"
|
||||
type="text"
|
||||
value={form.wechat}
|
||||
onChange={(e) => updateField("wechat", e.target.value)}
|
||||
required
|
||||
placeholder="用于联系你加入社群"
|
||||
className="w-full rounded-xl border border-gray-200 px-4 py-3 text-gray-900 placeholder-gray-400 transition-colors focus:border-[#ff4d4f] focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 手机号 */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||
>
|
||||
手机号
|
||||
</label>
|
||||
<div className="flex rounded-xl border border-gray-200 bg-white transition-colors focus-within:border-[#ff4d4f] focus-within:ring-2 focus-within:ring-[#ff4d4f]/20">
|
||||
<span className="flex items-center border-r border-gray-200 px-4 py-3 text-sm text-gray-500">
|
||||
+86
|
||||
</span>
|
||||
<input
|
||||
id="phone"
|
||||
type="tel"
|
||||
value={form.phone}
|
||||
onChange={(e) => updateField("phone", e.target.value)}
|
||||
placeholder="选填"
|
||||
className="flex-1 rounded-r-xl border-0 px-4 py-3 text-gray-900 placeholder-gray-400 focus:ring-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 简单介绍自己 */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="intro"
|
||||
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||
>
|
||||
简单介绍自己
|
||||
</label>
|
||||
<textarea
|
||||
id="intro"
|
||||
rows={3}
|
||||
value={form.intro}
|
||||
onChange={(e) => updateField("intro", e.target.value)}
|
||||
placeholder="简单介绍一下你的背景、兴趣或加入原因..."
|
||||
className="w-full resize-none rounded-xl border border-gray-200 px-4 py-3 text-gray-900 placeholder-gray-400 transition-colors focus:border-[#ff4d4f] focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full rounded-xl bg-[#ff4d4f] px-4 py-3.5 font-semibold text-white transition-colors hover:bg-[#e64547] active:bg-[#cc3d3f]"
|
||||
>
|
||||
提交申请 →
|
||||
</button>
|
||||
<p className="mt-3 text-center text-xs text-gray-500">
|
||||
提交后,我们会在24小时内通过微信联系你
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Bottom: Trust Indicators */}
|
||||
<section className="border-t border-gray-200 bg-white px-4 py-10">
|
||||
<div className="mx-auto flex max-w-lg flex-wrap items-center justify-center gap-8 sm:gap-12">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl font-bold text-gray-900">2,860+</span>
|
||||
<span className="text-sm text-gray-600">成员</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl font-bold text-gray-900">30+</span>
|
||||
<span className="text-sm text-gray-600">城市</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl font-bold text-gray-900">每月 8+</span>
|
||||
<span className="text-sm text-gray-600">聚会</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
app/layout.tsx
Normal file
37
app/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Navbar from "./components/Navbar";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "游牧中国 - 数字游民的理想目的地",
|
||||
description:
|
||||
"发现全球最适合远程工作和生活的城市,加入中国数字游民社区,探索远程办公新方式",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<Navbar />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
441
app/meetups/page.tsx
Normal file
441
app/meetups/page.tsx
Normal file
@@ -0,0 +1,441 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
/* ===== Types ===== */
|
||||
interface Attendee {
|
||||
name: string;
|
||||
photo: string;
|
||||
}
|
||||
|
||||
interface Meetup {
|
||||
id: string;
|
||||
city: string;
|
||||
emoji: string;
|
||||
date: string;
|
||||
time: string;
|
||||
venue: string;
|
||||
address: string;
|
||||
description: string;
|
||||
rsvpCount: number;
|
||||
gradientFrom: string;
|
||||
gradientTo: string;
|
||||
attendees: Attendee[];
|
||||
isUpcoming: boolean;
|
||||
}
|
||||
|
||||
/* ===== Mock Data ===== */
|
||||
const allMeetups: Meetup[] = [
|
||||
{
|
||||
id: "1",
|
||||
city: "大理",
|
||||
emoji: "🏯",
|
||||
date: "2026-03-15",
|
||||
time: "14:00",
|
||||
venue: "大理古城人民路咖啡馆",
|
||||
address: "大理市大理古城人民路中段",
|
||||
description: "苍山脚下,洱海边,与大理的慢生活游民们一起喝咖啡、晒太阳、聊人生。",
|
||||
rsvpCount: 12,
|
||||
gradientFrom: "#3b82f6",
|
||||
gradientTo: "#4f46e5",
|
||||
attendees: [
|
||||
{ name: "林晓", photo: "https://i.pravatar.cc/150?img=1" },
|
||||
{ name: "张明", photo: "https://i.pravatar.cc/150?img=2" },
|
||||
{ name: "陈静", photo: "https://i.pravatar.cc/150?img=3" },
|
||||
{ name: "周杰", photo: "https://i.pravatar.cc/150?img=4" },
|
||||
{ name: "吴芳", photo: "https://i.pravatar.cc/150?img=5" },
|
||||
],
|
||||
isUpcoming: true,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
city: "成都",
|
||||
emoji: "🐼",
|
||||
date: "2026-03-18",
|
||||
time: "19:00",
|
||||
venue: "方所书店",
|
||||
address: "成都市锦江区中纱帽街8号远洋太古里",
|
||||
description: "天府之国游民聚会,在文艺书店里品茶聊天,探讨成都的慢生活与远程工作可能性。",
|
||||
rsvpCount: 9,
|
||||
gradientFrom: "#22c55e",
|
||||
gradientTo: "#10b981",
|
||||
attendees: [
|
||||
{ name: "杨帆", photo: "https://i.pravatar.cc/150?img=6" },
|
||||
{ name: "赵磊", photo: "https://i.pravatar.cc/150?img=7" },
|
||||
{ name: "黄薇", photo: "https://i.pravatar.cc/150?img=8" },
|
||||
{ name: "孙浩", photo: "https://i.pravatar.cc/150?img=9" },
|
||||
],
|
||||
isUpcoming: true,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
city: "深圳",
|
||||
emoji: "🌃",
|
||||
date: "2026-03-20",
|
||||
time: "19:30",
|
||||
venue: "WeWork 卓越前海壹号",
|
||||
address: "深圳市南山区前海深港合作区卓越前海壹号",
|
||||
description: "深圳数字游民线下见面会,探讨创业、远程工作与湾区发展机遇。",
|
||||
rsvpCount: 15,
|
||||
gradientFrom: "#ff4d4f",
|
||||
gradientTo: "#ff7a45",
|
||||
attendees: [
|
||||
{ name: "李娜", photo: "https://i.pravatar.cc/150?img=10" },
|
||||
{ name: "王强", photo: "https://i.pravatar.cc/150?img=11" },
|
||||
{ name: "刘洋", photo: "https://i.pravatar.cc/150?img=12" },
|
||||
{ name: "陈思", photo: "https://i.pravatar.cc/150?img=13" },
|
||||
{ name: "郑凯", photo: "https://i.pravatar.cc/150?img=14" },
|
||||
{ name: "何琳", photo: "https://i.pravatar.cc/150?img=15" },
|
||||
],
|
||||
isUpcoming: true,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
city: "上海",
|
||||
emoji: "🏙️",
|
||||
date: "2026-03-22",
|
||||
time: "19:00",
|
||||
venue: "裸心社 新天地",
|
||||
address: "上海市黄浦区马当路388号复兴SOHO广场",
|
||||
description: "魔都数字游民月度聚会,在时尚共享空间交流国际化远程工作与创业心得。",
|
||||
rsvpCount: 18,
|
||||
gradientFrom: "#ec4899",
|
||||
gradientTo: "#ef4444",
|
||||
attendees: [
|
||||
{ name: "许婷", photo: "https://i.pravatar.cc/150?img=16" },
|
||||
{ name: "马超", photo: "https://i.pravatar.cc/150?img=17" },
|
||||
{ name: "罗敏", photo: "https://i.pravatar.cc/150?img=18" },
|
||||
{ name: "林晓", photo: "https://i.pravatar.cc/150?img=19" },
|
||||
{ name: "张明", photo: "https://i.pravatar.cc/150?img=20" },
|
||||
],
|
||||
isUpcoming: true,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
city: "杭州",
|
||||
emoji: "⛲",
|
||||
date: "2026-03-25",
|
||||
time: "18:00",
|
||||
venue: "梦想小镇·良仓咖啡",
|
||||
address: "杭州市余杭区梦想小镇互联网村",
|
||||
description: "西湖畔数字游民聚会,在创业氛围浓厚的梦想小镇交流产品与运营经验。",
|
||||
rsvpCount: 11,
|
||||
gradientFrom: "#06b6d4",
|
||||
gradientTo: "#0d9488",
|
||||
attendees: [
|
||||
{ name: "陈静", photo: "https://i.pravatar.cc/150?img=21" },
|
||||
{ name: "周杰", photo: "https://i.pravatar.cc/150?img=22" },
|
||||
{ name: "吴芳", photo: "https://i.pravatar.cc/150?img=23" },
|
||||
{ name: "杨帆", photo: "https://i.pravatar.cc/150?img=24" },
|
||||
],
|
||||
isUpcoming: true,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
city: "厦门",
|
||||
emoji: "🌊",
|
||||
date: "2026-03-28",
|
||||
time: "15:00",
|
||||
venue: "鼓浪屿海角8号咖啡馆",
|
||||
address: "厦门市思明区鼓浪屿龙头路",
|
||||
description: "海岛慢生活聚会,在鼓浪屿的文艺咖啡馆里结识热爱海边远程工作的游民朋友。",
|
||||
rsvpCount: 7,
|
||||
gradientFrom: "#0ea5e9",
|
||||
gradientTo: "#0284c7",
|
||||
attendees: [
|
||||
{ name: "赵磊", photo: "https://i.pravatar.cc/150?img=25" },
|
||||
{ name: "黄薇", photo: "https://i.pravatar.cc/150?img=26" },
|
||||
{ name: "孙浩", photo: "https://i.pravatar.cc/150?img=27" },
|
||||
],
|
||||
isUpcoming: true,
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
city: "北京",
|
||||
emoji: "🏛️",
|
||||
date: "2026-03-30",
|
||||
time: "19:00",
|
||||
venue: "氪空间 望京SOHO",
|
||||
address: "北京市朝阳区望京街10号望京SOHO T3",
|
||||
description: "首都数字游民聚会,在望京科技圈交流北漂远程工作与创业故事。",
|
||||
rsvpCount: 14,
|
||||
gradientFrom: "#6366f1",
|
||||
gradientTo: "#4f46e5",
|
||||
attendees: [
|
||||
{ name: "李娜", photo: "https://i.pravatar.cc/150?img=28" },
|
||||
{ name: "王强", photo: "https://i.pravatar.cc/150?img=29" },
|
||||
{ name: "刘洋", photo: "https://i.pravatar.cc/150?img=30" },
|
||||
{ name: "陈思", photo: "https://i.pravatar.cc/150?img=31" },
|
||||
{ name: "郑凯", photo: "https://i.pravatar.cc/150?img=32" },
|
||||
],
|
||||
isUpcoming: true,
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
city: "昆明",
|
||||
emoji: "🌸",
|
||||
date: "2026-04-02",
|
||||
time: "14:00",
|
||||
venue: "翠湖边上咖啡馆",
|
||||
address: "昆明市五华区翠湖东路",
|
||||
description: "春城数字游民聚会,在四季如春的昆明享受慢节奏,交流旅居与远程工作心得。",
|
||||
rsvpCount: 8,
|
||||
gradientFrom: "#f472b6",
|
||||
gradientTo: "#ec4899",
|
||||
attendees: [
|
||||
{ name: "何琳", photo: "https://i.pravatar.cc/150?img=33" },
|
||||
{ name: "许婷", photo: "https://i.pravatar.cc/150?img=34" },
|
||||
{ name: "马超", photo: "https://i.pravatar.cc/150?img=35" },
|
||||
{ name: "罗敏", photo: "https://i.pravatar.cc/150?img=36" },
|
||||
],
|
||||
isUpcoming: true,
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
city: "广州",
|
||||
emoji: "🌺",
|
||||
date: "2026-04-05",
|
||||
time: "19:00",
|
||||
venue: "TIMETABLE 天德广场",
|
||||
address: "广州市天河区临江大道395号天德广场",
|
||||
description: "羊城数字游民聚会,在珠江新城的高端共享空间探讨粤港澳大湾区远程工作机遇。",
|
||||
rsvpCount: 12,
|
||||
gradientFrom: "#f59e0b",
|
||||
gradientTo: "#ea580c",
|
||||
attendees: [
|
||||
{ name: "林晓", photo: "https://i.pravatar.cc/150?img=37" },
|
||||
{ name: "张明", photo: "https://i.pravatar.cc/150?img=38" },
|
||||
{ name: "陈静", photo: "https://i.pravatar.cc/150?img=39" },
|
||||
{ name: "周杰", photo: "https://i.pravatar.cc/150?img=40" },
|
||||
{ name: "吴芳", photo: "https://i.pravatar.cc/150?img=41" },
|
||||
],
|
||||
isUpcoming: true,
|
||||
},
|
||||
/* ===== 往期聚会 ===== */
|
||||
{
|
||||
id: "10",
|
||||
city: "南京",
|
||||
emoji: "🍃",
|
||||
date: "2026-03-05",
|
||||
time: "19:00",
|
||||
venue: "先锋书店 五台山店",
|
||||
address: "南京市鼓楼区广州路173号五台山体育馆地下",
|
||||
description: "金陵数字游民聚会,在南京最具文艺气息的书店里交流古都文化与远程工作。",
|
||||
rsvpCount: 6,
|
||||
gradientFrom: "#84cc16",
|
||||
gradientTo: "#65a30d",
|
||||
attendees: [
|
||||
{ name: "杨帆", photo: "https://i.pravatar.cc/150?img=42" },
|
||||
{ name: "赵磊", photo: "https://i.pravatar.cc/150?img=43" },
|
||||
],
|
||||
isUpcoming: false,
|
||||
},
|
||||
{
|
||||
id: "11",
|
||||
city: "重庆",
|
||||
emoji: "🌶️",
|
||||
date: "2026-03-02",
|
||||
time: "18:30",
|
||||
venue: "洪崖洞观景咖啡馆",
|
||||
address: "重庆市渝中区嘉滨路88号洪崖洞",
|
||||
description: "山城数字游民聚会,在魔幻8D城市里结识热爱火锅与远程工作的游民朋友。",
|
||||
rsvpCount: 10,
|
||||
gradientFrom: "#dc2626",
|
||||
gradientTo: "#b91c1c",
|
||||
attendees: [
|
||||
{ name: "黄薇", photo: "https://i.pravatar.cc/150?img=44" },
|
||||
{ name: "孙浩", photo: "https://i.pravatar.cc/150?img=45" },
|
||||
{ name: "李娜", photo: "https://i.pravatar.cc/150?img=46" },
|
||||
{ name: "王强", photo: "https://i.pravatar.cc/150?img=47" },
|
||||
],
|
||||
isUpcoming: false,
|
||||
},
|
||||
{
|
||||
id: "12",
|
||||
city: "长沙",
|
||||
emoji: "🎪",
|
||||
date: "2026-02-28",
|
||||
time: "19:00",
|
||||
venue: "梅溪湖创新中心 星巴克",
|
||||
address: "长沙市岳麓区梅溪湖国际新城",
|
||||
description: "星城数字游民聚会,在网红城市长沙交流新媒体、直播与远程工作新业态。",
|
||||
rsvpCount: 8,
|
||||
gradientFrom: "#f97316",
|
||||
gradientTo: "#ea580c",
|
||||
attendees: [
|
||||
{ name: "刘洋", photo: "https://i.pravatar.cc/150?img=48" },
|
||||
{ name: "陈思", photo: "https://i.pravatar.cc/150?img=49" },
|
||||
{ name: "郑凯", photo: "https://i.pravatar.cc/150?img=50" },
|
||||
],
|
||||
isUpcoming: false,
|
||||
},
|
||||
];
|
||||
|
||||
/* ===== Date formatting ===== */
|
||||
const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
||||
|
||||
function formatMeetupDate(dateStr: string, time: string): string {
|
||||
const d = new Date(dateStr + "T" + time);
|
||||
const weekday = weekdays[d.getDay()];
|
||||
const year = d.getFullYear();
|
||||
const month = d.getMonth() + 1;
|
||||
const day = d.getDate();
|
||||
const timeFormatted = time.slice(0, 5);
|
||||
return `${weekday}, ${year}年${month}月${day}日, ${timeFormatted}`;
|
||||
}
|
||||
|
||||
/* ===== Meetup Card Component ===== */
|
||||
function MeetupCard({ meetup }: { meetup: Meetup }) {
|
||||
const formattedDate = formatMeetupDate(meetup.date, meetup.time);
|
||||
|
||||
return (
|
||||
<article className="group bg-white rounded-2xl overflow-hidden shadow-[0_2px_8px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)] hover:shadow-[0_12px_28px_rgba(0,0,0,0.12),0_0_0_1px_rgba(0,0,0,0.05)] hover:-translate-y-2 transition-all duration-300 cursor-pointer flex flex-col h-full">
|
||||
{/* Gradient header */}
|
||||
<div
|
||||
className="h-24 sm:h-28 flex flex-col justify-end p-4 text-white"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${meetup.gradientFrom}, ${meetup.gradientTo})`,
|
||||
}}
|
||||
>
|
||||
<p className="text-xs sm:text-sm text-white/90 font-medium">
|
||||
{formattedDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex flex-col p-4 sm:p-5">
|
||||
{/* City + RSVP badge */}
|
||||
<div className="flex items-center justify-between gap-3 mb-2">
|
||||
<h3 className="text-xl sm:text-2xl font-bold text-gray-900">
|
||||
{meetup.emoji} {meetup.city}
|
||||
</h3>
|
||||
<span className="shrink-0 bg-[#ff4d4f] text-white text-xs font-semibold px-2.5 py-1 rounded-full">
|
||||
{meetup.rsvpCount} 人报名
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Venue */}
|
||||
<p className="text-sm text-gray-600 font-medium mb-0.5">
|
||||
@ {meetup.venue}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mb-3">{meetup.address}</p>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm text-gray-600 line-clamp-2 flex-1 mb-2">
|
||||
{meetup.description}
|
||||
</p>
|
||||
<button className="text-sm font-medium text-[#ff4d4f] hover:text-[#ff7a45] transition-colors text-left w-fit">
|
||||
查看详情 →
|
||||
</button>
|
||||
|
||||
{/* Attendee avatars */}
|
||||
<div className="flex items-center gap-1 mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="flex -space-x-2">
|
||||
{meetup.attendees.map((a) => (
|
||||
<img
|
||||
key={a.name}
|
||||
src={a.photo}
|
||||
alt={a.name}
|
||||
className="w-7 h-7 sm:w-8 sm:h-8 rounded-full border-2 border-white shadow-sm object-cover"
|
||||
title={a.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 ml-1">
|
||||
已报名成员
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
/* ===== Page Component ===== */
|
||||
export default function MeetupsPage() {
|
||||
const [activeTab, setActiveTab] = useState<"upcoming" | "previous">("upcoming");
|
||||
|
||||
const upcomingMeetups = useMemo(
|
||||
() => allMeetups.filter((m) => m.isUpcoming),
|
||||
[]
|
||||
);
|
||||
const previousMeetups = useMemo(
|
||||
() => allMeetups.filter((m) => !m.isUpcoming),
|
||||
[]
|
||||
);
|
||||
|
||||
const displayedMeetups =
|
||||
activeTab === "upcoming" ? upcomingMeetups : previousMeetups;
|
||||
const totalCount =
|
||||
activeTab === "upcoming"
|
||||
? upcomingMeetups.length
|
||||
: previousMeetups.length;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#fafafa]">
|
||||
<main className="max-w-[1400px] mx-auto px-4 sm:px-6 md:px-10 py-8 sm:py-12">
|
||||
{/* Page header */}
|
||||
<header className="mb-8">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 flex items-center gap-2">
|
||||
<span className="w-1 h-8 bg-[#ff4d4f] rounded-full" />
|
||||
线下聚会
|
||||
</h1>
|
||||
<p className="text-gray-500 mt-1 text-sm sm:text-base">
|
||||
与全球数字游民面对面交流,拓展人脉,分享经验
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 p-1 bg-white rounded-xl shadow-sm border border-gray-100 mb-8 inline-flex">
|
||||
<button
|
||||
onClick={() => setActiveTab("upcoming")}
|
||||
className={`px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
|
||||
activeTab === "upcoming"
|
||||
? "bg-[#ff4d4f] text-white shadow-sm"
|
||||
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
即将举办
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("previous")}
|
||||
className={`px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
|
||||
activeTab === "previous"
|
||||
? "bg-[#ff4d4f] text-white shadow-sm"
|
||||
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
往期聚会
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Subtitle with count */}
|
||||
<p className="text-gray-500 text-sm mb-6">
|
||||
{activeTab === "upcoming" ? "共" : "共"}
|
||||
<span className="font-semibold text-gray-700 mx-1">
|
||||
{totalCount}
|
||||
</span>
|
||||
{activeTab === "upcoming" ? "场即将举办的聚会" : "场往期聚会"}
|
||||
</p>
|
||||
|
||||
{/* Meetup cards grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5 sm:gap-6">
|
||||
{displayedMeetups.map((meetup) => (
|
||||
<MeetupCard key={meetup.id} meetup={meetup} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
{displayedMeetups.length === 0 && (
|
||||
<div className="text-center py-16 sm:py-24">
|
||||
<span className="text-5xl sm:text-6xl mb-4 block">🍹</span>
|
||||
<p className="text-gray-500 text-base sm:text-lg">
|
||||
{activeTab === "upcoming"
|
||||
? "暂无即将举办的聚会"
|
||||
: "暂无往期聚会记录"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
527
app/page.tsx
Normal file
527
app/page.tsx
Normal file
@@ -0,0 +1,527 @@
|
||||
"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]">
|
||||
🗓 3月15日
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user