'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user