Skip to content

Commit 5fc68ef

Browse files
authored
[add] Lark Wiki pages (#32)
1 parent 566646d commit 5fc68ef

File tree

8 files changed

+469
-296
lines changed

8 files changed

+469
-296
lines changed

.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
NEXT_PUBLIC_SITE_NAME = 开源市集
22
NEXT_PUBLIC_SITE_SUMMARY =
33
NEXT_PUBLIC_LOGO = https://github.com/Open-Source-Bazaar.png
4+
5+
NEXT_PUBLIC_LARK_API_HOST = https://open.feishu.cn/open-apis/
6+
NEXT_PUBLIC_LARK_APP_ID = cli_a8094a652022900d
7+
NEXT_PUBLIC_LARK_WIKI_URL = https://open-source-bazaar.feishu.cn/wiki/space/7052192153363054596

components/Navigator/MainNavigator.tsx

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,57 @@
11
import { observer } from 'mobx-react';
22
import dynamic from 'next/dynamic';
33
import { useRouter } from 'next/router';
4-
import { FC, useContext } from 'react';
5-
import { Container, Nav, Navbar } from 'react-bootstrap';
4+
import { AnchorHTMLAttributes, FC, useContext } from 'react';
5+
import { Container, Image, Nav, Navbar, NavDropdown } from 'react-bootstrap';
66

7+
import { DefaultImage } from '../../models/configuration';
78
import { i18n, I18nContext } from '../../models/Translation';
89

910
const LanguageMenu = dynamic(() => import('./LanguageMenu'), { ssr: false });
1011

11-
export type MenuItem = Record<'href' | 'name', string>;
12+
export interface MenuItem extends Pick<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'title'> {
13+
subs?: MenuItem[];
14+
}
1215

1316
const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
14-
{ href: '/article/about', name: t('about') },
15-
{ href: '/article/history', name: t('history') },
16-
{ href: '/article/code-of-conduct', name: t('code_of_conduct') },
17-
{ href: '/article/join-us', name: t('join_us') },
1817
{
19-
href: '/article/open-collaborator-award',
20-
name: t('open_collaborator_award'),
18+
title: t('about'),
19+
subs: [
20+
{ href: '/article/about', title: t('about') },
21+
{ href: '/article/history', title: t('history') },
22+
{ href: '/article/code-of-conduct', title: t('code_of_conduct') },
23+
],
24+
},
25+
{
26+
title: t('join_us'),
27+
subs: [
28+
{ href: '/article/join-us', title: t('join_us') },
29+
{
30+
href: '/article/open-collaborator-award',
31+
title: t('open_collaborator_award'),
32+
},
33+
{ href: '/volunteer', title: t('volunteer') },
34+
],
35+
},
36+
{
37+
title: t('open_source_projects'),
38+
subs: [
39+
{ href: '/project', title: t('open_source_projects') },
40+
{ href: '/issue', title: 'GitHub issues' },
41+
{
42+
href: 'https://github.com/Open-Source-Bazaar/Git-Hackathon-scaffold',
43+
title: t('hackathon'),
44+
},
45+
{ href: '/license-filter', title: t('license_filter') },
46+
],
2147
},
22-
{ href: '/volunteer', name: t('volunteer') },
23-
{ href: '/project', name: t('open_source_projects') },
24-
{ href: '/issue', name: 'GitHub issues' },
2548
{
26-
href: 'https://github.com/Open-Source-Bazaar/Git-Hackathon-scaffold',
27-
name: t('hackathon'),
49+
title: t('wiki'),
50+
subs: [
51+
{ href: '/wiki', title: t('wiki') },
52+
{ href: '/policy', title: t('policy') },
53+
],
2854
},
29-
{ href: '/license-filter', name: t('license_filter') },
30-
{ href: '/policy', name: t('policy') },
3155
];
3256

3357
export interface MainNavigatorProps {
@@ -44,21 +68,32 @@ export const MainNavigator: FC<MainNavigatorProps> = observer(({ menu }) => {
4468
return (
4569
<Navbar bg="dark" variant="dark" fixed="top" expand="lg">
4670
<Container>
47-
<Navbar.Brand href="/" className="fw-bolder">
71+
<Navbar.Brand href="/" className="fw-bolder d-flex align-items-center gap-2">
72+
<Image width={40} src={DefaultImage} alt={t('open_source_bazaar')} />
4873
{t('open_source_bazaar')}
4974
</Navbar.Brand>
5075
<Navbar.Toggle aria-controls="navbarScroll" />
5176
<Navbar.Collapse id="navbarScroll">
5277
<Nav className="me-auto my-2 my-lg-0" navbarScroll>
53-
{menu.map(({ href, name }) => (
54-
<Nav.Link
55-
key={`${href}-${name}`}
56-
href={href}
57-
className={pathname === `${href}` ? 'fw-bolder text-light' : ''}
58-
>
59-
{name}
60-
</Nav.Link>
61-
))}
78+
{menu.map(({ href, title, subs }) =>
79+
subs ? (
80+
<NavDropdown key={title} title={title}>
81+
{subs.map(({ href, title }) => (
82+
<NavDropdown.Item key={href} href={href}>
83+
{title}
84+
</NavDropdown.Item>
85+
))}
86+
</NavDropdown>
87+
) : (
88+
<Nav.Link
89+
key={`${href}-${title}`}
90+
href={href}
91+
className={pathname === `${href}` ? 'fw-bolder text-light' : ''}
92+
>
93+
{title}
94+
</Nav.Link>
95+
),
96+
)}
6297
</Nav>
6398

6499
<LanguageMenu />

models/Wiki.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
import { Content, ContentModel } from 'mobx-github';
2+
import { DocumentModel, WikiNodeModel } from 'mobx-lark';
23
import { DataObject } from 'mobx-restful';
34

5+
import { lark } from '../pages/api/Lark/core';
46
import './Base';
7+
import { LarkWikiDomain, LarkWikiId } from './configuration';
58

69
export interface XContent extends Content {
710
meta?: DataObject;
811
children?: XContent[];
912
}
1013

1114
export const policyContentStore = new ContentModel('fpsig', 'open-source-policy');
15+
16+
export class MyWikiNodeModel extends WikiNodeModel {
17+
client = lark.client;
18+
}
19+
20+
export const wikiStore = new MyWikiNodeModel(LarkWikiDomain, LarkWikiId);
21+
22+
export class MyDocumentModel extends DocumentModel {
23+
client = lark.client;
24+
}
25+
26+
export const documentStore = new MyDocumentModel(LarkWikiDomain);

models/configuration.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ export const LarkAppMeta = {
2727
id: process.env.NEXT_PUBLIC_LARK_APP_ID!,
2828
secret: process.env.LARK_APP_SECRET!,
2929
};
30+
const { hostname, pathname } = new URL(process.env.NEXT_PUBLIC_LARK_WIKI_URL!);
31+
32+
export const LarkWikiDomain = hostname;
33+
export const LarkWikiId = pathname.split('/').pop()!;

package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"private": true,
66
"scripts": {
77
"prepare": "husky",
8+
"install": "pnpx git-utility download https://github.com/Open-Source-Bazaar/key-vault main Open-Source-Bazaar.github.io || true",
89
"dev": "next dev",
910
"build": "next build",
1011
"start": "next start",
@@ -13,8 +14,8 @@
1314
},
1415
"dependencies": {
1516
"@koa/router": "^14.0.0",
16-
"@mdx-js/loader": "^3.1.0",
17-
"@mdx-js/react": "^3.1.0",
17+
"@mdx-js/loader": "^3.1.1",
18+
"@mdx-js/react": "^3.1.1",
1819
"@next/mdx": "^15.5.2",
1920
"core-js": "^3.45.1",
2021
"file-type": "^21.0.0",
@@ -27,7 +28,7 @@
2728
"mobx": "^6.13.7",
2829
"mobx-github": "^0.4.0",
2930
"mobx-i18n": "^0.7.1",
30-
"mobx-lark": "^2.4.0",
31+
"mobx-lark": "^2.4.1",
3132
"mobx-react": "^9.2.0",
3233
"mobx-react-helper": "^0.5.1",
3334
"mobx-restful": "^2.1.0",
@@ -47,11 +48,11 @@
4748
"@babel/plugin-proposal-decorators": "^7.28.0",
4849
"@babel/plugin-transform-typescript": "^7.28.0",
4950
"@babel/preset-react": "^7.27.1",
50-
"@cspell/eslint-plugin": "^9.2.0",
51+
"@cspell/eslint-plugin": "^9.2.1",
5152
"@eslint/js": "^9.34.0",
5253
"@next/eslint-plugin-next": "^15.5.2",
5354
"@softonus/prettier-plugin-duplicate-remover": "^1.1.2",
54-
"@stylistic/eslint-plugin": "^5.2.3",
55+
"@stylistic/eslint-plugin": "^5.3.1",
5556
"@types/eslint-config-prettier": "^6.11.3",
5657
"@types/koa": "^3.0.0",
5758
"@types/koa__router": "^12.0.4",
@@ -69,13 +70,13 @@
6970
"jiti": "^2.5.1",
7071
"less": "^4.4.1",
7172
"less-loader": "^12.3.0",
72-
"lint-staged": "^16.1.5",
73+
"lint-staged": "^16.1.6",
7374
"next-with-less": "^3.0.1",
7475
"prettier": "^3.6.2",
7576
"prettier-plugin-css-order": "^2.1.2",
7677
"sass": "^1.91.0",
7778
"typescript": "~5.9.2",
78-
"typescript-eslint": "^8.41.0"
79+
"typescript-eslint": "^8.42.0"
7980
},
8081
"resolutions": {
8182
"next": "$next"

pages/wiki/[node_token].tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Block, renderBlocks, WikiNode } from 'mobx-lark';
2+
import { GetStaticPaths, GetStaticProps } from 'next';
3+
import { FC } from 'react';
4+
import { Container } from 'react-bootstrap';
5+
import { Minute, Second } from 'web-utility';
6+
7+
import { PageHead } from '../../components/Layout/PageHead';
8+
import { documentStore, wikiStore } from '../../models/Wiki';
9+
import { lark } from '../api/Lark/core';
10+
11+
export const getStaticPaths: GetStaticPaths = async () => {
12+
await lark.getAccessToken();
13+
14+
const nodes = await wikiStore.getAll();
15+
16+
return {
17+
paths: nodes.map(({ node_token }) => ({ params: { node_token } })),
18+
fallback: 'blocking',
19+
};
20+
};
21+
22+
export const getStaticProps: GetStaticProps = async ({ params }) => {
23+
await lark.getAccessToken();
24+
25+
const node = await wikiStore.getOne(params!.node_token as string);
26+
27+
if (node?.obj_type !== 'docx') return { notFound: true };
28+
29+
try {
30+
const blocks = await documentStore.getOneBlocks(
31+
node.obj_token,
32+
token => `/api/Lark/file/${token}/placeholder`,
33+
);
34+
35+
return { props: { node, blocks } };
36+
} catch (error) {
37+
console.error(error);
38+
39+
return { notFound: true, revalidate: Minute / Second };
40+
}
41+
};
42+
43+
interface WikiDocumentPageProps {
44+
node: WikiNode;
45+
blocks: Block<any, any, any>[];
46+
}
47+
48+
const WikiDocumentPage: FC<WikiDocumentPageProps> = ({ node, blocks }) => (
49+
<Container>
50+
<PageHead title={node.title} />
51+
52+
{renderBlocks(blocks)}
53+
</Container>
54+
);
55+
56+
export default WikiDocumentPage;

pages/wiki/index.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { WikiNode } from 'mobx-lark';
2+
import { observer } from 'mobx-react';
3+
import { GetStaticProps } from 'next';
4+
import { FC, useContext } from 'react';
5+
import { 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 { wikiStore } from '../../models/Wiki';
11+
import { lark } from '../api/Lark/core';
12+
13+
export const getStaticProps: GetStaticProps = async () => {
14+
await lark.getAccessToken();
15+
16+
const nodes = await wikiStore.getAll();
17+
18+
return { props: { nodes } };
19+
};
20+
21+
interface XWikiNode extends WikiNode {
22+
// eslint-disable-next-line no-restricted-syntax
23+
children?: XWikiNode[];
24+
}
25+
26+
const renderTree = (children?: XWikiNode[]) =>
27+
children && (
28+
<ol>
29+
{children.map(({ node_token, title, children }) => (
30+
<li key={node_token}>
31+
<a href={`/wiki/${node_token}`}>{title}</a>
32+
33+
{renderTree(children)}
34+
</li>
35+
))}
36+
</ol>
37+
);
38+
39+
const WikiIndexPage: FC<{ nodes: XWikiNode[] }> = observer(({ nodes }) => {
40+
const { t } = useContext(I18nContext);
41+
42+
return (
43+
<Container>
44+
<PageHead title={t('wiki')} />
45+
46+
<h1>{t('wiki')}</h1>
47+
48+
{renderTree(treeFrom(nodes, 'node_token', 'parent_node_token', 'children'))}
49+
</Container>
50+
);
51+
});
52+
53+
export default WikiIndexPage;

0 commit comments

Comments
 (0)