Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions models/Wiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface XContent extends Content {

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

export const recipeContentStore = new ContentModel('Gar-b-age', 'CookLikeHOC');

export class MyWikiNodeModel extends WikiNodeModel {
client = lark.client;
}
Expand Down
152 changes: 152 additions & 0 deletions pages/recipes/[...slug].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { marked } from 'marked';
import { observer } from 'mobx-react';
import { GetStaticPaths, GetStaticProps } from 'next';
import { ParsedUrlQuery } from 'querystring';
import { FC, useContext } from 'react';
import { Badge, Breadcrumb, Button, Container } from 'react-bootstrap';
import { decodeBase64 } from 'web-utility';

import { PageHead } from '../../components/Layout/PageHead';
import { I18nContext } from '../../models/Translation';
import { recipeContentStore, XContent } from '../../models/Wiki';
import { splitFrontMatter } from '../api/core';

interface RecipePageParams extends ParsedUrlQuery {
slug: string[];
}

export const getStaticPaths: GetStaticPaths<RecipePageParams> = async () => {
const nodes = await recipeContentStore.getAll();

const paths = nodes
.filter(({ type }) => type === 'file')
.map(({ path }) => ({ params: { slug: path.split('/') } }));

return { paths, fallback: 'blocking' };
};

export const getStaticProps: GetStaticProps<XContent, RecipePageParams> = async ({ params }) => {
const { slug } = params!;

const node = await recipeContentStore.getOne(slug.join('/'));

const { meta, markdown } = splitFrontMatter(decodeBase64(node.content!));

const markup = marked(markdown) as string;

return {
props: JSON.parse(JSON.stringify({ ...node, content: markup, meta })),
revalidate: 300, // Revalidate every 5 minutes
};
};

const RecipePage: FC<XContent> = observer(({ name, path, parent_path, content, meta }) => {
const { t } = useContext(I18nContext);

return (
<Container className="py-4">
<PageHead title={name} />

<Breadcrumb className="mb-4">
<Breadcrumb.Item href="/recipes">{t('recipes')}</Breadcrumb.Item>

{parent_path?.split('/').map((segment, index, array) => {
const breadcrumbPath = array.slice(0, index + 1).join('/');

return (
<Breadcrumb.Item key={breadcrumbPath} href={`/recipes/${breadcrumbPath}`}>
{segment}
</Breadcrumb.Item>
);
})}
<Breadcrumb.Item active>{name}</Breadcrumb.Item>
</Breadcrumb>

<article>
<header className="mb-4">
<h1>{name}</h1>

{meta && (
<div className="d-flex flex-wrap align-items-center gap-3 mb-3">
<ul className="mb-0">
{meta['category'] && (
<li>
<Badge bg="primary">{meta['category']}</Badge>
</li>
)}
{meta['difficulty'] && (
<li>
<Badge bg="secondary">{meta['difficulty']}</Badge>
</li>
)}
{meta['time'] && (
<li>
<Badge bg="success">{meta['time']}</Badge>
</li>
)}
</ul>
</div>
)}

<div className="d-flex justify-content-between align-items-center text-muted small mb-3">
<div>
{meta?.['servings'] && (
<span>
{t('servings')}: {meta['servings']}
</span>
)}
{meta?.['prep_time'] && (
<span className="ms-3">
{t('prep_time')}: {meta['prep_time']}
</span>
)}
</div>

<div className="d-flex gap-2">
<Button
variant="outline-primary"
size="sm"
href={`https://github.com/Gar-b-age/CookLikeHOC/blob/main/${path}`}
target="_blank"
rel="noopener noreferrer"
>
{t('edit_on_github')}
</Button>
{meta?.url && (
<Button
variant="outline-secondary"
size="sm"
href={meta.url}
target="_blank"
rel="noopener noreferrer"
>
{t('view_original')}
</Button>
)}
</div>
</div>
</header>

<div dangerouslySetInnerHTML={{ __html: content || '' }} className="markdown-body" />
</article>

<footer className="mt-5 pt-4 border-top">
<div className="text-center">
<p className="text-muted">
{t('github_recipe_description')}
<a
href={`https://github.com/Gar-b-age/CookLikeHOC/blob/main/${path}`}
target="_blank"
rel="noopener noreferrer"
className="ms-2"
>
{t('view_or_edit_on_github')}
</a>
</p>
</div>
</footer>
</Container>
);
});

export default RecipePage;
89 changes: 89 additions & 0 deletions pages/recipes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { observer } from 'mobx-react';
import { GetStaticProps } from 'next';
import Link from 'next/link';
import React, { FC, useContext } from 'react';
import { Badge, Button, Card, Container } from 'react-bootstrap';
import { treeFrom } from 'web-utility';

import { PageHead } from '../../components/Layout/PageHead';
import { I18nContext } from '../../models/Translation';
import { recipeContentStore, XContent } from '../../models/Wiki';
import { MD_pattern, splitFrontMatter } from '../api/core';

export const getStaticProps: GetStaticProps<{ nodes: XContent[] }> = async () => {
const nodes = (await recipeContentStore.getAll())
.filter(({ type, name }) => type !== 'file' || MD_pattern.test(name))
.map(({ content, ...rest }) => {
const { meta, markdown } = content ? splitFrontMatter(content) : {};

return { ...rest, content: markdown, meta };
});

return {
props: JSON.parse(JSON.stringify({ nodes })),
revalidate: 300, // Revalidate every 5 minutes
};
};

const renderTree = (nodes: XContent[], level = 0) => (
<ol className={level === 0 ? 'list-unstyled' : ''}>
{nodes.map(({ path, name, type, meta, children }) => (
<li key={path} className={level > 0 ? 'ms-3' : ''}>
{type !== 'dir' ? (
<Link className="h4 d-flex align-items-center py-1" href={`/recipes/${path}`}>
{name}

{meta?.['category'] && (
<Badge bg="secondary" className="ms-2 small">
{meta['category']}
</Badge>
)}
</Link>
) : (
<details>
<summary className="h4">{name}</summary>

{renderTree(children || [], level + 1)}
</details>
)}
</li>
))}
</ol>
);

const RecipeIndexPage: FC<{ nodes: XContent[] }> = observer(({ nodes }) => {
const { t } = useContext(I18nContext);

return (
<Container className="py-4">
<PageHead title={`${t('recipes')} - ${t('knowledge_base')}`} />

<hgroup className="d-flex justify-content-between align-items-center mb-4">
<h1>
{t('recipes')} ({nodes.length})
</h1>
<Button
href="https://github.com/Gar-b-age/CookLikeHOC"
target="_blank"
rel="noopener noreferrer"
variant="outline-primary"
>
{t('contribute_content')}
</Button>
</hgroup>

{nodes[0] ? (
renderTree(treeFrom(nodes, 'path', 'parent_path', 'children'))
) : (
<Card>
<Card.Body className="text-muted text-center">
<p>{t('no_docs_available')}</p>
<p>{t('docs_auto_load_from_github')}</p>
</Card.Body>
</Card>
)}
</Container>
);
});

export default RecipeIndexPage;
6 changes: 6 additions & 0 deletions translation/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ export default {
github_document_description: 'This is a document page based on a GitHub repository.',
view_or_edit_on_github: 'View or edit this content on GitHub',

// Recipes
recipes: 'Recipes',
servings: 'Servings',
prep_time: 'Prep Time',
github_recipe_description: 'This is a recipe page based on a GitHub repository.',

// China NGO Map
NGO: 'NGO',
China_NGO_DB: 'China NGO Database',
Expand Down
6 changes: 6 additions & 0 deletions translation/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export default {
github_document_description: '这是一个基于 GitHub 仓库的文档页面。',
view_or_edit_on_github: '在 GitHub 上查看或编辑此内容',

// Recipes
recipes: '菜谱',
servings: '份数',
prep_time: '准备时间',
github_recipe_description: '这是一个基于 GitHub 仓库的菜谱页面。',

// China Public Interest Map
NGO: '公益',
China_NGO_DB: '中国公益数据库',
Expand Down
6 changes: 6 additions & 0 deletions translation/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export default {
github_document_description: '這是一個基於 GitHub 存儲庫的文檔頁面。',
view_or_edit_on_github: '在 GitHub 上查看或編輯此內容',

// Recipes
recipes: '菜譜',
servings: '份數',
prep_time: '準備時間',
github_recipe_description: '這是一個基於 GitHub 存儲庫的菜譜頁面。',

// China Public Interest Map
NGO: '公益',
China_NGO_DB: '中國公益數據庫',
Expand Down
Loading