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
50 changes: 50 additions & 0 deletions components/Layout/ContentTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Link from 'next/link';
import { FC } from 'react';
import { Badge } from 'react-bootstrap';

import { XContent } from '../../models/Wiki';

export interface ContentTreeProps {
nodes: XContent[];
basePath: string;
level?: number;
metaKey?: string;
}

export const ContentTree: FC<ContentTreeProps> = ({
nodes,
basePath,
level = 0,
metaKey = 'category',
}) => (
<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={`${basePath}/${path}`}>
{name}

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

<ContentTree
nodes={children}
basePath={basePath}
level={level + 1}
metaKey={metaKey}
/>
</details>
)
)}
</li>
))}
</ol>
);
1 change: 1 addition & 0 deletions components/Navigator/MainNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
subs: [
{ href: '/wiki', title: t('wiki') },
{ href: '/policy', title: t('policy') },
{ href: '/recipe', title: t('recipe') },
],
},
];
Expand Down
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
4 changes: 4 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ const rewrites: NextConfig['rewrites'] = async () => ({
source: '/proxy/geo.datav.aliyun.com/:path*',
destination: 'https://geo.datav.aliyun.com/:path*',
},
{
source: '/recipe/images/:path*',
destination: 'https://raw.githubusercontent.com/Gar-b-age/CookLikeHOC/main/images/:path*',
},
],
afterFiles: [],
});
Expand Down
15 changes: 15 additions & 0 deletions pages/api/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'core-js/full/array/from-async';

import { Context, Middleware } from 'koa';
import { HTTPError } from 'koajax';
import { Content } from 'mobx-github';
import { DataObject } from 'mobx-restful';
import { KoaOption, withKoa } from 'next-ssr-middleware';
import Path from 'path';
Expand Down Expand Up @@ -123,3 +124,17 @@ export function* traverseTree<K extends string, N extends TreeNode<K>>(
yield* traverseTree(node as N, key);
}
}

export const filterMarkdownFiles = (nodes: Content[]) =>
nodes
.filter(
({ path, type, name }) =>
!path.startsWith('.') &&
!name.startsWith('.') &&
(type !== 'file' || MD_pattern.test(name)),
)
.map(({ content, ...rest }) => {
const { meta, markdown } = content ? splitFrontMatter(content) : {};

return { ...rest, content: markdown, meta };
});
47 changes: 14 additions & 33 deletions pages/policy/[...slug].tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { marked } from 'marked';
import { observer } from 'mobx-react';
import { BadgeBar } from 'mobx-restful-table';
import { GetStaticPaths, GetStaticProps } from 'next';
import { ParsedUrlQuery } from 'querystring';
import { FC, useContext } from 'react';
import { Badge, Breadcrumb, Button, Container } from 'react-bootstrap';
import { Breadcrumb, Button, Container } from 'react-bootstrap';
import { decodeBase64 } from 'web-utility';

import { PageHead } from '../../components/Layout/PageHead';
Expand Down Expand Up @@ -66,49 +67,29 @@ const WikiPage: FC<XContent> = observer(({ name, path, parent_path, content, met
<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['主题分类'] && (
<li>
<Badge bg="primary">{meta['主题分类']}</Badge>
</li>
)}
{meta['发文机构'] && (
<li>
<Badge bg="secondary">{meta['发文机构']}</Badge>
</li>
)}
{meta['有效性'] && (
<li>
<Badge bg={meta['有效性'] === '现行有效' ? 'success' : 'warning'}>
{meta['有效性']}
</Badge>
</li>
)}
</ul>
</div>
)}
{meta && <BadgeBar list={Object.values(meta).map(text => ({ text }))} />}

<div className="d-flex justify-content-between align-items-center text-muted small mb-3">
<div>
<dl>
{meta?.['成文日期'] && (
<span>
{t('creation_date')}: {meta['成文日期']}
</span>
<>
<dt>{t('creation_date')}:</dt>
<dd>{meta['成文日期']}</dd>
</>
)}
{meta?.['发布日期'] && meta['发布日期'] !== meta['成文日期'] && (
<span className="ms-3">
{t('publication_date')}: {meta['发布日期']}
</span>
<>
<dt>{t('publication_date')}:</dt>
<dd>{meta['发布日期']}</dd>
</>
)}
</div>
</dl>

<div className="d-flex gap-2">
<Button
variant="outline-primary"
size="sm"
href={`https://github.com/fpsig/open-source-policy/blob/main/China/政策/${path}`}
href={`https://github.com/fpsig/open-source-policy/edit/main/China/政策/${path}`}
target="_blank"
rel="noopener noreferrer"
>
Expand Down
46 changes: 9 additions & 37 deletions pages/policy/index.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,24 @@
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 { Button, Card, Container } from 'react-bootstrap';
import { treeFrom } from 'web-utility';

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

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

return { ...rest, content: markdown, meta };
});
const nodes = filterMarkdownFiles(await policyContentStore.getAll());

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={`/policy/${path}`}>
{name}

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

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

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

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

{nodes[0] ? (
renderTree(treeFrom(nodes, 'path', 'parent_path', 'children'))
<ContentTree
nodes={treeFrom(nodes, 'path', 'parent_path', 'children')}
basePath="/policy"
metaKey="主题分类"
/>
) : (
<Card>
<Card.Body className="text-muted text-center">
Expand Down
135 changes: 135 additions & 0 deletions pages/recipe/[...slug].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { marked } from 'marked';
import { observer } from 'mobx-react';
import { BadgeBar } from 'mobx-restful-table';
import { GetStaticPaths, GetStaticProps } from 'next';
import { ParsedUrlQuery } from 'querystring';
import { FC, useContext } from 'react';
import { 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, name }) => type === 'file' && !name.startsWith('.'))
.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="/recipe">{t('recipe')}</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 && <BadgeBar list={Object.values(meta).map(text => ({ text }))} />}

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

<div className="d-flex gap-2">
<Button
variant="outline-primary"
size="sm"
href={`https://github.com/Gar-b-age/CookLikeHOC/edit/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_document_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;
Loading
Loading