Skip to content

[add] GitHub-reward issue model, card & page #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 4, 2025
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
68 changes: 68 additions & 0 deletions components/Git/Issue/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Avatar, Card, CardContent, CardProps, Chip } from '@mui/material';
import { marked } from 'marked';
import { Issue } from 'mobx-github';
import { FC } from 'react';

import { SymbolIcon } from '../../Icon';

export type IssueCardProps = Issue & Omit<CardProps, 'id'>;

export const IssueCard: FC<IssueCardProps> = ({
id,
repository_url,
number,
title,
labels,
body,
html_url,
user,
comments,
created_at,
...props
}) => (
<Card {...props}>
<CardContent className="flex h-full flex-col justify-between gap-2">
<h2 className="text-2xl">
<a
href={html_url}
target="_blank"
rel="noreferrer"
style={{ textDecoration: 'none', color: 'inherit' }}
>
{repository_url.split('/').slice(-2).join('/')}#{number}
<br />
{title}
</a>
</h2>

<div className="flex items-center gap-2">
{labels?.map(
label =>
typeof label === 'object' && (
<Chip
key={label.name}
label={label.name}
style={{ backgroundColor: `#${label.color || 'e0e0e0'}` }}
/>
),
)}
</div>

<article dangerouslySetInnerHTML={{ __html: marked(body || '') }} />

<footer className="flex items-center justify-between">
{user && (
<div className="flex items-center gap-2">
<Avatar src={user.avatar_url} alt={user.name || ''} />
{user.name || ''}
</div>
)}
<div className="flex items-center gap-2">
<SymbolIcon name="chat" />
{comments}
</div>
<time dateTime={created_at}>{new Date(created_at).toLocaleString()}</time>
</footer>
</CardContent>
</Card>
);
7 changes: 6 additions & 1 deletion components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ export interface IconProps extends HTMLAttributes<HTMLSpanElement> {
variant?: 'outlined' | 'rounded' | 'sharp';
}

export const SymbolIcon: FC<IconProps> = ({ className, name, variant = 'outlined', ...props }) => (
export const SymbolIcon: FC<IconProps> = ({
className = '',
name,
variant = 'outlined',
...props
}) => (
<span
aria-hidden="false"
aria-label={`${name} icon`}
Expand Down
17 changes: 4 additions & 13 deletions components/Layout/MainNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import {
AppBar,
Button,
Drawer,
IconButton,
Menu,
MenuItem,
PopoverProps,
Toolbar,
} from '@mui/material';
import { AppBar, Drawer, IconButton, Menu, MenuItem, PopoverProps, Toolbar } from '@mui/material';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import Image from 'next/image';
import Link from 'next/link';
import { Component } from 'react';

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

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

renderLinks = () =>
mainNavLinks().map(({ title, href }) => (
<Link key={title} className="py-1" href={href}>
mainNavLinks().map(({ title, href, target }) => (
<Link key={title} className="py-1" href={href} target={target}>
{title}
</Link>
));
Expand Down
7 changes: 2 additions & 5 deletions components/Member/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { CardProps, Chip } from '@mui/material';
import { marked } from 'marked';
import { observer } from 'mobx-react';
import Image from 'next/image';
import Link from 'next/link';
import { FC } from 'react';

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

<div className="flex w-auto items-center gap-4">
{github && (
<Image
width={64}
height={64}
<img
style={{ width: '4rem', height: '4rem' }}
className="rounded-full object-cover"
src={`https://github.com/${String(github)}.png`}
alt={String(github)}
/>
)}

<Link href={`/member/${String(nickname)}`} aria-label={String(nickname)}>
<h2 className="text-base">{String(nickname)}</h2>
<p className="text-sm">{String(position ?? '')}</p>
Expand Down
16 changes: 15 additions & 1 deletion models/Base.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { HTTPClient } from 'koajax';
import MIME from 'mime';
import { githubClient } from 'mobx-github';
import { TableCellValue, TableCellMedia, TableCellAttachment } from 'mobx-lark';

import { API_Host } from './configuration';
import { API_Host, GithubToken, isServer } from './configuration';

if (!isServer()) githubClient.baseURI = `${API_Host}/api/GitHub/`;

githubClient.use(({ request }, next) => {
if (GithubToken)
request.headers = {
Authorization: `Bearer ${GithubToken}`,
...request.headers,
};
return next();
});

export { githubClient };

export const larkClient = new HTTPClient({
baseURI: `${API_Host}/api/Lark/`,
Expand Down
39 changes: 39 additions & 0 deletions models/Issue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { isEmpty } from 'lodash';
import { Issue } from 'mobx-github';
import { Filter, ListModel } from 'mobx-restful';
import { buildURLData } from 'web-utility';

import { githubClient } from './Base';

interface SearchData<T> {
total_count: number;
incomplete_results: boolean;
items: T[];
}

export type IssueFilter = Filter<Issue>;

export class IssueModel extends ListModel<Issue, IssueFilter> {
baseURI = 'search/issues';
client = githubClient;

async loadPage(
page = this.pageIndex,
per_page = this.pageSize,
{ repository_url, state, title }: IssueFilter,
) {
const [org, repo] = repository_url?.replace('https://github.com/', '').split('/') || [];

const condition = Object.entries({ org, repo, type: 'issue', state })
.filter(([, value]) => !isEmpty(value))
.map(([key, value]) => `${key}:${value}`)
.join(' ');

const { body } = await this.client.get<SearchData<Issue>>(
`${this.baseURI}?${buildURLData({ page, per_page, q: `${condition} ${title}` })}`,
);
return { pageData: body!.items, totalCount: body!.total_count };
}
}

export default new IssueModel();
15 changes: 2 additions & 13 deletions models/Repository.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import { observable } from 'mobx';
import { githubClient, GitRepository, RepositoryFilter, RepositoryModel } from 'mobx-github';
import { GitRepository, RepositoryFilter, RepositoryModel } from 'mobx-github';
import { Stream } from 'mobx-restful';

import { API_Host, GithubToken, isServer } from './configuration';

if (!isServer()) githubClient.baseURI = `${API_Host}/api/GitHub/`;

githubClient.use(({ request }, next) => {
if (GithubToken)
request.headers = {
authorization: `Bearer ${GithubToken}`,
...request.headers,
};
return next();
});
import { githubClient } from './Base';

export class GitRepositoryModel extends Stream<GitRepository, RepositoryFilter>(RepositoryModel) {
client = githubClient;
Expand Down
10 changes: 5 additions & 5 deletions models/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { parseCookie } from 'mobx-i18n';

export const Name = process.env.NEXT_PUBLIC_SITE_NAME,
Summary = process.env.NEXT_PUBLIC_SITE_SUMMARY,
DefaultImage = process.env.NEXT_PUBLIC_LOGO || '/og.png';

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

export const VercelHost = process.env.VERCEL_URL;
export const { VERCEL } = process.env,
VercelHost = process.env.VERCEL_URL;

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

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

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

export const GithubToken = process.env.GITHUB_TOKEN;

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

Expand Down
16 changes: 16 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { withSentryConfig } from '@sentry/nextjs';
import { NextConfig } from 'next';
import setPWA from 'next-pwa';
import webpack from 'webpack';

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

const rewrites: NextConfig['rewrites'] = async () => ({
beforeFiles: [
{
source: '/proxy/github.com/:path*',
destination: 'https://github.com/:path*',
},
{
source: '/proxy/raw.githubusercontent.com/:path*',
destination: 'https://raw.githubusercontent.com/:path*',
},
],
afterFiles: [],
});

const nextConfig = withPWA({
output: CI ? 'standalone' : undefined,
compiler: {
Expand All @@ -30,6 +45,7 @@ const nextConfig = withPWA({
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return config;
},
rewrites,
});

export default isDev || !SENTRY_AUTH_TOKEN
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"@giscus/react": "^3.1.0",
"@koa/bodyparser": "^5.1.1",
"@koa/router": "^13.1.0",
"@mui/lab": "6.0.0-beta.31",
"@mui/material": "^6.4.11",
"@mui/lab": "^7.0.0-beta.11",
"@mui/material": "^7.0.2",
"@sentry/nextjs": "^9.15.0",
"file-type": "^20.5.0",
"jsonwebtoken": "^9.0.2",
Expand Down
2 changes: 1 addition & 1 deletion pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default function Document() {
* */}
<link
rel="stylesheet"
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"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=swap"
/>
<script type="application/ld+json">{JSON.stringify(siteNameJsonLd)}</script>
</Head>
Expand Down
13 changes: 13 additions & 0 deletions pages/api/GitHub/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Context, Middleware } from 'koa';
import { githubClient } from 'mobx-github';
import { githubOAuth2 } from 'next-ssr-middleware';

import { ProxyBaseURL, VERCEL } from '../../../models/configuration';

export const proxyGithub = async <T>({
method,
Expand All @@ -20,3 +23,13 @@ export const proxyGitHubAll: Middleware = async context => {
context.status = status;
context.body = body;
};

const client_id = process.env.GITHUB_OAUTH_CLIENT_ID!,
client_secret = process.env.GITHUB_OAUTH_CLIENT_SECRET!;

export const githubOAuth = githubOAuth2({
rootBaseURL: VERCEL ? undefined : `${ProxyBaseURL}/github.com/`,
client_id,
client_secret,
scopes: ['user', 'repo'],
});
50 changes: 50 additions & 0 deletions pages/project/reward/issue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Grid } from '@mui/material';
import { Issue } from 'mobx-github';
import { observer } from 'mobx-react';
import { cache, compose, errorLogger, translator } from 'next-ssr-middleware';
import { FC } from 'react';

import { IssueCard } from '../../../components/Git/Issue/Card';
import { PageHead } from '../../../components/PageHead';
import { ScrollList } from '../../../components/ScrollList';
import issueStore, { IssueFilter, IssueModel } from '../../../models/Issue';
import { i18n } from '../../../models/Translation';
import { githubOAuth } from '../../api/GitHub/core';

const issueFilter: IssueFilter = {
repository_url: 'https://github.com/idea2app',
state: 'open',
title: 'reward',
};

export const getServerSideProps = compose(githubOAuth, errorLogger, translator(i18n), async () => {
const list = await new IssueModel().getList(issueFilter);

return { props: JSON.parse(JSON.stringify({ list })) };
});

const IssuesPage: FC<{ list: Issue[] }> = observer(({ list }) => (
<Grid container className="px-4 py-20">
<PageHead title="GitHub-reward issues" />

<h1>GitHub-reward issues</h1>

<ScrollList
translator={i18n}
store={issueStore}
filter={issueFilter}
defaultData={list}
renderList={allItems => (
<Grid container spacing={2}>
{allItems.map(issue => (
<Grid key={issue.id} size={{ xs: 12, sm: 6, md: 4 }}>
<IssueCard className="h-full" {...issue} />
</Grid>
))}
</Grid>
)}
/>
</Grid>
));

export default IssuesPage;
Loading