diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 219db24..8199249 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -14,7 +14,6 @@
"redhat.vscode-yaml",
"clinyong.vscode-css-modules",
"akamud.vscode-caniuse",
- "visualstudioexptteam.intellicode-api-usage-examples",
"pflannery.vscode-versionlens",
"christian-kohler.npm-intellisense",
"esbenp.prettier-vscode",
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 6a99f7c..94180b3 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -7,7 +7,6 @@
"redhat.vscode-yaml",
"clinyong.vscode-css-modules",
"akamud.vscode-caniuse",
- "visualstudioexptteam.intellicode-api-usage-examples",
"pflannery.vscode-versionlens",
"christian-kohler.npm-intellisense",
"esbenp.prettier-vscode",
diff --git a/app/Barrier-Free-Bites/layout.tsx b/app/Barrier-Free-Bites/layout.tsx
deleted file mode 100644
index 70e851f..0000000
--- a/app/Barrier-Free-Bites/layout.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import type { Metadata } from "next";
-import React from "react";
-
-export const metadata: Metadata = {
- title: "无障碍友好美食指南",
-};
-
-export default function Layout({ children }: { children: React.ReactNode }) {
- return children;
-}
\ No newline at end of file
diff --git a/app/activities/[id]/page.tsx b/app/activities/[id]/page.tsx
new file mode 100644
index 0000000..9b14389
--- /dev/null
+++ b/app/activities/[id]/page.tsx
@@ -0,0 +1,238 @@
+import { ChinaMapWrapper } from '@/components/ChinaMapWrapper';
+import { CommentBox } from '@/components/CommentBox';
+import { TimelineItem } from '@/components/TimelineItem';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ ACTIVITIES_API_URL,
+ ExternalDeadlineItem,
+ transformItem,
+} from '@/lib/activities';
+import { formatTimezoneToUTC } from '@/lib/utils';
+import {
+ ArrowLeft,
+ Calendar,
+ Clock,
+ ExternalLink,
+ MapPin,
+ MessageSquare,
+ Pencil,
+} from 'lucide-react';
+import { DateTime } from 'luxon';
+import Link from 'next/link';
+import { notFound } from 'next/navigation';
+
+const DATA_EDIT_URL =
+ 'https://github.com/GoodAction-Hub/GoodAction-data/edit/main/data/activities.yml';
+
+async function findActivity(id: string) {
+ const res = await fetch(ACTIVITIES_API_URL, { cache: 'force-cache' });
+
+ if (!res.ok) throw new URIError(`Failed to fetch activities: ${res.status}`);
+
+ const externalData = (await res.json()) as ExternalDeadlineItem[];
+
+ for (const raw of externalData) {
+ const item = transformItem(raw);
+
+ for (const event of item.events)
+ if (event.id === id) return { item, event };
+ }
+}
+
+export default async function EventDetailPage({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params;
+ const found = await findActivity(id);
+ if (!found) notFound();
+
+ const { item, event } = found;
+ const displayTimezone = 'Asia/Shanghai';
+ const now = DateTime.now().setZone(displayTimezone);
+
+ const upcomingDeadlines = event.timeline
+ .map((tl, index) => ({
+ ...tl,
+ date: DateTime.fromISO(tl.deadline, { zone: event.timezone }),
+ index,
+ }))
+ .filter((tl) => tl.date.setZone(displayTimezone) > now)
+ .sort((a, b) => a.date.toMillis() - b.date.toMillis());
+
+ const nextDeadline = upcomingDeadlines[0];
+ const upcomingIndexes = upcomingDeadlines.map((tl) => tl.index);
+ const ended = upcomingDeadlines.length === 0;
+
+ const eventTimezoneUTC = formatTimezoneToUTC(event.timezone);
+
+ const categoryStyle = {
+ conference: 'bg-gradient-to-r from-purple-500 to-purple-600 text-white',
+ competition: 'bg-gradient-to-r from-pink-500 to-pink-600 text-white',
+ activity: 'bg-gradient-to-r from-cyan-500 to-cyan-600 text-white',
+ }[item.category];
+
+ return (
+
+ {/* Background decorations */}
+
+
+
+ {/* Back + Edit buttons */}
+
+
+ {/* Main card */}
+
+
+ {/* Category badge + title */}
+
+
+
+ {item.category === 'conference'
+ ? '会议'
+ : item.category === 'competition'
+ ? '竞赛'
+ : '活动'}
+
+
+ {event.year}
+
+ {ended && (
+
+ 已结束
+
+ )}
+
+
+
+
+
+ {item.title}
+
+
+
+
+
+
+ {item.description}
+
+
+
+ {/* Tags */}
+ {item.tags.length > 0 && (
+
+ {item.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+ {/* Date / timezone / place */}
+
+ {event.date && (
+
+
+ {event.date}
+
+ )}
+
+
+ {eventTimezoneUTC}
+
+ {event.place && (
+
+
+ {event.place}
+
+ )}
+
+
+ {/* Timeline */}
+
+
+
+ 时间轴
+
+
+
+
+ {event.timeline.map((timelineEvent, index) => (
+
+ ))}
+
+
+
+
+
+
+ {/* Map section */}
+ {event.place && (
+
+
+
+
+ 活动地点
+
+
+
+
+
+
+ )}
+
+ {/* Comments section */}
+
+
+
+
+ 评论
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/deadlines/page.tsx b/app/activities/page.tsx
similarity index 94%
rename from app/deadlines/page.tsx
rename to app/activities/page.tsx
index d51763f..e7b5125 100644
--- a/app/deadlines/page.tsx
+++ b/app/activities/page.tsx
@@ -206,6 +206,14 @@ export default function Home() {
GitHub
+
+ + 发布活动
+
公益慈善会议、竞赛和活动重要截止日期概览,不再错过参与公益事业、奉献爱心和社会服务的机会
diff --git a/app/api/data/route.ts b/app/api/data/route.ts
index 8ca5db2..cda253e 100644
--- a/app/api/data/route.ts
+++ b/app/api/data/route.ts
@@ -1,79 +1,26 @@
-import { NextResponse } from 'next/server'
-import { DeadlineItem, EventData } from '@/lib/data'
+import { NextResponse } from 'next/server';
+import {
+ ACTIVITIES_API_URL,
+ ExternalDeadlineItem,
+ transformItem,
+} from '@/lib/activities';
-export const dynamic = 'force-static'
-
-const DATA_API_URL =
- 'https://goodaction-hub.github.io/GoodAction-data/activities.json'
-
-interface ExternalEventData {
- id: string
- link: string
- start_time?: string
- end_time?: string
- timeline: { deadline: string; comment: string }[]
- timezone: string
- place: string
-}
-
-interface ExternalDeadlineItem {
- title: string
- description: string
- category: 'meetup' | 'conference' | 'competition'
- tags: string[]
- events: ExternalEventData[]
-}
-
-function transformEvent(event: ExternalEventData): EventData {
- const startTime = event.start_time ?? event.timeline[0]?.deadline ?? ''
- const startDate = startTime ? new Date(startTime.replace(' ', 'T')) : null
- const year = startDate ? startDate.getFullYear() : new Date().getFullYear()
-
- const formatDateToChinese = (d: Date) =>
- `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
- let date = startDate ? formatDateToChinese(startDate) : ''
- if (startDate && event.end_time) {
- const endDate = new Date(event.end_time.replace(' ', 'T'))
- if (endDate.getTime() !== startDate.getTime()) {
- date = `${date}-${endDate.getMonth() + 1}月${endDate.getDate()}日`
- }
- }
-
- return {
- year,
- id: event.id,
- link: event.link,
- timeline: event.timeline,
- timezone: event.timezone,
- date,
- place: event.place,
- }
-}
-
-function transformItem(item: ExternalDeadlineItem): DeadlineItem {
- return {
- title: item.title,
- description: item.description,
- category: item.category === 'meetup' ? 'activity' : item.category,
- tags: item.tags ?? [],
- events: item.events.map(transformEvent),
- }
-}
+export const dynamic = 'force-static';
export async function GET() {
try {
- const res = await fetch(DATA_API_URL, { cache: 'force-cache' })
+ const res = await fetch(ACTIVITIES_API_URL, { cache: 'force-cache' });
if (!res.ok) {
return NextResponse.json(
{ error: 'Failed to fetch data from external API' },
{ status: 502 },
- )
+ );
}
- const externalData = (await res.json()) as ExternalDeadlineItem[]
- const data: DeadlineItem[] = externalData.map(transformItem)
- return NextResponse.json(data)
+ const externalData = (await res.json()) as ExternalDeadlineItem[];
+ const data = externalData.map(transformItem);
+ return NextResponse.json(data);
} catch (err) {
- console.error('Failed to fetch data from external API:', err)
- return NextResponse.json({ error: 'Failed to load data' }, { status: 500 })
+ console.error('Failed to fetch data from external API:', err);
+ return NextResponse.json({ error: 'Failed to load data' }, { status: 500 });
}
}
diff --git a/app/globals.css b/app/globals.css
index 26d2551..d19ea73 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,4 +1,5 @@
@import "tailwindcss";
+@import "leaflet/dist/leaflet.css";
/* @import "tw-animate-css"; */
@custom-variant dark (&:is(.dark *));
diff --git a/app/home/page.tsx b/app/home/page.tsx
index e4a5ca3..d1ef059 100644
--- a/app/home/page.tsx
+++ b/app/home/page.tsx
@@ -1,9 +1,9 @@
-"use client"
+'use client';
-import Link from "next/link"
-import Image from "next/image"
-import { Card, CardContent } from "@/components/ui/card"
-import { Calendar, Utensils } from "lucide-react"
+import Image from 'next/image';
+import Link from 'next/link';
+import { Card, CardContent } from '@/components/ui/card';
+import { Calendar, Utensils } from 'lucide-react';
export default function HomeSelector() {
return (
@@ -20,7 +20,7 @@ export default function HomeSelector() {
{/* 公益慈善活动截止日期 */}
-
+
@@ -41,7 +41,7 @@ export default function HomeSelector() {
{/* 无障碍友好美食指南 */}
-
+
@@ -62,23 +62,18 @@ export default function HomeSelector() {
-
- {/* 微信二维码图片 */}
+
+ {/* 群二维码 */}
-
-
-
- 加入GoodAction Hub开源公益生活交流群
-
-
+
- )
+ );
}
diff --git a/app/layout.tsx b/app/layout.tsx
index c32fc4c..b555750 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -3,6 +3,7 @@ import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import Link from 'next/link';
+import Image from 'next/image';
import { SwitchLanguage } from '@/components/SwitchLanguage';
const inter = Inter({
@@ -43,13 +44,29 @@ export default function RootLayout({