Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions components/Home/UpcomingEvents.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,47 @@
import { observer } from 'mobx-react';
import Link from 'next/link';
import { FC } from 'react';
import { Button, Card, Col, Container, Row } from 'react-bootstrap';
import { FC, useContext } from 'react';
import { Button, Card, Col, Row } from 'react-bootstrap';

import { I18nContext } from '../../models/Translation';
import { ArticleMeta } from '../../pages/api/core';
import { SectionTitle } from './SectionTitle';

interface UpcomingEventsProps {
events: ArticleMeta[];
}

export const UpcomingEvents: FC<UpcomingEventsProps> = ({ events }) => (
<div className="py-5 bg-white w-100 m-0">
<Container>
<SectionTitle>近期活动</SectionTitle>
export const UpcomingEvents: FC<UpcomingEventsProps> = observer(({ events }) => {
const { t } = useContext(I18nContext);

return (
<>
<SectionTitle>{t('upcoming_events')}</SectionTitle>

<Row className="g-4" xs={1} sm={2} md={3}>
{events.map(({ name, meta, path }) => (
<Col key={name}>
<Card body>
<Card.Title className="text-dark">{name}</Card.Title>
<Card.Text className="text-dark">
时间: {meta?.start || 'N/A'}
{t('activity_time')}: {meta?.start || 'N/A'}
</Card.Text>
<Card.Text className="text-dark">
地点: {meta?.address || 'N/A'}
{t('activity_location')}: {meta?.address || 'N/A'}
</Card.Text>

<Link href={path || '#'} className="btn btn-primary">
查看详情
{t('view_details')}
</Link>
</Card>
</Col>
))}
</Row>
<div className="text-center mt-4">
<Button variant="outline-primary" size="lg" href="/article/Activity">
查看全部活动
<Button variant="outline-primary" size="lg" href="/activity">
{t('view_all_activities')}
</Button>
</div>
</Container>
</div>
);
</>
);
});
2 changes: 1 addition & 1 deletion eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default tsEslint.config(
warnOnUnsupportedTypeScriptVersion: false,
},
},
// @ts-expect-error Next.js 15.4 compatibility bug
// @ts-expect-error https://github.com/vercel/next.js/issues/81695
rules: {
// spellchecker
'@cspell/spellchecker': [
Expand Down
84 changes: 84 additions & 0 deletions pages/activity/[[...page]].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { observer } from 'mobx-react';
import { Pager, PagerProps } from 'mobx-restful-table';
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next';
import { FC, useContext } from 'react';
import { Container } from 'react-bootstrap';

import { UpcomingEvents } from '../../components/Home/UpcomingEvents';
import { PageHead } from '../../components/Layout/PageHead';
import { isServer } from '../../models/configuration';
import { I18nContext } from '../../models/Translation';
import { ArticleMeta, getMarkdownListSortedByDate } from '../api/core';

const ITEMS_PER_PAGE = 10;

interface ActivityPageProps extends Pick<PagerProps, 'pageIndex' | 'pageCount'> {
activities: ArticleMeta[];
}

export const getStaticPaths: GetStaticPaths = async () => {
const activities = await getMarkdownListSortedByDate('/article/Wiki/_posts/Activity');
const totalPages = Math.ceil(activities.length / ITEMS_PER_PAGE);

const paths = [
{ params: { page: [] } },
...Array.from({ length: totalPages }, (_, i) => ({
params: { page: [i + 1 + ''] },
})),
];

return { paths, fallback: false };
};

export const getStaticProps: GetStaticProps<ActivityPageProps> = async ({ params }) => {
const pageIndex = Number(params?.page?.[0]) || 1;
const activities = await getMarkdownListSortedByDate('/article/Wiki/_posts/Activity');

const startIndex = (pageIndex - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const paginatedActivities = activities.slice(startIndex, endIndex);
const pageCount = Math.ceil(activities.length / ITEMS_PER_PAGE);

return {
props: { activities: paginatedActivities, pageIndex, pageCount },
revalidate: 3600,
};
};

const ActivityPage: FC<InferGetStaticPropsType<typeof getStaticProps>> = observer(
({ activities, pageIndex, pageCount }) => {
const { t } = useContext(I18nContext);

return (
<Container className="py-5 mt-5">
<PageHead title={t('activity_calendar')} />

<hgroup className="d-flex flex-column align-items-center gap-4">
<h1>{t('activity_calendar')}</h1>
<p className="lead">{t('activity_calendar_description')}</p>
</hgroup>

<section className="d-flex flex-column align-items-center gap-3">
<h2>{t('activity_calendar')}</h2>
<iframe
src="https://open-source-bazaar.feishu.cn/share/base/view/shrcn6jNjSKvE9MKPqk56SeSd7p"
className="w-100 vh-100 border-0"
allowFullScreen
/>
</section>

{activities.length > 0 && (
<div className="py-5 bg-white">
<UpcomingEvents events={activities} />
</div>
)}
{pageCount > 1 && !isServer() && (
<div className="d-flex justify-content-center mt-4">
<Pager {...{ pageIndex, pageCount }} pageSize={ITEMS_PER_PAGE} />
</div>
)}
</Container>
);
},
);
export default ActivityPage;
42 changes: 30 additions & 12 deletions pages/api/core.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'core-js/full/array/from-async';

import Router, { RouterParamContext } from '@koa/router';
import { Context, Middleware } from 'koa';
import { HTTPError } from 'koajax';
Expand Down Expand Up @@ -68,10 +70,7 @@ export async function frontMatterOf(path: string) {
return frontMatter && parse(frontMatter);
}

export async function* pageListOf(
path: string,
prefix = 'pages',
): AsyncGenerator<ArticleMeta> {
export async function* pageListOf(path: string, prefix = 'pages'): AsyncGenerator<ArticleMeta> {
const { readdir } = await import('fs/promises');

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

if (meta) article.meta = meta;
} catch (error) {
console.error(
`Error reading front matter for ${node.path}/${node.name}:`,
error,
);
console.error(`Error reading front matter for ${node.path}/${node.name}:`, error);
}
yield article;
}
Expand All @@ -112,12 +108,34 @@ export type TreeNode<K extends string> = {
[key in K]: TreeNode<K>[];
};

export function* traverseTree<K extends string>(
tree: TreeNode<K>,
key: K,
): Generator<TreeNode<K>> {
export function* traverseTree<K extends string>(tree: TreeNode<K>, key: K): Generator<TreeNode<K>> {
for (const node of tree[key] || []) {
yield node;
yield* traverseTree(node, key);
}
}

/**
* Get markdown file list from a directory and its subdirectories, sorted by date descending
*
* @param path - Directory path to search
* @param prefix - Path prefix (default: 'pages')
* @returns - Sorted list of articles
*/
export async function getMarkdownListSortedByDate(
path: string,
prefix = 'pages',
): Promise<ArticleMeta[]> {
const data = await Array.fromAsync(pageListOf(path, prefix));

return data
.map(root => [...traverseTree(root, 'subs')])
.flat()
.filter((event): event is ArticleMeta => 'meta' in event)
.sort((a, b) => {
const dateA = a.meta?.date ? new Date(a.meta.date).getTime() : 0;
const dateB = b.meta?.date ? new Date(b.meta.date).getTime() : 0;

return dateB - dateA;
});
}
102 changes: 47 additions & 55 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,65 +63,57 @@ export const getStaticProps = async () => {
};
};

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

<div className={styles.hero}>
<Container>
<Row className="d-flex align-items-center">
<Col xs={12} md={7}>
<h1 className="fw-bold display-4 hero-dark-text">
freeCodeCamp 成都社区
</h1>
<p className="fs-5 mt-3 hero-dark-text">
一个友好的技术社区,致力于交流、学习和互助,帮助成都的开发者和技术爱好者提升个人技术能力。
</p>
<div className="mt-4">
<Button
variant="primary"
size="lg"
className="me-3"
href="https://open-source-bazaar.feishu.cn/share/base/form/shrcnUC1stOces9sfPbHbEseep8"
>
加入社区
</Button>
<Button variant="outline-primary" size="lg" href="#">
查看活动
</Button>
</div>
</Col>
<Col
xs={12}
md={5}
className="d-flex justify-content-center mt-5 mt-md-0"
>
<div
className="bg-white rounded-4 d-flex justify-content-center align-items-center"
style={{ width: '25rem', height: '18.75rem' }}
<div className={styles.hero}>
<Container>
<Row className="d-flex align-items-center">
<Col xs={12} md={7}>
<h1 className="fw-bold display-4 hero-dark-text">freeCodeCamp 成都社区</h1>
<p className="fs-5 mt-3 hero-dark-text">
一个友好的技术社区,致力于交流、学习和互助,帮助成都的开发者和技术爱好者提升个人技术能力。
</p>
<div className="mt-4">
<Button
variant="primary"
size="lg"
className="me-3"
href="https://open-source-bazaar.feishu.cn/share/base/form/shrcnUC1stOces9sfPbHbEseep8"
>
<Image
src="https://github.com/FreeCodeCamp-Chengdu.png"
alt="freeCodeCamp Chengdu"
fluid
className="rounded-3"
style={{ maxWidth: '80%', maxHeight: '80%' }}
/>
</div>
</Col>
</Row>
</Container>
</div>
<UpcomingEvents events={upcomingEvents} />
加入社区
</Button>
<Button variant="outline-primary" size="lg" href="/activity">
查看活动
</Button>
</div>
</Col>
<Col xs={12} md={5} className="d-flex justify-content-center mt-5 mt-md-0">
<div
className="bg-white rounded-4 d-flex justify-content-center align-items-center"
style={{ width: '25rem', height: '18.75rem' }}
>
<Image
src="https://github.com/FreeCodeCamp-Chengdu.png"
alt="freeCodeCamp Chengdu"
fluid
className="rounded-3"
style={{ maxWidth: '80%', maxHeight: '80%' }}
/>
</div>
</Col>
</Row>
</Container>
</div>
<UpcomingEvents events={upcomingEvents} />

<LatestBlogs articles={latestArticles} />
<LatestBlogs articles={latestArticles} />

<CommunityStats />
<CommunityStats />

<Sponsors sponsors={sponsors} />
</main>
),
);
<Sponsors sponsors={sponsors} />
</main>
));

export default HomePage;
11 changes: 11 additions & 0 deletions translation/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,15 @@ export default {
// Search
keywords: 'Keywords',
search_results: 'Search Results',

// Activity page
activity_calendar: 'Activity Calendar',
activity_calendar_description:
'View the latest activity schedules and historical activity records of freeCodeCamp Chengdu community',
activity_list: 'Activity List',
activity_time: 'Time',
activity_location: 'Location',
view_details: 'View Details',
upcoming_events: 'Upcoming Events',
view_all_activities: 'View All Activities',
} as const;
10 changes: 10 additions & 0 deletions translation/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,14 @@ export default {
// Search
keywords: '关键词',
search_results: '搜索结果',

// Activity page
activity_calendar: '活动日历',
activity_calendar_description: '查看 freeCodeCamp 成都社区的最新活动安排和历史活动记录',
activity_list: '活动列表',
activity_time: '时间',
activity_location: '地点',
view_details: '查看详情',
upcoming_events: '近期活动',
view_all_activities: '查看全部活动',
} as const;
10 changes: 10 additions & 0 deletions translation/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,14 @@ export default {
// Search
keywords: '關鍵詞',
search_results: '搜尋結果',

// Activity page
activity_calendar: '活動日曆',
activity_calendar_description: '查看 freeCodeCamp 成都社群的最新活動安排和歷史活動記錄',
activity_list: '活動列表',
activity_time: '時間',
activity_location: '地點',
view_details: '查看詳情',
upcoming_events: '近期活動',
view_all_activities: '查看全部活動',
} as const;