Skip to content

Commit d004bd4

Browse files
committed
[add] API, components & page of GitHub issue forked from https://github.com/kaiyuanshe/OSS-toolbox
1 parent 30a842b commit d004bd4

16 files changed

Lines changed: 537 additions & 2 deletions

File tree

components/Git/Issue/Card.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Icon, Nameplate, text2color } from 'idea-react';
2+
import { marked } from 'marked';
3+
import { Issue } from 'mobx-github';
4+
import { FC } from 'react';
5+
import { Badge, Card, CardProps, Stack } from 'react-bootstrap';
6+
7+
export type IssueCardProps = Issue & Omit<CardProps, 'id' | 'body'>;
8+
9+
export const IssueCard: FC<IssueCardProps> = ({
10+
bg = 'light',
11+
text = 'dark',
12+
id,
13+
number,
14+
title,
15+
labels,
16+
body,
17+
html_url,
18+
user,
19+
comments,
20+
created_at,
21+
...props
22+
}) => (
23+
<Card {...{ ...props, bg, text }}>
24+
<Card.Header
25+
as="h4"
26+
className="d-flex justify-content-between align-items-center gap-3"
27+
>
28+
<a
29+
className="text-decoration-none text-secondary text-truncate"
30+
title={title}
31+
href={html_url}
32+
target="_blank"
33+
rel="noreferrer"
34+
>
35+
#{number} {title}
36+
</a>
37+
<Stack direction="horizontal" gap={2}>
38+
{labels.map(
39+
label =>
40+
typeof label === 'object' && (
41+
<Badge
42+
key={label.name}
43+
className="fs-6"
44+
{...(label.color
45+
? {
46+
bg: '',
47+
style: { background: `#${label.color}` },
48+
}
49+
: {
50+
bg: text2color(label.name || '', ['light']),
51+
})}
52+
>
53+
{label.name}
54+
</Badge>
55+
),
56+
)}
57+
</Stack>
58+
</Card.Header>
59+
<Card.Body
60+
as="article"
61+
dangerouslySetInnerHTML={{ __html: marked(body || '') }}
62+
/>
63+
<Card.Footer className="d-flex justify-content-between align-items-center">
64+
{user && <Nameplate name={user.name || ''} avatar={user.avatar_url} />}
65+
66+
<Stack direction="horizontal" gap={2}>
67+
<Icon name="chat-left-text" />
68+
{comments}
69+
</Stack>
70+
71+
<time dateTime={created_at}>{new Date(created_at).toLocaleString()}</time>
72+
</Card.Footer>
73+
</Card>
74+
);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { text2color } from 'idea-react';
2+
import type { GitRepository } from 'mobx-github';
3+
import { FC } from 'react';
4+
import { Accordion, Badge, Col, Row } from 'react-bootstrap';
5+
6+
import { IssueCard } from './Card';
7+
8+
export const IssueModule: FC<GitRepository> = ({ name, language, issues }) => (
9+
<Accordion.Item eventKey={name}>
10+
<Accordion.Header>
11+
<Row className="flex-fill align-items-center gx-3">
12+
<Col xs={4} sm={2}>
13+
{language && (
14+
<Badge className="fs-6" bg={text2color(language, ['light'])}>
15+
{language}
16+
</Badge>
17+
)}
18+
</Col>
19+
<Col xs={6} sm={8} as="h3" className="m-0 text-truncate">
20+
{name}
21+
</Col>
22+
<Col xs={2} className="text-end">
23+
<Badge className="fs-6" pill bg="info">
24+
{issues?.length}
25+
</Badge>
26+
</Col>
27+
</Row>
28+
</Accordion.Header>
29+
30+
<Accordion.Body>
31+
<Row xs={1} sm={2} xl={2} className="g-3">
32+
{issues?.map(issue => (
33+
<Col key={issue.title}>
34+
<IssueCard className="h-100" {...issue} />
35+
</Col>
36+
))}
37+
</Row>
38+
</Accordion.Body>
39+
</Accordion.Item>
40+
);

components/Navigator/MainNavigator.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
1919
href: '/article/open-collaborator-award',
2020
name: t('open_collaborator_award'),
2121
},
22+
{ href: '/issue', name: 'GitHub issues' },
2223
{
2324
href: 'https://github.com/Open-Source-Bazaar/Git-Hackathon-scaffold',
2425
name: t('hackathon'),

models/Base.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,53 @@
11
import 'core-js/full/array/from-async';
22

33
import { HTTPClient } from 'koajax';
4+
import { githubClient } from 'mobx-github';
45
import { TableCellAttachment, TableCellMedia, TableCellValue } from 'mobx-lark';
6+
import { DataObject } from 'mobx-restful';
7+
import { isEmpty } from 'web-utility';
58

6-
import { LARK_API_HOST } from './configuration';
9+
import {
10+
API_Host,
11+
GithubToken,
12+
isServer,
13+
ProxyBaseURL,
14+
LARK_API_HOST,
15+
} from './configuration';
16+
17+
export const ownClient = new HTTPClient({
18+
baseURI: `${API_Host}/api/`,
19+
responseType: 'json',
20+
});
21+
22+
if (!isServer()) githubClient.baseURI = `${API_Host}/api/GitHub/`;
23+
24+
githubClient.use(({ request }, next) => {
25+
if (GithubToken)
26+
request.headers = {
27+
authorization: `Bearer ${GithubToken}`,
28+
...request.headers,
29+
};
30+
return next();
31+
});
32+
33+
export { githubClient };
34+
35+
export const githubRawClient = new HTTPClient({
36+
baseURI: `${ProxyBaseURL}/raw.githubusercontent.com/`,
37+
responseType: 'arraybuffer',
38+
});
39+
40+
export interface GithubSearchData<T> {
41+
total_count: number;
42+
incomplete_results: boolean;
43+
items: T[];
44+
}
45+
46+
export const makeGithubSearchCondition = (queryMap: DataObject) =>
47+
Object.entries(queryMap)
48+
.filter(([, value]) => !isEmpty(value))
49+
.map(([key, value]) => `${key}:${value}`)
50+
.join(' ');
751

852
export const larkClient = new HTTPClient({
953
baseURI: LARK_API_HOST,

models/Repository.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Repository, RepositoryModel, UserModel } from 'mobx-github';
2+
import { Filter, ListModel, toggle } from 'mobx-restful';
3+
import { buildURLData } from 'web-utility';
4+
5+
import {
6+
githubClient,
7+
githubRawClient,
8+
GithubSearchData,
9+
makeGithubSearchCondition,
10+
} from './Base';
11+
12+
export class GitRepositoryModel extends RepositoryModel {
13+
@toggle('downloading')
14+
async downloadRaw(
15+
path: string,
16+
repository = this.currentOne.name,
17+
ref = this.currentOne.default_branch,
18+
) {
19+
const owner = this.owner || (await userStore.getSession()).login;
20+
const identity = `${owner}/${repository}`;
21+
22+
if (!ref) {
23+
const { default_branch } = await this.getOne(identity);
24+
25+
ref = default_branch;
26+
}
27+
const { body } = await githubRawClient.get<ArrayBuffer>(
28+
`${identity}/${ref}/${path}`,
29+
);
30+
31+
return body!;
32+
}
33+
}
34+
35+
export const userStore = new UserModel();
36+
export const repositoryStore = new GitRepositoryModel('Open-Source-Bazaar');
37+
38+
export type RepositoryFilter = Filter<Repository>;
39+
40+
export class RepositorySearchModel extends ListModel<
41+
Repository,
42+
RepositoryFilter
43+
> {
44+
baseURI = 'search/repositories';
45+
client = githubClient;
46+
47+
async loadPage(
48+
page = this.pageIndex,
49+
per_page = this.pageSize,
50+
{ full_name }: RepositoryFilter,
51+
) {
52+
const name = full_name?.split('/').at(-1);
53+
54+
const queryMap = { in: name ? 'name' : undefined },
55+
keyword = name;
56+
const condition = makeGithubSearchCondition(queryMap);
57+
58+
const { body } = await this.client.get<GithubSearchData<Repository>>(
59+
`${this.baseURI}?${buildURLData({ page, per_page, q: `${condition} ${keyword}` })}`,
60+
);
61+
return { pageData: body!.items, totalCount: body!.total_count };
62+
}
63+
}

models/configuration.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { parseCookie } from 'mobx-i18n';
2+
13
export const isServer = () => typeof window === 'undefined';
24

35
export const Name = process.env.NEXT_PUBLIC_SITE_NAME,
46
Summary = process.env.NEXT_PUBLIC_SITE_SUMMARY,
57
DefaultImage = process.env.NEXT_PUBLIC_LOGO!;
68

7-
export const { VERCEL_URL } = process.env;
9+
export const { VERCEL, VERCEL_URL } = process.env;
810

911
export const API_Host = isServer()
1012
? VERCEL_URL
@@ -16,6 +18,11 @@ export const CACHE_HOST = process.env.NEXT_PUBLIC_CACHE_HOST!;
1618

1719
export const LARK_API_HOST = `${API_Host}/api/Lark/`;
1820

21+
export const ProxyBaseURL = 'https://bazaar.fcc-cd.dev/proxy';
22+
23+
export const GithubToken =
24+
(globalThis.document && parseCookie().token) || process.env.GITHUB_TOKEN;
25+
1926
export const LarkAppMeta = {
2027
host: process.env.NEXT_PUBLIC_LARK_API_HOST,
2128
id: process.env.NEXT_PUBLIC_LARK_APP_ID!,

next.config.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import setMDX from '@next/mdx';
2+
import { NextConfig } from 'next';
23
import setPWA from 'next-pwa';
34
// @ts-expect-error no official types
45
import withLess from 'next-with-less';
@@ -21,11 +22,30 @@ const withMDX = setMDX({
2122
disable: isDev,
2223
});
2324

25+
const rewrites: NextConfig['rewrites'] = async () => ({
26+
beforeFiles: [
27+
{
28+
source: '/proxy/github.com/:path*',
29+
destination: 'https://github.com/:path*',
30+
},
31+
{
32+
source: '/proxy/raw.githubusercontent.com/:path*',
33+
destination: 'https://raw.githubusercontent.com/:path*',
34+
},
35+
{
36+
source: '/proxy/geo.datav.aliyun.com/:path*',
37+
destination: 'https://geo.datav.aliyun.com/:path*',
38+
},
39+
],
40+
afterFiles: [],
41+
});
42+
2443
export default withPWA(
2544
withLess(
2645
withMDX({
2746
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
2847
output: CI ? 'standalone' : undefined,
48+
rewrites,
2949
}),
3050
),
3151
);

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
"marked": "^16.0.0",
2525
"mime": "^4.0.7",
2626
"mobx": "^6.13.7",
27+
"mobx-github": "^0.3.11",
2728
"mobx-i18n": "^0.7.1",
2829
"mobx-lark": "^2.2.0",
2930
"mobx-react": "^9.2.0",
3031
"mobx-restful": "^2.1.0",
32+
"mobx-restful-table": "^2.5.2",
3133
"next": "^15.3.5",
3234
"next-pwa": "^5.6.0",
3335
"next-ssr-middleware": "^1.0.1",

pages/api/GitHub/[...slug].ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { createKoaRouter, withKoaRouter } from 'next-ssr-middleware';
2+
3+
import { safeAPI } from '../core';
4+
import { proxyGitHubAll } from './core';
5+
6+
export const config = { api: { bodyParser: false } };
7+
8+
const router = createKoaRouter(import.meta.url);
9+
10+
router.get('/(.*)', safeAPI, proxyGitHubAll);
11+
12+
export default withKoaRouter(router);

pages/api/GitHub/core.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Context, Middleware } from 'koa';
2+
import { githubOAuth2 } from 'next-ssr-middleware';
3+
4+
import { githubClient } from '../../../models/Base';
5+
import { ProxyBaseURL, VERCEL } from '../../../models/configuration';
6+
7+
export const proxyGithub = async <T>({
8+
method,
9+
url,
10+
headers: { host, ...headers },
11+
request,
12+
}: Context) => {
13+
const path = url!.slice(`/api/GitHub/`.length),
14+
body = Reflect.get(request, 'body');
15+
16+
// @ts-expect-error KoAJAX type compatibility
17+
return githubClient.request<T>({ method, path, headers, body });
18+
};
19+
20+
export const proxyGitHubAll: Middleware = async context => {
21+
const { status, body } = await proxyGithub(context);
22+
23+
context.status = status;
24+
context.body = body;
25+
};
26+
27+
const client_id = process.env.GITHUB_OAUTH_CLIENT_ID!,
28+
client_secret = process.env.GITHUB_OAUTH_CLIENT_SECRET!;
29+
30+
export const githubOAuth = githubOAuth2({
31+
rootBaseURL: VERCEL ? undefined : `${ProxyBaseURL}/github.com/`,
32+
client_id,
33+
client_secret,
34+
scopes: ['user', 'repo'],
35+
});

0 commit comments

Comments
 (0)