Skip to content

Commit 3bf3e0d

Browse files
authored
[add] GitHub-reward issue model, card & page (#78)
1 parent 8f92d0d commit 3bf3e0d

File tree

14 files changed

+270
-144
lines changed

14 files changed

+270
-144
lines changed

components/Git/Issue/Card.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Avatar, Card, CardContent, CardProps, Chip } from '@mui/material';
2+
import { marked } from 'marked';
3+
import { Issue } from 'mobx-github';
4+
import { FC } from 'react';
5+
6+
import { SymbolIcon } from '../../Icon';
7+
8+
export type IssueCardProps = Issue & Omit<CardProps, 'id'>;
9+
10+
export const IssueCard: FC<IssueCardProps> = ({
11+
id,
12+
repository_url,
13+
number,
14+
title,
15+
labels,
16+
body,
17+
html_url,
18+
user,
19+
comments,
20+
created_at,
21+
...props
22+
}) => (
23+
<Card {...props}>
24+
<CardContent className="flex h-full flex-col justify-between gap-2">
25+
<h2 className="text-2xl">
26+
<a
27+
href={html_url}
28+
target="_blank"
29+
rel="noreferrer"
30+
style={{ textDecoration: 'none', color: 'inherit' }}
31+
>
32+
{repository_url.split('/').slice(-2).join('/')}#{number}
33+
<br />
34+
{title}
35+
</a>
36+
</h2>
37+
38+
<div className="flex items-center gap-2">
39+
{labels?.map(
40+
label =>
41+
typeof label === 'object' && (
42+
<Chip
43+
key={label.name}
44+
label={label.name}
45+
style={{ backgroundColor: `#${label.color || 'e0e0e0'}` }}
46+
/>
47+
),
48+
)}
49+
</div>
50+
51+
<article dangerouslySetInnerHTML={{ __html: marked(body || '') }} />
52+
53+
<footer className="flex items-center justify-between">
54+
{user && (
55+
<div className="flex items-center gap-2">
56+
<Avatar src={user.avatar_url} alt={user.name || ''} />
57+
{user.name || ''}
58+
</div>
59+
)}
60+
<div className="flex items-center gap-2">
61+
<SymbolIcon name="chat" />
62+
{comments}
63+
</div>
64+
<time dateTime={created_at}>{new Date(created_at).toLocaleString()}</time>
65+
</footer>
66+
</CardContent>
67+
</Card>
68+
);

components/Icon.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ export interface IconProps extends HTMLAttributes<HTMLSpanElement> {
55
variant?: 'outlined' | 'rounded' | 'sharp';
66
}
77

8-
export const SymbolIcon: FC<IconProps> = ({ className, name, variant = 'outlined', ...props }) => (
8+
export const SymbolIcon: FC<IconProps> = ({
9+
className = '',
10+
name,
11+
variant = 'outlined',
12+
...props
13+
}) => (
914
<span
1015
aria-hidden="false"
1116
aria-label={`${name} icon`}

components/Layout/MainNavigator.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
1-
import {
2-
AppBar,
3-
Button,
4-
Drawer,
5-
IconButton,
6-
Menu,
7-
MenuItem,
8-
PopoverProps,
9-
Toolbar,
10-
} from '@mui/material';
1+
import { AppBar, Drawer, IconButton, Menu, MenuItem, PopoverProps, Toolbar } from '@mui/material';
112
import { observable } from 'mobx';
123
import { observer } from 'mobx-react';
13-
import Image from 'next/image';
144
import Link from 'next/link';
155
import { Component } from 'react';
166

@@ -21,6 +11,7 @@ import { BrandLogo, GithubIcon } from './Svg';
2111

2212
export const mainNavLinks = () => [
2313
{ title: t('latest_projects'), href: '/project' },
14+
{ title: 'GitHub-reward', href: '/project/reward/issue', target: '_top' },
2415
{ title: t('member'), href: '/member' },
2516
{ title: t('open_source_project'), href: '/open-source' },
2617
];
@@ -36,8 +27,8 @@ export class MainNavigator extends Component {
3627
};
3728

3829
renderLinks = () =>
39-
mainNavLinks().map(({ title, href }) => (
40-
<Link key={title} className="py-1" href={href}>
30+
mainNavLinks().map(({ title, href, target }) => (
31+
<Link key={title} className="py-1" href={href} target={target}>
4132
{title}
4233
</Link>
4334
));

components/Member/Card.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { CardProps, Chip } from '@mui/material';
22
import { marked } from 'marked';
33
import { observer } from 'mobx-react';
4-
import Image from 'next/image';
54
import Link from 'next/link';
65
import { FC } from 'react';
76

@@ -29,15 +28,13 @@ export const MemberCard: FC<MemberCardProps> = observer(
2928

3029
<div className="flex w-auto items-center gap-4">
3130
{github && (
32-
<Image
33-
width={64}
34-
height={64}
31+
<img
32+
style={{ width: '4rem', height: '4rem' }}
3533
className="rounded-full object-cover"
3634
src={`https://github.com/${String(github)}.png`}
3735
alt={String(github)}
3836
/>
3937
)}
40-
4138
<Link href={`/member/${String(nickname)}`} aria-label={String(nickname)}>
4239
<h2 className="text-base">{String(nickname)}</h2>
4340
<p className="text-sm">{String(position ?? '')}</p>

models/Base.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
import { HTTPClient } from 'koajax';
22
import MIME from 'mime';
3+
import { githubClient } from 'mobx-github';
34
import { TableCellValue, TableCellMedia, TableCellAttachment } from 'mobx-lark';
45

5-
import { API_Host } from './configuration';
6+
import { API_Host, GithubToken, isServer } from './configuration';
7+
8+
if (!isServer()) githubClient.baseURI = `${API_Host}/api/GitHub/`;
9+
10+
githubClient.use(({ request }, next) => {
11+
if (GithubToken)
12+
request.headers = {
13+
Authorization: `Bearer ${GithubToken}`,
14+
...request.headers,
15+
};
16+
return next();
17+
});
18+
19+
export { githubClient };
620

721
export const larkClient = new HTTPClient({
822
baseURI: `${API_Host}/api/Lark/`,

models/Issue.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { isEmpty } from 'lodash';
2+
import { Issue } from 'mobx-github';
3+
import { Filter, ListModel } from 'mobx-restful';
4+
import { buildURLData } from 'web-utility';
5+
6+
import { githubClient } from './Base';
7+
8+
interface SearchData<T> {
9+
total_count: number;
10+
incomplete_results: boolean;
11+
items: T[];
12+
}
13+
14+
export type IssueFilter = Filter<Issue>;
15+
16+
export class IssueModel extends ListModel<Issue, IssueFilter> {
17+
baseURI = 'search/issues';
18+
client = githubClient;
19+
20+
async loadPage(
21+
page = this.pageIndex,
22+
per_page = this.pageSize,
23+
{ repository_url, state, title }: IssueFilter,
24+
) {
25+
const [org, repo] = repository_url?.replace('https://github.com/', '').split('/') || [];
26+
27+
const condition = Object.entries({ org, repo, type: 'issue', state })
28+
.filter(([, value]) => !isEmpty(value))
29+
.map(([key, value]) => `${key}:${value}`)
30+
.join(' ');
31+
32+
const { body } = await this.client.get<SearchData<Issue>>(
33+
`${this.baseURI}?${buildURLData({ page, per_page, q: `${condition} ${title}` })}`,
34+
);
35+
return { pageData: body!.items, totalCount: body!.total_count };
36+
}
37+
}
38+
39+
export default new IssueModel();

models/Repository.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,8 @@
11
import { observable } from 'mobx';
2-
import { githubClient, GitRepository, RepositoryFilter, RepositoryModel } from 'mobx-github';
2+
import { GitRepository, RepositoryFilter, RepositoryModel } from 'mobx-github';
33
import { Stream } from 'mobx-restful';
44

5-
import { API_Host, GithubToken, isServer } from './configuration';
6-
7-
if (!isServer()) githubClient.baseURI = `${API_Host}/api/GitHub/`;
8-
9-
githubClient.use(({ request }, next) => {
10-
if (GithubToken)
11-
request.headers = {
12-
authorization: `Bearer ${GithubToken}`,
13-
...request.headers,
14-
};
15-
return next();
16-
});
5+
import { githubClient } from './Base';
176

187
export class GitRepositoryModel extends Stream<GitRepository, RepositoryFilter>(RepositoryModel) {
198
client = githubClient;

models/configuration.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { parseCookie } from 'mobx-i18n';
2-
31
export const Name = process.env.NEXT_PUBLIC_SITE_NAME,
42
Summary = process.env.NEXT_PUBLIC_SITE_SUMMARY,
53
DefaultImage = process.env.NEXT_PUBLIC_LOGO || '/og.png';
64

75
export const isServer = () => typeof window === 'undefined';
86

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

1110
export const API_Host = isServer()
1211
? VercelHost
@@ -18,8 +17,9 @@ export const CACHE_HOST = process.env.NEXT_PUBLIC_CACHE_HOST!;
1817

1918
export const { CRAWLER_TOKEN, JWT_SECRET } = process.env;
2019

21-
export const GithubToken =
22-
parseCookie(globalThis.document?.cookie || '').token || process.env.GITHUB_TOKEN;
20+
export const ProxyBaseURL = `https://idea2.app/proxy`;
21+
22+
export const GithubToken = process.env.GITHUB_TOKEN;
2323

2424
export const LARK_API_HOST = `${API_Host}/api/Lark/`;
2525

next.config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { withSentryConfig } from '@sentry/nextjs';
2+
import { NextConfig } from 'next';
23
import setPWA from 'next-pwa';
34
import webpack from 'webpack';
45

@@ -12,6 +13,20 @@ const withPWA = setPWA({
1213
disable: isDev,
1314
});
1415

16+
const rewrites: NextConfig['rewrites'] = async () => ({
17+
beforeFiles: [
18+
{
19+
source: '/proxy/github.com/:path*',
20+
destination: 'https://github.com/:path*',
21+
},
22+
{
23+
source: '/proxy/raw.githubusercontent.com/:path*',
24+
destination: 'https://raw.githubusercontent.com/:path*',
25+
},
26+
],
27+
afterFiles: [],
28+
});
29+
1530
const nextConfig = withPWA({
1631
output: CI ? 'standalone' : undefined,
1732
compiler: {
@@ -30,6 +45,7 @@ const nextConfig = withPWA({
3045
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
3146
return config;
3247
},
48+
rewrites,
3349
});
3450

3551
export default isDev || !SENTRY_AUTH_TOKEN

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
"@giscus/react": "^3.1.0",
1313
"@koa/bodyparser": "^5.1.1",
1414
"@koa/router": "^13.1.0",
15-
"@mui/lab": "6.0.0-beta.31",
16-
"@mui/material": "^6.4.11",
15+
"@mui/lab": "^7.0.0-beta.11",
16+
"@mui/material": "^7.0.2",
1717
"@sentry/nextjs": "^9.15.0",
1818
"file-type": "^20.5.0",
1919
"jsonwebtoken": "^9.0.2",

pages/_document.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default function Document() {
4040
* */}
4141
<link
4242
rel="stylesheet"
43-
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=code,dark_mode,diversity_3,keyboard_arrow_down,language,light_mode,menu,translate,trending_up&display=swap"
43+
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=swap"
4444
/>
4545
<script type="application/ld+json">{JSON.stringify(siteNameJsonLd)}</script>
4646
</Head>

pages/api/GitHub/core.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { Context, Middleware } from 'koa';
22
import { githubClient } from 'mobx-github';
3+
import { githubOAuth2 } from 'next-ssr-middleware';
4+
5+
import { ProxyBaseURL, VERCEL } from '../../../models/configuration';
36

47
export const proxyGithub = async <T>({
58
method,
@@ -20,3 +23,13 @@ export const proxyGitHubAll: Middleware = async context => {
2023
context.status = status;
2124
context.body = body;
2225
};
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+
});

pages/project/reward/issue.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Grid } from '@mui/material';
2+
import { Issue } from 'mobx-github';
3+
import { observer } from 'mobx-react';
4+
import { cache, compose, errorLogger, translator } from 'next-ssr-middleware';
5+
import { FC } from 'react';
6+
7+
import { IssueCard } from '../../../components/Git/Issue/Card';
8+
import { PageHead } from '../../../components/PageHead';
9+
import { ScrollList } from '../../../components/ScrollList';
10+
import issueStore, { IssueFilter, IssueModel } from '../../../models/Issue';
11+
import { i18n } from '../../../models/Translation';
12+
import { githubOAuth } from '../../api/GitHub/core';
13+
14+
const issueFilter: IssueFilter = {
15+
repository_url: 'https://github.com/idea2app',
16+
state: 'open',
17+
title: 'reward',
18+
};
19+
20+
export const getServerSideProps = compose(githubOAuth, errorLogger, translator(i18n), async () => {
21+
const list = await new IssueModel().getList(issueFilter);
22+
23+
return { props: JSON.parse(JSON.stringify({ list })) };
24+
});
25+
26+
const IssuesPage: FC<{ list: Issue[] }> = observer(({ list }) => (
27+
<Grid container className="px-4 py-20">
28+
<PageHead title="GitHub-reward issues" />
29+
30+
<h1>GitHub-reward issues</h1>
31+
32+
<ScrollList
33+
translator={i18n}
34+
store={issueStore}
35+
filter={issueFilter}
36+
defaultData={list}
37+
renderList={allItems => (
38+
<Grid container spacing={2}>
39+
{allItems.map(issue => (
40+
<Grid key={issue.id} size={{ xs: 12, sm: 6, md: 4 }}>
41+
<IssueCard className="h-full" {...issue} />
42+
</Grid>
43+
))}
44+
</Grid>
45+
)}
46+
/>
47+
</Grid>
48+
));
49+
50+
export default IssuesPage;

0 commit comments

Comments
 (0)