442 lines
15 KiB
TypeScript
442 lines
15 KiB
TypeScript
"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>
|
||
);
|
||
}
|