Skip to content

Commit 8771159

Browse files
CopilotTechQuery
andauthored
[add] Recipe Wiki pages for CookLikeHOC repository with code refactoring (#38)
Co-authored-by: TechQuery <[email protected]>
1 parent 9612670 commit 8771159

File tree

12 files changed

+318
-70
lines changed

12 files changed

+318
-70
lines changed

components/Layout/ContentTree.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Link from 'next/link';
2+
import { FC } from 'react';
3+
import { Badge } from 'react-bootstrap';
4+
5+
import { XContent } from '../../models/Wiki';
6+
7+
export interface ContentTreeProps {
8+
nodes: XContent[];
9+
basePath: string;
10+
level?: number;
11+
metaKey?: string;
12+
}
13+
14+
export const ContentTree: FC<ContentTreeProps> = ({
15+
nodes,
16+
basePath,
17+
level = 0,
18+
metaKey = 'category',
19+
}) => (
20+
<ol className={level === 0 ? 'list-unstyled' : ''}>
21+
{nodes.map(({ path, name, type, meta, children }) => (
22+
<li key={path} className={level > 0 ? 'ms-3' : ''}>
23+
{type !== 'dir' ? (
24+
<Link className="h4 d-flex align-items-center py-1" href={`${basePath}/${path}`}>
25+
{name}
26+
27+
{meta?.[metaKey] && (
28+
<Badge bg="secondary" className="ms-2 small">
29+
{meta[metaKey]}
30+
</Badge>
31+
)}
32+
</Link>
33+
) : (
34+
children?.[0] && (
35+
<details>
36+
<summary className="h4">{name}</summary>
37+
38+
<ContentTree
39+
nodes={children}
40+
basePath={basePath}
41+
level={level + 1}
42+
metaKey={metaKey}
43+
/>
44+
</details>
45+
)
46+
)}
47+
</li>
48+
))}
49+
</ol>
50+
);

components/Navigator/MainNavigator.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
6060
subs: [
6161
{ href: '/wiki', title: t('wiki') },
6262
{ href: '/policy', title: t('policy') },
63+
{ href: '/recipe', title: t('recipe') },
6364
],
6465
},
6566
];

models/Wiki.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export interface XContent extends Content {
1313

1414
export const policyContentStore = new ContentModel('fpsig', 'open-source-policy');
1515

16+
export const recipeContentStore = new ContentModel('Gar-b-age', 'CookLikeHOC');
17+
1618
export class MyWikiNodeModel extends WikiNodeModel {
1719
client = lark.client;
1820
}

next.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ const rewrites: NextConfig['rewrites'] = async () => ({
3838
source: '/proxy/geo.datav.aliyun.com/:path*',
3939
destination: 'https://geo.datav.aliyun.com/:path*',
4040
},
41+
{
42+
source: '/recipe/images/:path*',
43+
destination: 'https://raw.githubusercontent.com/Gar-b-age/CookLikeHOC/main/images/:path*',
44+
},
4145
],
4246
afterFiles: [],
4347
});

pages/api/core.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'core-js/full/array/from-async';
22

33
import { Context, Middleware } from 'koa';
44
import { HTTPError } from 'koajax';
5+
import { Content } from 'mobx-github';
56
import { DataObject } from 'mobx-restful';
67
import { KoaOption, withKoa } from 'next-ssr-middleware';
78
import Path from 'path';
@@ -123,3 +124,17 @@ export function* traverseTree<K extends string, N extends TreeNode<K>>(
123124
yield* traverseTree(node as N, key);
124125
}
125126
}
127+
128+
export const filterMarkdownFiles = (nodes: Content[]) =>
129+
nodes
130+
.filter(
131+
({ path, type, name }) =>
132+
!path.startsWith('.') &&
133+
!name.startsWith('.') &&
134+
(type !== 'file' || MD_pattern.test(name)),
135+
)
136+
.map(({ content, ...rest }) => {
137+
const { meta, markdown } = content ? splitFrontMatter(content) : {};
138+
139+
return { ...rest, content: markdown, meta };
140+
});

pages/policy/[...slug].tsx

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { marked } from 'marked';
22
import { observer } from 'mobx-react';
3+
import { BadgeBar } from 'mobx-restful-table';
34
import { GetStaticPaths, GetStaticProps } from 'next';
45
import { ParsedUrlQuery } from 'querystring';
56
import { FC, useContext } from 'react';
6-
import { Badge, Breadcrumb, Button, Container } from 'react-bootstrap';
7+
import { Breadcrumb, Button, Container } from 'react-bootstrap';
78
import { decodeBase64 } from 'web-utility';
89

910
import { PageHead } from '../../components/Layout/PageHead';
@@ -66,49 +67,29 @@ const WikiPage: FC<XContent> = observer(({ name, path, parent_path, content, met
6667
<header className="mb-4">
6768
<h1>{name}</h1>
6869

69-
{meta && (
70-
<div className="d-flex flex-wrap align-items-center gap-3 mb-3">
71-
<ul className="mb-0">
72-
{meta['主题分类'] && (
73-
<li>
74-
<Badge bg="primary">{meta['主题分类']}</Badge>
75-
</li>
76-
)}
77-
{meta['发文机构'] && (
78-
<li>
79-
<Badge bg="secondary">{meta['发文机构']}</Badge>
80-
</li>
81-
)}
82-
{meta['有效性'] && (
83-
<li>
84-
<Badge bg={meta['有效性'] === '现行有效' ? 'success' : 'warning'}>
85-
{meta['有效性']}
86-
</Badge>
87-
</li>
88-
)}
89-
</ul>
90-
</div>
91-
)}
70+
{meta && <BadgeBar list={Object.values(meta).map(text => ({ text }))} />}
9271

9372
<div className="d-flex justify-content-between align-items-center text-muted small mb-3">
94-
<div>
73+
<dl>
9574
{meta?.['成文日期'] && (
96-
<span>
97-
{t('creation_date')}: {meta['成文日期']}
98-
</span>
75+
<>
76+
<dt>{t('creation_date')}:</dt>
77+
<dd>{meta['成文日期']}</dd>
78+
</>
9979
)}
10080
{meta?.['发布日期'] && meta['发布日期'] !== meta['成文日期'] && (
101-
<span className="ms-3">
102-
{t('publication_date')}: {meta['发布日期']}
103-
</span>
81+
<>
82+
<dt>{t('publication_date')}:</dt>
83+
<dd>{meta['发布日期']}</dd>
84+
</>
10485
)}
105-
</div>
86+
</dl>
10687

10788
<div className="d-flex gap-2">
10889
<Button
10990
variant="outline-primary"
11091
size="sm"
111-
href={`https://github.com/fpsig/open-source-policy/blob/main/China/政策/${path}`}
92+
href={`https://github.com/fpsig/open-source-policy/edit/main/China/政策/${path}`}
11293
target="_blank"
11394
rel="noopener noreferrer"
11495
>

pages/policy/index.tsx

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,24 @@
11
import { observer } from 'mobx-react';
22
import { GetStaticProps } from 'next';
3-
import Link from 'next/link';
43
import React, { FC, useContext } from 'react';
5-
import { Badge, Button, Card, Container } from 'react-bootstrap';
4+
import { Button, Card, Container } from 'react-bootstrap';
65
import { treeFrom } from 'web-utility';
76

7+
import { ContentTree } from '../../components/Layout/ContentTree';
88
import { PageHead } from '../../components/Layout/PageHead';
99
import { I18nContext } from '../../models/Translation';
1010
import { policyContentStore, XContent } from '../../models/Wiki';
11-
import { MD_pattern, splitFrontMatter } from '../api/core';
11+
import { filterMarkdownFiles } from '../api/core';
1212

1313
export const getStaticProps: GetStaticProps<{ nodes: XContent[] }> = async () => {
14-
const nodes = (await policyContentStore.getAll())
15-
.filter(({ type, name }) => type !== 'file' || MD_pattern.test(name))
16-
.map(({ content, ...rest }) => {
17-
const { meta, markdown } = content ? splitFrontMatter(content) : {};
18-
19-
return { ...rest, content: markdown, meta };
20-
});
14+
const nodes = filterMarkdownFiles(await policyContentStore.getAll());
2115

2216
return {
2317
props: JSON.parse(JSON.stringify({ nodes })),
2418
revalidate: 300, // Revalidate every 5 minutes
2519
};
2620
};
2721

28-
const renderTree = (nodes: XContent[], level = 0) => (
29-
<ol className={level === 0 ? 'list-unstyled' : ''}>
30-
{nodes.map(({ path, name, type, meta, children }) => (
31-
<li key={path} className={level > 0 ? 'ms-3' : ''}>
32-
{type !== 'dir' ? (
33-
<Link className="h4 d-flex align-items-center py-1" href={`/policy/${path}`}>
34-
{name}
35-
36-
{meta?.['主题分类'] && (
37-
<Badge bg="secondary" className="ms-2 small">
38-
{meta['主题分类']}
39-
</Badge>
40-
)}
41-
</Link>
42-
) : (
43-
<details>
44-
<summary className="h4">{name}</summary>
45-
46-
{renderTree(children || [], level + 1)}
47-
</details>
48-
)}
49-
</li>
50-
))}
51-
</ol>
52-
);
53-
5422
const WikiIndexPage: FC<{ nodes: XContent[] }> = observer(({ nodes }) => {
5523
const { t } = useContext(I18nContext);
5624

@@ -73,7 +41,11 @@ const WikiIndexPage: FC<{ nodes: XContent[] }> = observer(({ nodes }) => {
7341
</hgroup>
7442

7543
{nodes[0] ? (
76-
renderTree(treeFrom(nodes, 'path', 'parent_path', 'children'))
44+
<ContentTree
45+
nodes={treeFrom(nodes, 'path', 'parent_path', 'children')}
46+
basePath="/policy"
47+
metaKey="主题分类"
48+
/>
7749
) : (
7850
<Card>
7951
<Card.Body className="text-muted text-center">

pages/recipe/[...slug].tsx

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { marked } from 'marked';
2+
import { observer } from 'mobx-react';
3+
import { BadgeBar } from 'mobx-restful-table';
4+
import { GetStaticPaths, GetStaticProps } from 'next';
5+
import { ParsedUrlQuery } from 'querystring';
6+
import { FC, useContext } from 'react';
7+
import { Breadcrumb, Button, Container } from 'react-bootstrap';
8+
import { decodeBase64 } from 'web-utility';
9+
10+
import { PageHead } from '../../components/Layout/PageHead';
11+
import { I18nContext } from '../../models/Translation';
12+
import { recipeContentStore, XContent } from '../../models/Wiki';
13+
import { splitFrontMatter } from '../api/core';
14+
15+
interface RecipePageParams extends ParsedUrlQuery {
16+
slug: string[];
17+
}
18+
19+
export const getStaticPaths: GetStaticPaths<RecipePageParams> = async () => {
20+
const nodes = await recipeContentStore.getAll();
21+
22+
const paths = nodes
23+
.filter(({ type, name }) => type === 'file' && !name.startsWith('.'))
24+
.map(({ path }) => ({ params: { slug: path.split('/') } }));
25+
26+
return { paths, fallback: 'blocking' };
27+
};
28+
29+
export const getStaticProps: GetStaticProps<XContent, RecipePageParams> = async ({ params }) => {
30+
const { slug } = params!;
31+
32+
const node = await recipeContentStore.getOne(slug.join('/'));
33+
34+
const { meta, markdown } = splitFrontMatter(decodeBase64(node.content!));
35+
36+
const markup = marked(markdown) as string;
37+
38+
return {
39+
props: JSON.parse(JSON.stringify({ ...node, content: markup, meta })),
40+
revalidate: 300, // Revalidate every 5 minutes
41+
};
42+
};
43+
44+
const RecipePage: FC<XContent> = observer(({ name, path, parent_path, content, meta }) => {
45+
const { t } = useContext(I18nContext);
46+
47+
return (
48+
<Container className="py-4">
49+
<PageHead title={name} />
50+
51+
<Breadcrumb className="mb-4">
52+
<Breadcrumb.Item href="/recipe">{t('recipe')}</Breadcrumb.Item>
53+
54+
{parent_path?.split('/').map((segment, index, array) => {
55+
const breadcrumbPath = array.slice(0, index + 1).join('/');
56+
57+
return (
58+
<Breadcrumb.Item key={breadcrumbPath} href={`/recipes/${breadcrumbPath}`}>
59+
{segment}
60+
</Breadcrumb.Item>
61+
);
62+
})}
63+
<Breadcrumb.Item active>{name}</Breadcrumb.Item>
64+
</Breadcrumb>
65+
66+
<article>
67+
<header className="mb-4">
68+
<h1>{name}</h1>
69+
70+
{meta && <BadgeBar list={Object.values(meta).map(text => ({ text }))} />}
71+
72+
<div className="d-flex justify-content-between align-items-center text-muted small mb-3">
73+
<dl>
74+
{meta?.['servings'] && (
75+
<>
76+
<dt>{t('servings')}:</dt>
77+
<dd>{meta['servings']}</dd>
78+
</>
79+
)}
80+
{meta?.['preparation_time'] && (
81+
<>
82+
<dt>{t('preparation_time')}:</dt>
83+
<dd>{meta['preparation_time']}</dd>
84+
</>
85+
)}
86+
</dl>
87+
88+
<div className="d-flex gap-2">
89+
<Button
90+
variant="outline-primary"
91+
size="sm"
92+
href={`https://github.com/Gar-b-age/CookLikeHOC/edit/main/${path}`}
93+
target="_blank"
94+
rel="noopener noreferrer"
95+
>
96+
{t('edit_on_github')}
97+
</Button>
98+
{meta?.url && (
99+
<Button
100+
variant="outline-secondary"
101+
size="sm"
102+
href={meta.url}
103+
target="_blank"
104+
rel="noopener noreferrer"
105+
>
106+
{t('view_original')}
107+
</Button>
108+
)}
109+
</div>
110+
</div>
111+
</header>
112+
113+
<div dangerouslySetInnerHTML={{ __html: content || '' }} className="markdown-body" />
114+
</article>
115+
116+
<footer className="mt-5 pt-4 border-top">
117+
<div className="text-center">
118+
<p className="text-muted">
119+
{t('github_document_description')}
120+
<a
121+
href={`https://github.com/Gar-b-age/CookLikeHOC/blob/main/${path}`}
122+
target="_blank"
123+
rel="noopener noreferrer"
124+
className="ms-2"
125+
>
126+
{t('view_or_edit_on_github')}
127+
</a>
128+
</p>
129+
</div>
130+
</footer>
131+
</Container>
132+
);
133+
});
134+
135+
export default RecipePage;

0 commit comments

Comments
 (0)