Files
gitlab-instance-0a899031_cn…/app/meetups/page.tsx
2026-03-07 12:33:14 -06:00

442 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useMemo } from "react";
/* ===== 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>
);
}