Skip to content

Commit 0195cb0

Browse files
CopilotTechQuery
andcommitted
Add complete recipes section for CookLikeHOC repository
Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
1 parent 2e5ce52 commit 0195cb0

6 files changed

Lines changed: 261 additions & 0 deletions

File tree

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
}

pages/recipes/[...slug].tsx

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

pages/recipes/index.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { observer } from 'mobx-react';
2+
import { GetStaticProps } from 'next';
3+
import Link from 'next/link';
4+
import React, { FC, useContext } from 'react';
5+
import { Badge, Button, Card, Container } from 'react-bootstrap';
6+
import { treeFrom } from 'web-utility';
7+
8+
import { PageHead } from '../../components/Layout/PageHead';
9+
import { I18nContext } from '../../models/Translation';
10+
import { recipeContentStore, XContent } from '../../models/Wiki';
11+
import { MD_pattern, splitFrontMatter } from '../api/core';
12+
13+
export const getStaticProps: GetStaticProps<{ nodes: XContent[] }> = async () => {
14+
const nodes = (await recipeContentStore.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+
});
21+
22+
return {
23+
props: JSON.parse(JSON.stringify({ nodes })),
24+
revalidate: 300, // Revalidate every 5 minutes
25+
};
26+
};
27+
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={`/recipes/${path}`}>
34+
{name}
35+
36+
{meta?.['category'] && (
37+
<Badge bg="secondary" className="ms-2 small">
38+
{meta['category']}
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+
54+
const RecipeIndexPage: FC<{ nodes: XContent[] }> = observer(({ nodes }) => {
55+
const { t } = useContext(I18nContext);
56+
57+
return (
58+
<Container className="py-4">
59+
<PageHead title={`${t('recipes')} - ${t('knowledge_base')}`} />
60+
61+
<hgroup className="d-flex justify-content-between align-items-center mb-4">
62+
<h1>
63+
{t('recipes')} ({nodes.length})
64+
</h1>
65+
<Button
66+
href="https://github.com/Gar-b-age/CookLikeHOC"
67+
target="_blank"
68+
rel="noopener noreferrer"
69+
variant="outline-primary"
70+
>
71+
{t('contribute_content')}
72+
</Button>
73+
</hgroup>
74+
75+
{nodes[0] ? (
76+
renderTree(treeFrom(nodes, 'path', 'parent_path', 'children'))
77+
) : (
78+
<Card>
79+
<Card.Body className="text-muted text-center">
80+
<p>{t('no_docs_available')}</p>
81+
<p>{t('docs_auto_load_from_github')}</p>
82+
</Card.Body>
83+
</Card>
84+
)}
85+
</Container>
86+
);
87+
});
88+
89+
export default RecipeIndexPage;

translation/en-US.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ export default {
102102
github_document_description: 'This is a document page based on a GitHub repository.',
103103
view_or_edit_on_github: 'View or edit this content on GitHub',
104104

105+
// Recipes
106+
recipes: 'Recipes',
107+
servings: 'Servings',
108+
prep_time: 'Prep Time',
109+
github_recipe_description: 'This is a recipe page based on a GitHub repository.',
110+
105111
// China NGO Map
106112
NGO: 'NGO',
107113
China_NGO_DB: 'China NGO Database',

translation/zh-CN.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ export default {
100100
github_document_description: '这是一个基于 GitHub 仓库的文档页面。',
101101
view_or_edit_on_github: '在 GitHub 上查看或编辑此内容',
102102

103+
// Recipes
104+
recipes: '菜谱',
105+
servings: '份数',
106+
prep_time: '准备时间',
107+
github_recipe_description: '这是一个基于 GitHub 仓库的菜谱页面。',
108+
103109
// China Public Interest Map
104110
NGO: '公益',
105111
China_NGO_DB: '中国公益数据库',

translation/zh-TW.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ export default {
100100
github_document_description: '這是一個基於 GitHub 存儲庫的文檔頁面。',
101101
view_or_edit_on_github: '在 GitHub 上查看或編輯此內容',
102102

103+
// Recipes
104+
recipes: '菜譜',
105+
servings: '份數',
106+
prep_time: '準備時間',
107+
github_recipe_description: '這是一個基於 GitHub 存儲庫的菜譜頁面。',
108+
103109
// China Public Interest Map
104110
NGO: '公益',
105111
China_NGO_DB: '中國公益數據庫',

0 commit comments

Comments
 (0)