Skip to content

Commit 13012cb

Browse files
CopilotTechQuery
andauthored
[add] Activity page with Calendar & List (#40)
Co-authored-by: TechQuery <shiy2008@gmail.com>
1 parent 7bb4582 commit 13012cb

File tree

9 files changed

+212
-83
lines changed

9 files changed

+212
-83
lines changed

components/Home/UpcomingEvents.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,47 @@
1+
import { observer } from 'mobx-react';
12
import Link from 'next/link';
2-
import { FC } from 'react';
3-
import { Button, Card, Col, Container, Row } from 'react-bootstrap';
3+
import { FC, useContext } from 'react';
4+
import { Button, Card, Col, Row } from 'react-bootstrap';
45

6+
import { I18nContext } from '../../models/Translation';
57
import { ArticleMeta } from '../../pages/api/core';
68
import { SectionTitle } from './SectionTitle';
79

810
interface UpcomingEventsProps {
911
events: ArticleMeta[];
1012
}
1113

12-
export const UpcomingEvents: FC<UpcomingEventsProps> = ({ events }) => (
13-
<div className="py-5 bg-white w-100 m-0">
14-
<Container>
15-
<SectionTitle>近期活动</SectionTitle>
14+
export const UpcomingEvents: FC<UpcomingEventsProps> = observer(({ events }) => {
15+
const { t } = useContext(I18nContext);
16+
17+
return (
18+
<>
19+
<SectionTitle>{t('upcoming_events')}</SectionTitle>
1620

1721
<Row className="g-4" xs={1} sm={2} md={3}>
1822
{events.map(({ name, meta, path }) => (
1923
<Col key={name}>
2024
<Card body>
2125
<Card.Title className="text-dark">{name}</Card.Title>
2226
<Card.Text className="text-dark">
23-
时间: {meta?.start || 'N/A'}
27+
{t('activity_time')}: {meta?.start || 'N/A'}
2428
</Card.Text>
2529
<Card.Text className="text-dark">
26-
地点: {meta?.address || 'N/A'}
30+
{t('activity_location')}: {meta?.address || 'N/A'}
2731
</Card.Text>
2832

2933
<Link href={path || '#'} className="btn btn-primary">
30-
查看详情
34+
{t('view_details')}
3135
</Link>
3236
</Card>
3337
</Col>
3438
))}
3539
</Row>
3640
<div className="text-center mt-4">
37-
<Button variant="outline-primary" size="lg" href="/article/Activity">
38-
查看全部活动
41+
<Button variant="outline-primary" size="lg" href="/activity">
42+
{t('view_all_activities')}
3943
</Button>
4044
</div>
41-
</Container>
42-
</div>
43-
);
45+
</>
46+
);
47+
});

eslint.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export default tsEslint.config(
4747
warnOnUnsupportedTypeScriptVersion: false,
4848
},
4949
},
50-
// @ts-expect-error Next.js 15.4 compatibility bug
50+
// @ts-expect-error https://github.com/vercel/next.js/issues/81695
5151
rules: {
5252
// spellchecker
5353
'@cspell/spellchecker': [

pages/activity/[[...page]].tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { observer } from 'mobx-react';
2+
import { Pager, PagerProps } from 'mobx-restful-table';
3+
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next';
4+
import { FC, useContext } from 'react';
5+
import { Container } from 'react-bootstrap';
6+
7+
import { UpcomingEvents } from '../../components/Home/UpcomingEvents';
8+
import { PageHead } from '../../components/Layout/PageHead';
9+
import { isServer } from '../../models/configuration';
10+
import { I18nContext } from '../../models/Translation';
11+
import { ArticleMeta, getMarkdownListSortedByDate } from '../api/core';
12+
13+
const ITEMS_PER_PAGE = 10;
14+
15+
interface ActivityPageProps extends Pick<PagerProps, 'pageIndex' | 'pageCount'> {
16+
activities: ArticleMeta[];
17+
}
18+
19+
export const getStaticPaths: GetStaticPaths = async () => {
20+
const activities = await getMarkdownListSortedByDate('/article/Wiki/_posts/Activity');
21+
const totalPages = Math.ceil(activities.length / ITEMS_PER_PAGE);
22+
23+
const paths = [
24+
{ params: { page: [] } },
25+
...Array.from({ length: totalPages }, (_, i) => ({
26+
params: { page: [i + 1 + ''] },
27+
})),
28+
];
29+
30+
return { paths, fallback: false };
31+
};
32+
33+
export const getStaticProps: GetStaticProps<ActivityPageProps> = async ({ params }) => {
34+
const pageIndex = Number(params?.page?.[0]) || 1;
35+
const activities = await getMarkdownListSortedByDate('/article/Wiki/_posts/Activity');
36+
37+
const startIndex = (pageIndex - 1) * ITEMS_PER_PAGE;
38+
const endIndex = startIndex + ITEMS_PER_PAGE;
39+
const paginatedActivities = activities.slice(startIndex, endIndex);
40+
const pageCount = Math.ceil(activities.length / ITEMS_PER_PAGE);
41+
42+
return {
43+
props: { activities: paginatedActivities, pageIndex, pageCount },
44+
revalidate: 3600,
45+
};
46+
};
47+
48+
const ActivityPage: FC<InferGetStaticPropsType<typeof getStaticProps>> = observer(
49+
({ activities, pageIndex, pageCount }) => {
50+
const { t } = useContext(I18nContext);
51+
52+
return (
53+
<Container className="py-5 mt-5">
54+
<PageHead title={t('activity_calendar')} />
55+
56+
<hgroup className="d-flex flex-column align-items-center gap-4">
57+
<h1>{t('activity_calendar')}</h1>
58+
<p className="lead">{t('activity_calendar_description')}</p>
59+
</hgroup>
60+
61+
<section className="d-flex flex-column align-items-center gap-3">
62+
<h2>{t('activity_calendar')}</h2>
63+
<iframe
64+
src="https://open-source-bazaar.feishu.cn/share/base/view/shrcn6jNjSKvE9MKPqk56SeSd7p"
65+
className="w-100 vh-100 border-0"
66+
allowFullScreen
67+
/>
68+
</section>
69+
70+
{activities.length > 0 && (
71+
<div className="py-5 bg-white">
72+
<UpcomingEvents events={activities} />
73+
</div>
74+
)}
75+
{pageCount > 1 && !isServer() && (
76+
<div className="d-flex justify-content-center mt-4">
77+
<Pager {...{ pageIndex, pageCount }} pageSize={ITEMS_PER_PAGE} />
78+
</div>
79+
)}
80+
</Container>
81+
);
82+
},
83+
);
84+
export default ActivityPage;

pages/api/core.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'core-js/full/array/from-async';
2+
13
import Router, { RouterParamContext } from '@koa/router';
24
import { Context, Middleware } from 'koa';
35
import { HTTPError } from 'koajax';
@@ -68,10 +70,7 @@ export async function frontMatterOf(path: string) {
6870
return frontMatter && parse(frontMatter);
6971
}
7072

71-
export async function* pageListOf(
72-
path: string,
73-
prefix = 'pages',
74-
): AsyncGenerator<ArticleMeta> {
73+
export async function* pageListOf(path: string, prefix = 'pages'): AsyncGenerator<ArticleMeta> {
7574
const { readdir } = await import('fs/promises');
7675

7776
const list = await readdir(prefix + path, { withFileTypes: true });
@@ -93,10 +92,7 @@ export async function* pageListOf(
9392

9493
if (meta) article.meta = meta;
9594
} catch (error) {
96-
console.error(
97-
`Error reading front matter for ${node.path}/${node.name}:`,
98-
error,
99-
);
95+
console.error(`Error reading front matter for ${node.path}/${node.name}:`, error);
10096
}
10197
yield article;
10298
}
@@ -112,12 +108,34 @@ export type TreeNode<K extends string> = {
112108
[key in K]: TreeNode<K>[];
113109
};
114110

115-
export function* traverseTree<K extends string>(
116-
tree: TreeNode<K>,
117-
key: K,
118-
): Generator<TreeNode<K>> {
111+
export function* traverseTree<K extends string>(tree: TreeNode<K>, key: K): Generator<TreeNode<K>> {
119112
for (const node of tree[key] || []) {
120113
yield node;
121114
yield* traverseTree(node, key);
122115
}
123116
}
117+
118+
/**
119+
* Get markdown file list from a directory and its subdirectories, sorted by date descending
120+
*
121+
* @param path - Directory path to search
122+
* @param prefix - Path prefix (default: 'pages')
123+
* @returns - Sorted list of articles
124+
*/
125+
export async function getMarkdownListSortedByDate(
126+
path: string,
127+
prefix = 'pages',
128+
): Promise<ArticleMeta[]> {
129+
const data = await Array.fromAsync(pageListOf(path, prefix));
130+
131+
return data
132+
.map(root => [...traverseTree(root, 'subs')])
133+
.flat()
134+
.filter((event): event is ArticleMeta => 'meta' in event)
135+
.sort((a, b) => {
136+
const dateA = a.meta?.date ? new Date(a.meta.date).getTime() : 0;
137+
const dateB = b.meta?.date ? new Date(b.meta.date).getTime() : 0;
138+
139+
return dateB - dateA;
140+
});
141+
}

pages/index.tsx

Lines changed: 47 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -63,65 +63,57 @@ export const getStaticProps = async () => {
6363
};
6464
};
6565

66-
const HomePage: FC<HomePageProps> = observer(
67-
({ latestArticles, upcomingEvents, sponsors }) => (
68-
<main className="min-vh-100">
69-
<PageHead title="Home" />
66+
const HomePage: FC<HomePageProps> = observer(({ latestArticles, upcomingEvents, sponsors }) => (
67+
<main className="min-vh-100">
68+
<PageHead title="Home" />
7069

71-
<div className={styles.hero}>
72-
<Container>
73-
<Row className="d-flex align-items-center">
74-
<Col xs={12} md={7}>
75-
<h1 className="fw-bold display-4 hero-dark-text">
76-
freeCodeCamp 成都社区
77-
</h1>
78-
<p className="fs-5 mt-3 hero-dark-text">
79-
一个友好的技术社区,致力于交流、学习和互助,帮助成都的开发者和技术爱好者提升个人技术能力。
80-
</p>
81-
<div className="mt-4">
82-
<Button
83-
variant="primary"
84-
size="lg"
85-
className="me-3"
86-
href="https://open-source-bazaar.feishu.cn/share/base/form/shrcnUC1stOces9sfPbHbEseep8"
87-
>
88-
加入社区
89-
</Button>
90-
<Button variant="outline-primary" size="lg" href="#">
91-
查看活动
92-
</Button>
93-
</div>
94-
</Col>
95-
<Col
96-
xs={12}
97-
md={5}
98-
className="d-flex justify-content-center mt-5 mt-md-0"
99-
>
100-
<div
101-
className="bg-white rounded-4 d-flex justify-content-center align-items-center"
102-
style={{ width: '25rem', height: '18.75rem' }}
70+
<div className={styles.hero}>
71+
<Container>
72+
<Row className="d-flex align-items-center">
73+
<Col xs={12} md={7}>
74+
<h1 className="fw-bold display-4 hero-dark-text">freeCodeCamp 成都社区</h1>
75+
<p className="fs-5 mt-3 hero-dark-text">
76+
一个友好的技术社区,致力于交流、学习和互助,帮助成都的开发者和技术爱好者提升个人技术能力。
77+
</p>
78+
<div className="mt-4">
79+
<Button
80+
variant="primary"
81+
size="lg"
82+
className="me-3"
83+
href="https://open-source-bazaar.feishu.cn/share/base/form/shrcnUC1stOces9sfPbHbEseep8"
10384
>
104-
<Image
105-
src="https://github.com/FreeCodeCamp-Chengdu.png"
106-
alt="freeCodeCamp Chengdu"
107-
fluid
108-
className="rounded-3"
109-
style={{ maxWidth: '80%', maxHeight: '80%' }}
110-
/>
111-
</div>
112-
</Col>
113-
</Row>
114-
</Container>
115-
</div>
116-
<UpcomingEvents events={upcomingEvents} />
85+
加入社区
86+
</Button>
87+
<Button variant="outline-primary" size="lg" href="/activity">
88+
查看活动
89+
</Button>
90+
</div>
91+
</Col>
92+
<Col xs={12} md={5} className="d-flex justify-content-center mt-5 mt-md-0">
93+
<div
94+
className="bg-white rounded-4 d-flex justify-content-center align-items-center"
95+
style={{ width: '25rem', height: '18.75rem' }}
96+
>
97+
<Image
98+
src="https://github.com/FreeCodeCamp-Chengdu.png"
99+
alt="freeCodeCamp Chengdu"
100+
fluid
101+
className="rounded-3"
102+
style={{ maxWidth: '80%', maxHeight: '80%' }}
103+
/>
104+
</div>
105+
</Col>
106+
</Row>
107+
</Container>
108+
</div>
109+
<UpcomingEvents events={upcomingEvents} />
117110

118-
<LatestBlogs articles={latestArticles} />
111+
<LatestBlogs articles={latestArticles} />
119112

120-
<CommunityStats />
113+
<CommunityStats />
121114

122-
<Sponsors sponsors={sponsors} />
123-
</main>
124-
),
125-
);
115+
<Sponsors sponsors={sponsors} />
116+
</main>
117+
));
126118

127119
export default HomePage;

translation/en-US.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,15 @@ export default {
5959
// Search
6060
keywords: 'Keywords',
6161
search_results: 'Search Results',
62+
63+
// Activity page
64+
activity_calendar: 'Activity Calendar',
65+
activity_calendar_description:
66+
'View the latest activity schedules and historical activity records of freeCodeCamp Chengdu community',
67+
activity_list: 'Activity List',
68+
activity_time: 'Time',
69+
activity_location: 'Location',
70+
view_details: 'View Details',
71+
upcoming_events: 'Upcoming Events',
72+
view_all_activities: 'View All Activities',
6273
} as const;

translation/zh-CN.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,14 @@ export default {
5959
// Search
6060
keywords: '关键词',
6161
search_results: '搜索结果',
62+
63+
// Activity page
64+
activity_calendar: '活动日历',
65+
activity_calendar_description: '查看 freeCodeCamp 成都社区的最新活动安排和历史活动记录',
66+
activity_list: '活动列表',
67+
activity_time: '时间',
68+
activity_location: '地点',
69+
view_details: '查看详情',
70+
upcoming_events: '近期活动',
71+
view_all_activities: '查看全部活动',
6272
} as const;

translation/zh-TW.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,14 @@ export default {
5959
// Search
6060
keywords: '關鍵詞',
6161
search_results: '搜尋結果',
62+
63+
// Activity page
64+
activity_calendar: '活動日曆',
65+
activity_calendar_description: '查看 freeCodeCamp 成都社群的最新活動安排和歷史活動記錄',
66+
activity_list: '活動列表',
67+
activity_time: '時間',
68+
activity_location: '地點',
69+
view_details: '查看詳情',
70+
upcoming_events: '近期活動',
71+
view_all_activities: '查看全部活動',
6272
} as const;

0 commit comments

Comments
 (0)