This commit is contained in:
eric
2026-03-07 12:33:14 -06:00
parent 8b034e255b
commit 9bb8bcdf0b
28 changed files with 7231 additions and 78 deletions

441
app/meetups/page.tsx Normal file
View 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>
);
}