Skip to content

Commit 18e9c84

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 08d2e72 + 82d3019 commit 18e9c84

20 files changed

Lines changed: 1511 additions & 792 deletions

components/Git/Card.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { text2color } from 'idea-react';
2+
import { GitRepository } from 'mobx-github';
3+
import { observer } from 'mobx-react';
4+
import { FC, useContext } from 'react';
5+
import { Badge, Button, Card, Col, Row } from 'react-bootstrap';
6+
7+
import { I18nContext } from '../../models/Translation';
8+
import { GitLogo } from './Logo';
9+
10+
export interface GitCardProps
11+
extends Pick<GitRepository, 'full_name' | 'html_url' | 'languages'>,
12+
Partial<Pick<GitRepository, 'topics' | 'description' | 'homepage'>> {
13+
className?: string;
14+
}
15+
16+
export const GitCard: FC<GitCardProps> = observer(
17+
({
18+
className = 'shadow-sm',
19+
full_name,
20+
html_url,
21+
languages = [],
22+
topics = [],
23+
description,
24+
homepage,
25+
}) => {
26+
const { t } = useContext(I18nContext);
27+
28+
return (
29+
<Card className={className}>
30+
<Card.Body className="d-flex flex-column gap-3">
31+
<Card.Title as="h3" className="h5">
32+
<a target="_blank" href={html_url} rel="noreferrer">
33+
{full_name}
34+
</a>
35+
</Card.Title>
36+
37+
<nav className="flex-fill">
38+
{topics.map(topic => (
39+
<Badge
40+
key={topic}
41+
className="me-1"
42+
bg={text2color(topic, ['light'])}
43+
as="a"
44+
target="_blank"
45+
href={`https://github.com/topics/${topic}`}
46+
>
47+
{topic}
48+
</Badge>
49+
))}
50+
</nav>
51+
<Row as="ul" className="list-unstyled g-4" xs={4}>
52+
{languages.map(language => (
53+
<Col key={language} as="li">
54+
<GitLogo name={language} />
55+
</Col>
56+
))}
57+
</Row>
58+
<Card.Text>{description}</Card.Text>
59+
</Card.Body>
60+
<Card.Footer className="d-flex justify-content-between align-items-center">
61+
{homepage && (
62+
<Button variant="success" target="_blank" href={homepage}>
63+
{t('home_page')}
64+
</Button>
65+
)}
66+
</Card.Footer>
67+
</Card>
68+
);
69+
},
70+
);

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/Git/Logo.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { observable } from 'mobx';
2+
import { observer } from 'mobx-react';
3+
import { PureComponent } from 'react';
4+
import { Image } from 'react-bootstrap';
5+
6+
export interface GitLogoProps {
7+
name: string;
8+
}
9+
10+
@observer
11+
export class GitLogo extends PureComponent<GitLogoProps> {
12+
@observable
13+
accessor path = '';
14+
15+
async componentDidMount() {
16+
const { name } = this.props;
17+
const topic = name.toLowerCase();
18+
19+
try {
20+
const { src } = await this.loadImage(
21+
`https://raw.githubusercontent.com/github/explore/master/topics/${topic}/${topic}.png`,
22+
);
23+
this.path = src;
24+
} catch {
25+
const { src } = await this.loadImage(`https://github.com/${name}.png`);
26+
27+
this.path = src;
28+
}
29+
}
30+
31+
loadImage(path: string) {
32+
return new Promise<HTMLImageElement>((resolve, reject) => {
33+
const image = new globalThis.Image();
34+
35+
image.onload = () => resolve(image);
36+
image.onerror = reject;
37+
38+
image.src = path;
39+
});
40+
}
41+
42+
render() {
43+
const { path } = this;
44+
const { name } = this.props;
45+
46+
return path && <Image fluid src={path} alt={name} />;
47+
}
48+
}

components/Navigator/MainNavigator.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
1919
href: '/article/open-collaborator-award',
2020
name: t('open_collaborator_award'),
2121
},
22+
{ href: '/project', name: t('open_source_projects') },
23+
{ href: '/issue', name: 'GitHub issues' },
2224
{
2325
href: 'https://github.com/Open-Source-Bazaar/Git-Hackathon-scaffold',
2426
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!,

0 commit comments

Comments
 (0)