From 128e7133fdab6cff10e10f4b7c00080b5125c1fe Mon Sep 17 00:00:00 2001 From: South Drifted Date: Sat, 2 Mar 2024 17:25:47 +0000 Subject: [PATCH] [add] KaiYuanShe GitHub modules [fix] Repository names in Project files --- .github/workflows/deploy-production.yml | 4 +- README.md | 18 ++-- components/Git/Issue/Card.tsx | 75 ++++++++++++++++ components/Git/Issue/IssueModule.tsx | 53 ++++++++++++ components/data.ts | 4 + docker-compose.yml | 8 +- models/Base.ts | 2 +- models/Repository.ts | 87 ++++++++++++++----- package.json | 7 +- pages/api/GitHub/[...slug].ts | 3 + pages/api/GitHub/core.ts | 21 +++++ pages/index.tsx | 110 +++++++++++------------- pages/issue.tsx | 58 +++++++++++++ pnpm-lock.yaml | 9 ++ translation/en-US.ts | 3 + translation/zh-CN.ts | 3 + translation/zh-TW.ts | 3 + 17 files changed, 365 insertions(+), 103 deletions(-) create mode 100644 components/Git/Issue/Card.tsx create mode 100644 components/Git/Issue/IssueModule.tsx create mode 100644 pages/api/GitHub/[...slug].ts create mode 100644 pages/api/GitHub/core.ts create mode 100644 pages/issue.tsx diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index aceb7a1..f002906 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -9,8 +9,8 @@ on: - v* env: ARTIFACT_PATH: /tmp/artifact.tar - BOX_NAME: next-bootstrap-ts - BOX_URL: idea2app/next-bootstrap-ts + BOX_NAME: oss-toolbox + BOX_URL: kaiyuanshe/oss-toolbox jobs: deploy_docker_image: diff --git a/README.md b/README.md index 2001a32..e100a53 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Next-Bootstrap.ts +# Open Source treasure box [React][1] project scaffold based on [TypeScript][2], [Next.js][3], [Bootstrap][4] & [Workbox][5]. And this project bootstrapped with [`create-next-app`][6]. -[![CI & CD](https://github.com/idea2app/Next-Bootstrap-ts/actions/workflows/main.yml/badge.svg)][7] +[![CI & CD](https://github.com/kaiyuanshe/OSS-toolbox/actions/workflows/main.yml/badge.svg)][7] [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)][8] [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][9] @@ -96,9 +96,9 @@ pnpm container [4]: https://getbootstrap.com/ [5]: https://developers.google.com/web/tools/workbox [6]: https://github.com/vercel/next.js/tree/canary/packages/create-next-app -[7]: https://github.com/idea2app/Next-Bootstrap-ts/actions/workflows/main.yml -[8]: https://codespaces.new/idea2app/Next-Bootstrap-ts -[9]: https://gitpod.io/?autostart=true#https://github.com/idea2app/Next-Bootstrap-ts +[7]: https://github.com/kaiyuanshe/OSS-toolbox/actions/workflows/main.yml +[8]: https://codespaces.new/kaiyuanshe/OSS-toolbox +[9]: https://gitpod.io/?autostart=true#https://github.com/kaiyuanshe/OSS-toolbox [10]: https://www.typescriptlang.org/ [11]: https://mdxjs.com/ [12]: https://nextjs.org/ @@ -110,12 +110,12 @@ pnpm container [18]: https://sentry.io/ [19]: https://github.com/apps/settings [20]: https://github.com/new?template_name=Next-Bootstrap-ts&template_owner=idea2app -[21]: https://codespaces.new/idea2app/Next-Bootstrap-ts +[21]: https://codespaces.new/kaiyuanshe/OSS-toolbox [22]: https://github.com/idea2app/Next-Bootstrap-ts/blob/80967ed49045af9dbcf4d3695a2c39d53a6f71f1/.github/workflows/pull-request.yml#L9-L11 -[23]: https://github.com/idea2app/Next-Bootstrap-ts/settings/secrets/actions +[23]: https://github.com/kaiyuanshe/OSS-toolbox/settings/secrets/actions [24]: https://github.com/kaiyuanshe/kaiyuanshe.github.io/blob/bb4675a56bf1d6b207231313da5ed0af7cf0ebd6/.github/workflows/pull-request.yml#L32-L56 -[25]: https://github.com/idea2app/Next-Bootstrap-ts/issues/new/choose -[26]: https://github.com/idea2app/Next-Bootstrap-ts/projects +[25]: https://github.com/kaiyuanshe/OSS-toolbox/issues/new/choose +[26]: https://github.com/kaiyuanshe/OSS-toolbox/projects [27]: https://nextjs.org/docs/api-routes/introduction [28]: https://nextjs.org/docs/api-routes/introduction [29]: https://nextjs.org/docs diff --git a/components/Git/Issue/Card.tsx b/components/Git/Issue/Card.tsx new file mode 100644 index 0000000..f602dba --- /dev/null +++ b/components/Git/Issue/Card.tsx @@ -0,0 +1,75 @@ +import { Icon, Nameplate, text2color } from 'idea-react'; +import { marked } from 'marked'; +import { FC } from 'react'; +import { Badge, Card, CardProps, Stack } from 'react-bootstrap'; + +import { Issue } from '../../../models/Repository'; + +export type IssueCardProps = Issue & Omit; + +export const IssueCard: FC = ({ + bg = 'light', + text = 'dark', + id, + number, + title, + labels, + body, + html_url, + user, + comments, + created_at, + ...props +}) => ( + + + + #{number} {title} + + + {labels.map( + label => + typeof label === 'object' && ( + + {label.name} + + ), + )} + + + + + {user && } + + + + {comments} + + + + + +); diff --git a/components/Git/Issue/IssueModule.tsx b/components/Git/Issue/IssueModule.tsx new file mode 100644 index 0000000..ed15b3d --- /dev/null +++ b/components/Git/Issue/IssueModule.tsx @@ -0,0 +1,53 @@ +import { Icon, text2color } from 'idea-react'; +import { FC, useState } from 'react'; +import { Badge, Card, Col, Collapse, Row } from 'react-bootstrap'; + +import type { GitRepository } from '../../../models/Repository'; +import { IssueCard } from './Card'; + +export const IssueModule: FC = ({ name, language, issues }) => { + const [isExpand, setIsExpand] = useState(false); + + return ( + + setIsExpand(!isExpand)} + > + + + {language && ( + + {language} + + )} + + + {name} + + + + {issues.length} + + + + + + + + + + + {issues.map(issue => ( + + + + ))} + + + + ); +}; diff --git a/components/data.ts b/components/data.ts index f65e897..edc6434 100644 --- a/components/data.ts +++ b/components/data.ts @@ -9,6 +9,10 @@ export interface Link { } export const MainRoutes: () => Link[] = () => [ + { + title: 'GitHub issues', + path: '/issue', + }, { title: t('license_tool'), path: '/tool/license-filter' }, { title: `${t('Web_polyfill_CDN')} v1`, diff --git a/docker-compose.yml b/docker-compose.yml index 1a0df0a..e9c2775 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,8 @@ networks: idea2app: services: - next-bootstrap-ts: - image: idea2app/next-bootstrap-ts + oss-toolbox: + image: kaiyuanshe/oss-toolbox networks: - idea2app healthcheck: @@ -34,11 +34,11 @@ services: caddy: depends_on: - - next-bootstrap-ts + - oss-toolbox image: caddy ports: - 80:80 - 443:443 networks: - idea2app - command: caddy reverse-proxy --from your.domain.com --to next-bootstrap-ts:3000 + command: caddy reverse-proxy --from your.domain.com --to oss-toolbox:3000 diff --git a/models/Base.ts b/models/Base.ts index 56b4b00..c5481c8 100644 --- a/models/Base.ts +++ b/models/Base.ts @@ -17,7 +17,7 @@ export const ownClient = new HTTPClient({ }); export const githubClient = new HTTPClient({ - baseURI: isServer() ? 'https://api.github.com/' : `${API_Host}/api/github/`, + baseURI: isServer() ? 'https://api.github.com/' : `${API_Host}/api/GitHub/`, responseType: 'json', }).use(({ request }, next) => { if (GithubToken) diff --git a/models/Repository.ts b/models/Repository.ts index b8cba66..948ea10 100644 --- a/models/Repository.ts +++ b/models/Repository.ts @@ -1,47 +1,91 @@ import { components } from '@octokit/openapi-types'; import { memoize } from 'lodash'; -import { ListModel, toggle } from 'mobx-restful'; +import { Filter, ListModel, toggle } from 'mobx-restful'; import { averageOf, buildURLData } from 'web-utility'; import { githubClient } from './Base'; type Repository = components['schemas']['minimal-repository']; +export type Organization = components['schemas']['organization-full']; +export type Issue = components['schemas']['issue']; export interface GitRepository extends Repository { + issues: Issue[]; languages?: string[]; } -export type Organization = components['schemas']['organization-full']; - -const getGitLanguages = memoize(async (URI: string) => { - const { body: languageCount } = await githubClient.get< - Record - >(`repos/${URI}/languages`); - const languageAverage = averageOf(...Object.values(languageCount!)); - - const languageList = Object.entries(languageCount!) - .filter(([_, score]) => score >= languageAverage) - .sort(([_, a], [__, b]) => b - a); +export interface RepositoryFilter extends Filter { + relation: (keyof RepositoryModel['relation'])[]; +} - return languageList.map(([name]) => name); -}); +type ReturnMap = { + [K in keyof T]: T[K] extends (...data: any[]) => Promise + ? Awaited> + : T[K] extends (...data: any[]) => any + ? ReturnType + : never; +}; -export class RepositoryModel extends ListModel { +export class RepositoryModel extends ListModel< + GitRepository, + RepositoryFilter +> { client = githubClient; - baseURI = 'orgs/idea2app/repos'; + baseURI = 'orgs/kaiyuanshe/repos'; indexKey = 'full_name' as const; + relation = { + issues: memoize(async (URI: string) => { + const { body: issuesList } = await this.client.get( + `repos/${URI}/issues?per_page=100`, + ); + return issuesList!.filter(({ pull_request }) => !pull_request); + }), + languages: memoize(async (URI: string) => { + const { body: languageCount } = await this.client.get< + Record + >(`repos/${URI}/languages`); + + const languageAverage = averageOf(...Object.values(languageCount!)); + + const languageList = Object.entries(languageCount!) + .filter(([_, score]) => score >= languageAverage) + .sort(([_, a], [__, b]) => b - a); + + return languageList.map(([name]) => name); + }), + }; + + async getOneRelation( + URI: string, + relation: RepositoryFilter['relation'] = [], + ) { + const relationData = await Promise.all( + relation.map(async key => { + const value = await this.relation[key](URI); + return [key, value]; + }), + ); + return Object.fromEntries(relationData) as ReturnMap< + RepositoryModel['relation'] + >; + } + @toggle('downloading') - async getOne(URI: string) { + async getOne(URI: string, relation: RepositoryFilter['relation'] = []) { const { body } = await this.client.get(`repos/${URI}`); return (this.currentOne = { ...body!, - languages: await getGitLanguages(URI), + ...(await this.getOneRelation(URI, relation)), }); } - async loadPage(page: number, per_page: number) { + async loadPage( + page: number, + per_page: number, + { relation }: RepositoryFilter, + ) { const { body: list } = await this.client.get( `${this.baseURI}?${buildURLData({ type: 'public', @@ -51,10 +95,9 @@ export class RepositoryModel extends ListModel { })}`, ); const pageData = await Promise.all( - list!.map(async ({ full_name, ...item }) => ({ + list!.map(async item => ({ ...item, - full_name, - languages: await getGitLanguages(full_name), + ...(await this.getOneRelation(item.full_name, relation)), })), ); const [_, organization] = this.baseURI.split('/'); diff --git a/package.json b/package.json index 2ea818d..826a347 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@idea2app/next-bootstrap-ts", + "name": "@kaiyuanshe/oss-toolbox", "version": "1.6.0", "description": "React project scaffold based on TypeScript, Next.js, Bootstrap & Workbox.", "private": true, @@ -27,6 +27,7 @@ "less-loader": "^12.2.0", "license-filter": "^0.2.4", "lodash": "^4.17.21", + "marked": "^12.0.0", "mobx": "^6.12.0", "mobx-i18n": "^0.5.0", "mobx-react": "^9.1.0", @@ -82,7 +83,7 @@ "start": "next start", "lint": "next lint", "test": "lint-staged && npm run lint", - "pack-image": "docker build -t idea2app/next-bootstrap-ts:latest .", - "container": "docker rm -f next-bootstrap-ts && docker run --name next-bootstrap-ts -p 3000:3000 -d idea2app/next-bootstrap-ts:latest" + "pack-image": "docker build -t kaiyuanshe/oss-toolbox:latest .", + "container": "docker rm -f oss-toolbox && docker run --name oss-toolbox -p 3000:3000 -d kaiyuanshe/oss-toolbox:latest" } } diff --git a/pages/api/GitHub/[...slug].ts b/pages/api/GitHub/[...slug].ts new file mode 100644 index 0000000..b4f24b4 --- /dev/null +++ b/pages/api/GitHub/[...slug].ts @@ -0,0 +1,3 @@ +import { proxyGithub } from './core'; + +export default proxyGithub(); diff --git a/pages/api/GitHub/core.ts b/pages/api/GitHub/core.ts new file mode 100644 index 0000000..031dd4d --- /dev/null +++ b/pages/api/GitHub/core.ts @@ -0,0 +1,21 @@ +import { githubClient } from '../../../models/Base'; +import { safeAPI } from '../core'; + +export const proxyGithub = (dataFilter?: (path: string, data: T) => T) => + safeAPI(async ({ method, url, headers, body }, response) => { + delete headers.host; + + const path = url!.slice(`/api/github/`.length); + + const { status, body: data } = await githubClient.request({ + // @ts-ignore + method, + path, + // @ts-ignore + headers, + body: body || undefined, + }); + + response.status(status); + response.send(dataFilter?.(path, data as T) || data); + }); diff --git a/pages/index.tsx b/pages/index.tsx index 1f4856a..f0a2d0b 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,74 +1,60 @@ +import { Loading } from 'idea-react'; import { observer } from 'mobx-react'; -import { compose, translator } from 'next-ssr-middleware'; -import { Card, Col, Container, Row } from 'react-bootstrap'; +import { ScrollList } from 'mobx-restful-table'; +import { InferGetServerSidePropsType } from 'next'; +import { cache, compose, errorLogger, translator } from 'next-ssr-middleware'; +import { FC } from 'react'; +import { Col, Container, Row } from 'react-bootstrap'; import { GitCard } from '../components/Git/Card'; import { PageHead } from '../components/PageHead'; +import repositoryStore, { RepositoryModel } from '../models/Repository'; import { i18n } from '../models/Translation'; -import styles from '../styles/Home.module.less'; -import { framework, mainNav } from './api/home'; - -export const getServerSideProps = compose(translator(i18n)); const { t } = i18n; -const HomePage = observer(() => ( - - - -

- {t('welcome_to')} - - Next.js! - -

- -

- {t('get_started_by_editing')} - - pages/index.tsx - -

- - - {mainNav().map(({ link, title, summary }) => ( - - - - - - {title} → - - - {summary} - - - - ))} - - -

{t('upstream_projects')}

- - {framework.map( - ({ title, languages, tags, summary, link, repository }) => ( - - - - ), +export const getServerSideProps = compose( + cache(), + errorLogger, + translator(i18n), + async () => { + const list = await new RepositoryModel().getList({ + relation: ['languages'], + }); + return { props: { list } }; + }, +); + +const ProjectListPage: FC< + InferGetServerSidePropsType +> = observer(({ list }) => ( + + +

{t('kaiyuanshe_projects')}

+ + {repositoryStore.downloading > 0 && } + + ( + + {allItems.map( + item => + item.homepage && ( + + + + ), + )} + )} -
+ defaultData={list} + />
)); -export default HomePage; +export default ProjectListPage; diff --git a/pages/issue.tsx b/pages/issue.tsx new file mode 100644 index 0000000..759b913 --- /dev/null +++ b/pages/issue.tsx @@ -0,0 +1,58 @@ +import { Loading } from 'idea-react'; +import { observer } from 'mobx-react'; +import { ScrollList } from 'mobx-restful-table'; +import { InferGetServerSidePropsType } from 'next'; +import { compose, translator } from 'next-ssr-middleware'; +import { FC } from 'react'; +import { Breadcrumb, Container, Row } from 'react-bootstrap'; + +import { IssueModule } from '../components/Git/Issue/IssueModule'; +import { PageHead } from '../components/PageHead'; +import repositoryStore, { RepositoryModel } from '../models/Repository'; +import { i18n } from '../models/Translation'; + +export const getServerSideProps = compose(translator(i18n), async () => { + const list = await new RepositoryModel().getList({ relation: ['issues'] }); + + return { props: { list } }; +}); + +const { t } = i18n; + +const IssuesPage: FC> = + observer(({ list }) => ( + + + + + {t('open_source_treasure_box')} + + GitHub issues + +

GitHub issues

+ + {repositoryStore.downloading > 0 && } + + ( + + {allItems.map( + repository => + !repository.archived && + repository.issues[0] && ( + + ), + )} + + )} + /> +
+ )); + +export default IssuesPage; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9025ae7..4e2336a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ dependencies: lodash: specifier: ^4.17.21 version: 4.17.21 + marked: + specifier: ^12.0.0 + version: 12.0.0 mobx: specifier: ^6.12.0 version: 6.12.0 @@ -4973,6 +4976,12 @@ packages: resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} dev: false + /marked@12.0.0: + resolution: {integrity: sha512-Vkwtq9rLqXryZnWaQc86+FHLC6tr/fycMfYAhiOIXkrNmeGAyhSxjqu0Rs1i0bBqw5u0S7+lV9fdH2ZSVaoa0w==} + engines: {node: '>= 18'} + hasBin: true + dev: false + /mdast-util-find-and-replace@3.0.1: resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} dependencies: diff --git a/translation/en-US.ts b/translation/en-US.ts index 07df95f..27fe99a 100644 --- a/translation/en-US.ts +++ b/translation/en-US.ts @@ -26,6 +26,9 @@ export default { open_source_mirror: 'Open-Source Mirror', license_tool: 'License Tool', + // GitHub project list page + kaiyuanshe_projects: 'KAIYUANSHE projects', + // Pagination Table create: 'Create', submit: 'Submit', diff --git a/translation/zh-CN.ts b/translation/zh-CN.ts index e9be162..a59c2b7 100644 --- a/translation/zh-CN.ts +++ b/translation/zh-CN.ts @@ -24,6 +24,9 @@ export default { open_source_mirror: '开源镜像站', license_tool: '开源许可证选择器', + // GitHub project list page + kaiyuanshe_projects: '开源社项目', + // Pagination Table create: '新增', submit: '提交', diff --git a/translation/zh-TW.ts b/translation/zh-TW.ts index 0137b6d..a1bac2a 100644 --- a/translation/zh-TW.ts +++ b/translation/zh-TW.ts @@ -24,6 +24,9 @@ export default { open_source_mirror: '開源鏡像站', license_tool: '開源許可證選擇器', + // GitHub project list page + kaiyuanshe_projects: '開源社專案', + // Pagination Table create: '新增', submit: '提交',