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
74 changes: 74 additions & 0 deletions components/Git/Issue/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Icon, Nameplate, text2color } from 'idea-react';
import { marked } from 'marked';
import { Issue } from 'mobx-github';
import { FC } from 'react';
import { Badge, Card, CardProps, Stack } from 'react-bootstrap';

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

export const IssueCard: FC<IssueCardProps> = ({
bg = 'light',
text = 'dark',
id,
number,
title,
labels,
body,
html_url,
user,
comments,
created_at,
...props
}) => (
<Card {...{ ...props, bg, text }}>
<Card.Header
as="h4"
className="d-flex justify-content-between align-items-center gap-3"
>
<a
className="text-decoration-none text-secondary text-truncate"
title={title}
href={html_url}
target="_blank"
rel="noreferrer"
>
#{number} {title}
</a>
<Stack direction="horizontal" gap={2}>
{labels.map(
label =>
typeof label === 'object' && (
<Badge
key={label.name}
className="fs-6"
{...(label.color
? {
bg: '',
style: { background: `#${label.color}` },
}
: {
bg: text2color(label.name || '', ['light']),
})}
>
{label.name}
</Badge>
),
)}
</Stack>
</Card.Header>
<Card.Body
as="article"
dangerouslySetInnerHTML={{ __html: marked(body || '') }}
/>
<Card.Footer className="d-flex justify-content-between align-items-center">
{user && <Nameplate name={user.name || ''} avatar={user.avatar_url} />}

<Stack direction="horizontal" gap={2}>
<Icon name="chat-left-text" />
{comments}
</Stack>

<time dateTime={created_at}>{new Date(created_at).toLocaleString()}</time>
</Card.Footer>
</Card>
);
40 changes: 40 additions & 0 deletions components/Git/Issue/IssueModule.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { text2color } from 'idea-react';
import type { GitRepository } from 'mobx-github';
import { FC } from 'react';
import { Accordion, Badge, Col, Row } from 'react-bootstrap';

import { IssueCard } from './Card';

export const IssueModule: FC<GitRepository> = ({ name, language, issues }) => (
<Accordion.Item eventKey={name}>
<Accordion.Header>
<Row className="flex-fill align-items-center gx-3">
<Col xs={4} sm={2}>
{language && (
<Badge className="fs-6" bg={text2color(language, ['light'])}>
{language}
</Badge>
)}
</Col>
<Col xs={6} sm={8} as="h3" className="m-0 text-truncate">
{name}
</Col>
<Col xs={2} className="text-end">
<Badge className="fs-6" pill bg="info">
{issues?.length}
</Badge>
</Col>
</Row>
</Accordion.Header>

<Accordion.Body>
<Row xs={1} sm={2} xl={2} className="g-3">
{issues?.map(issue => (
<Col key={issue.title}>
<IssueCard className="h-100" {...issue} />
</Col>
))}
</Row>
</Accordion.Body>
</Accordion.Item>
);
1 change: 1 addition & 0 deletions components/Navigator/MainNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
href: '/article/open-collaborator-award',
name: t('open_collaborator_award'),
},
{ href: '/issue', name: 'GitHub issues' },
{
href: 'https://github.com/Open-Source-Bazaar/Git-Hackathon-scaffold',
name: t('hackathon'),
Expand Down
46 changes: 45 additions & 1 deletion models/Base.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,53 @@
import 'core-js/full/array/from-async';

import { HTTPClient } from 'koajax';
import { githubClient } from 'mobx-github';
import { TableCellAttachment, TableCellMedia, TableCellValue } from 'mobx-lark';
import { DataObject } from 'mobx-restful';
import { isEmpty } from 'web-utility';

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

export const ownClient = new HTTPClient({
baseURI: `${API_Host}/api/`,
responseType: 'json',
});

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 githubRawClient = new HTTPClient({
baseURI: `${ProxyBaseURL}/raw.githubusercontent.com/`,
responseType: 'arraybuffer',
});

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

export const makeGithubSearchCondition = (queryMap: DataObject) =>
Object.entries(queryMap)
.filter(([, value]) => !isEmpty(value))
.map(([key, value]) => `${key}:${value}`)
.join(' ');

export const larkClient = new HTTPClient({
baseURI: LARK_API_HOST,
Expand Down
63 changes: 63 additions & 0 deletions models/Repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Repository, RepositoryModel, UserModel } from 'mobx-github';
import { Filter, ListModel, toggle } from 'mobx-restful';
import { buildURLData } from 'web-utility';

import {
githubClient,
githubRawClient,
GithubSearchData,
makeGithubSearchCondition,
} from './Base';

export class GitRepositoryModel extends RepositoryModel {
@toggle('downloading')
async downloadRaw(
path: string,
repository = this.currentOne.name,
ref = this.currentOne.default_branch,
) {
const owner = this.owner || (await userStore.getSession()).login;
const identity = `${owner}/${repository}`;

if (!ref) {
const { default_branch } = await this.getOne(identity);

ref = default_branch;
}
const { body } = await githubRawClient.get<ArrayBuffer>(
`${identity}/${ref}/${path}`,
);

return body!;
}
}

export const userStore = new UserModel();
export const repositoryStore = new GitRepositoryModel('Open-Source-Bazaar');

export type RepositoryFilter = Filter<Repository>;

export class RepositorySearchModel extends ListModel<
Repository,
RepositoryFilter
> {
baseURI = 'search/repositories';
client = githubClient;

async loadPage(
page = this.pageIndex,
per_page = this.pageSize,
{ full_name }: RepositoryFilter,
) {
const name = full_name?.split('/').at(-1);

const queryMap = { in: name ? 'name' : undefined },
keyword = name;
const condition = makeGithubSearchCondition(queryMap);

const { body } = await this.client.get<GithubSearchData<Repository>>(
`${this.baseURI}?${buildURLData({ page, per_page, q: `${condition} ${keyword}` })}`,
);
return { pageData: body!.items, totalCount: body!.total_count };
}
}
9 changes: 8 additions & 1 deletion models/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { parseCookie } from 'mobx-i18n';

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

export const Name = process.env.NEXT_PUBLIC_SITE_NAME,
Summary = process.env.NEXT_PUBLIC_SITE_SUMMARY,
DefaultImage = process.env.NEXT_PUBLIC_LOGO!;

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

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

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

export const ProxyBaseURL = 'https://bazaar.fcc-cd.dev/proxy';

export const GithubToken =
(globalThis.document && parseCookie().token) || process.env.GITHUB_TOKEN;

export const LarkAppMeta = {
host: process.env.NEXT_PUBLIC_LARK_API_HOST,
id: process.env.NEXT_PUBLIC_LARK_APP_ID!,
Expand Down
20 changes: 20 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import setMDX from '@next/mdx';
import { NextConfig } from 'next';
import setPWA from 'next-pwa';
// @ts-expect-error no official types
import withLess from 'next-with-less';
Expand All @@ -21,11 +22,30 @@ const withMDX = setMDX({
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*',
},
{
source: '/proxy/geo.datav.aliyun.com/:path*',
destination: 'https://geo.datav.aliyun.com/:path*',
},
],
afterFiles: [],
});

export default withPWA(
withLess(
withMDX({
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
output: CI ? 'standalone' : undefined,
rewrites,
}),
),
);
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
"marked": "^16.0.0",
"mime": "^4.0.7",
"mobx": "^6.13.7",
"mobx-github": "^0.3.11",
"mobx-i18n": "^0.7.1",
"mobx-lark": "^2.2.0",
"mobx-react": "^9.2.0",
"mobx-restful": "^2.1.0",
"mobx-restful-table": "^2.5.2",
"next": "^15.3.5",
"next-pwa": "^5.6.0",
"next-ssr-middleware": "^1.0.1",
Expand Down
12 changes: 12 additions & 0 deletions pages/api/GitHub/[...slug].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createKoaRouter, withKoaRouter } from 'next-ssr-middleware';

import { safeAPI } from '../core';
import { proxyGitHubAll } from './core';

export const config = { api: { bodyParser: false } };

const router = createKoaRouter(import.meta.url);

router.get('/(.*)', safeAPI, proxyGitHubAll);

export default withKoaRouter(router);
35 changes: 35 additions & 0 deletions pages/api/GitHub/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Context, Middleware } from 'koa';
import { githubOAuth2 } from 'next-ssr-middleware';

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

export const proxyGithub = async <T>({
method,
url,
headers: { host, ...headers },
request,
}: Context) => {
const path = url!.slice(`/api/GitHub/`.length),
body = Reflect.get(request, 'body');

// @ts-expect-error KoAJAX type compatibility
return githubClient.request<T>({ method, path, headers, body });
};

export const proxyGitHubAll: Middleware = async context => {
const { status, body } = await proxyGithub(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'],
});
Loading