From 40a2980a3a5c8113af0ecc5c82a71afcc12e60da Mon Sep 17 00:00:00 2001 From: South Drifted Date: Fri, 16 Aug 2024 07:21:42 +0800 Subject: [PATCH] [add] Type package, RESTful API & JWT middleware of OHP back-end v6 (#213) --- .env | 2 +- .env.development | 1 + README.md | 4 +- components/Activity/ActivityCard.tsx | 4 +- components/Activity/ActivityControl.tsx | 4 +- components/Activity/ActivityEditor.tsx | 5 +- components/Activity/ActivityEntry.tsx | 4 +- components/Activity/ActivityList.tsx | 11 +- components/Activity/AwardList.tsx | 8 +- components/Activity/EnrollmentList.tsx | 2 +- components/Git/Card.tsx | 2 +- components/Message/MessageList.tsx | 20 +- components/Organization/OrganizationList.tsx | 8 +- .../PlatformAdmin/PlatformAdminModal.tsx | 2 +- components/Team/TeamAdministratorTable.tsx | 4 +- components/Team/TeamAwardCard.tsx | 4 +- components/Team/TeamAwardList.tsx | 4 +- components/Team/TeamCard.tsx | 9 +- components/Team/TeamManageFrame.tsx | 2 +- components/Team/TeamParticipantTable.tsx | 2 +- components/Team/TeamWorkList.tsx | 8 +- components/Team/WorkEditor.tsx | 6 +- .../User/ActivityAdministratorModal.tsx | 2 +- components/User/TopUserList.tsx | 37 +- components/User/UserBar.tsx | 49 +- components/User/UserList.tsx | 10 +- components/layout/ScrollList.tsx | 8 +- models/Activity/Award.ts | 5 +- models/Activity/Enrollment.ts | 5 +- models/Activity/Log.ts | 3 +- models/Activity/Message.ts | 3 +- models/Activity/Organization.ts | 3 +- models/Activity/Staff.ts | 10 +- models/Activity/Team.ts | 16 +- models/Activity/index.ts | 67 +-- models/Base/index.ts | 26 +- models/Git.ts | 5 +- models/TemplateRepo.ts | 5 +- models/User/PlatformAdmin.ts | 2 +- models/User/Session.ts | 33 +- models/User/index.ts | 27 +- next.config.mjs | 26 +- package.json | 16 +- pages/_app.tsx | 4 +- pages/_error.tsx | 2 +- pages/activity/[name]/index.tsx | 10 +- .../activity/[name]/manage/administrator.tsx | 2 +- pages/activity/[name]/manage/git.tsx | 2 +- pages/activity/[name]/manage/message.tsx | 2 +- pages/activity/[name]/manage/organization.tsx | 2 +- pages/activity/[name]/team/[tid]/index.tsx | 10 +- .../activity/[name]/team/[tid]/manage/git.tsx | 35 +- .../[name]/team/[tid]/manage/participant.tsx | 6 +- .../[name]/team/[tid]/manage/role.tsx | 6 +- .../[name]/team/[tid]/work/[wid]/edit.tsx | 9 +- .../[name]/team/[tid]/work/create.tsx | 4 +- pages/activity/create.tsx | 11 +- pages/admin/platform-admin.tsx | 2 +- pages/api/core.ts | 59 ++ pnpm-lock.yaml | 544 ++++++++++++------ 60 files changed, 722 insertions(+), 462 deletions(-) create mode 100644 .env.development diff --git a/.env b/.env index 3486c4df..9ca3a058 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ NEXT_PUBLIC_SITE_NAME = 开放黑客松 NEXT_PUBLIC_SITE_SUMMARY = 基于 Git 云开发环境的开放黑客马拉松平台 -NEXT_PUBLIC_API_HOST = https://hackathon-api-test.kaiyuanshe.cn/v2/ +NEXT_PUBLIC_API_HOST = https://openhackathon-service-server.onrender.com NEXT_PUBLIC_AUTHING_APP_ID = 60178760106d5f26cb267ac1 \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 00000000..abb17d11 --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +NEXT_PUBLIC_API_HOST = http://127.0.0.1:8080 diff --git a/README.md b/README.md index 009e33e8..962cc3be 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ Open-source [Hackathon][1] Platform with **Git-based Cloud Development Environme - [Task kanban](https://github.com/orgs/kaiyuanshe/projects/9/) - [UI design](https://www.figma.com/file/HKPV8IB4kxrAVAuuSBZKd1/Open-Hackathon) - Web entry - - testing: https://open-hackathon.vercel.app/ + - testing: https://test.hackathon.kaiyuanshe.cn/ - production: https://hackathon.kaiyuanshe.cn/ - RESTful API - - production: https://hackathon-server.kaiyuanshe.cn/documentation/ + - production: https://openhackathon-service-server.onrender.com/ ## Technology stack diff --git a/components/Activity/ActivityCard.tsx b/components/Activity/ActivityCard.tsx index 47c56e6a..c932f677 100644 --- a/components/Activity/ActivityCard.tsx +++ b/components/Activity/ActivityCard.tsx @@ -4,10 +4,10 @@ import { faTags, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Hackathon } from '@kaiyuanshe/openhackathon-service'; import classNames from 'classnames'; import { Card, Col, Row } from 'react-bootstrap'; -import { Activity } from '../../models/Activity'; import { i18n } from '../../models/Base/Translation'; import { convertDatetime } from '../../utils/time'; import { ActivityControl, ActivityControlProps } from './ActivityControl'; @@ -15,7 +15,7 @@ import { ActivityEntry } from './ActivityEntry'; const { t } = i18n; -export interface ActivityCardProps extends Activity, ActivityControlProps { +export interface ActivityCardProps extends Hackathon, ActivityControlProps { className?: string; controls?: boolean; } diff --git a/components/Activity/ActivityControl.tsx b/components/Activity/ActivityControl.tsx index b6ff9049..38f7d6c1 100644 --- a/components/Activity/ActivityControl.tsx +++ b/components/Activity/ActivityControl.tsx @@ -1,15 +1,15 @@ +import { Hackathon } from '@kaiyuanshe/openhackathon-service'; import { observer } from 'mobx-react'; import { FC } from 'react'; import { Button } from 'react-bootstrap'; -import { Activity } from '../../models/Activity'; import { i18n } from '../../models/Base/Translation'; import platformAdmin from '../../models/User/PlatformAdmin'; const { t } = i18n; export interface ActivityControlProps - extends Pick { + extends Pick { onPublish?: (name: string) => any; onDelete?: (name: string) => any; } diff --git a/components/Activity/ActivityEditor.tsx b/components/Activity/ActivityEditor.tsx index f1302981..4cdb20d9 100644 --- a/components/Activity/ActivityEditor.tsx +++ b/components/Activity/ActivityEditor.tsx @@ -1,3 +1,4 @@ +import { Hackathon } from '@kaiyuanshe/openhackathon-service'; import { Loading } from 'idea-react'; import { observable } from 'mobx'; import { textJoin } from 'mobx-i18n'; @@ -8,7 +9,7 @@ import { FormEvent, PureComponent } from 'react'; import { Button, Col, Form, Row } from 'react-bootstrap'; import { formToJSON } from 'web-utility'; -import activityStore, { Activity } from '../../models/Activity'; +import activityStore from '../../models/Activity'; import fileStore from '../../models/Base/File'; import { i18n } from '../../models/Base/Translation'; import { DateTimeInput } from '../DateTimeInput'; @@ -16,7 +17,7 @@ import { DateTimeInput } from '../DateTimeInput'; const { t } = i18n, HTMLEditor = dynamic(() => import('../HTMLEditor'), { ssr: false }); -interface ActivityFormData extends Activity { +interface ActivityFormData extends Hackathon { bannerUrls: string[] | string; } diff --git a/components/Activity/ActivityEntry.tsx b/components/Activity/ActivityEntry.tsx index 78acf8a2..7f5cd7ab 100644 --- a/components/Activity/ActivityEntry.tsx +++ b/components/Activity/ActivityEntry.tsx @@ -1,14 +1,14 @@ +import { Hackathon } from '@kaiyuanshe/openhackathon-service'; import { FC } from 'react'; import { Button } from 'react-bootstrap'; import { diffTime } from 'web-utility'; -import { Activity } from '../../models/Activity'; import { i18n } from '../../models/Base/Translation'; const { t } = i18n; export type ActivityStatusMeta = Pick< - Activity, + Hackathon, | 'status' | 'enrollmentStartedAt' | 'enrollmentEndedAt' diff --git a/components/Activity/ActivityList.tsx b/components/Activity/ActivityList.tsx index 70cea5c7..8bf37d1a 100644 --- a/components/Activity/ActivityList.tsx +++ b/components/Activity/ActivityList.tsx @@ -1,12 +1,9 @@ +import { Hackathon } from '@kaiyuanshe/openhackathon-service'; import { ScrollList } from 'mobx-restful-table'; import { FC, PureComponent } from 'react'; import { Col, Row } from 'react-bootstrap'; -import { - Activity, - ActivityListType, - ActivityModel, -} from '../../models/Activity'; +import { ActivityListType, ActivityModel } from '../../models/Activity'; import { i18n } from '../../models/Base/Translation'; import platformAdmin from '../../models/User/PlatformAdmin'; import sessionStore from '../../models/User/Session'; @@ -16,11 +13,11 @@ import { ActivityCard, ActivityCardProps } from './ActivityCard'; const { t } = i18n; export interface ActivityListLayoutProps - extends XScrollListProps, + extends XScrollListProps, Pick { type?: ActivityListType; size?: 'sm' | 'lg'; - userId?: string; + userId?: number; } export const ActivityListLayout: FC = ({ diff --git a/components/Activity/AwardList.tsx b/components/Activity/AwardList.tsx index 4c2c1900..0d3ab65d 100644 --- a/components/Activity/AwardList.tsx +++ b/components/Activity/AwardList.tsx @@ -12,8 +12,8 @@ import { XScrollListProps } from '../layout/ScrollList'; const { t } = i18n; export interface AwardListLayoutProps extends XScrollListProps { - onEdit?: (id: string) => any; - onDelete?: (id: string) => any; + onEdit?: (id: number) => any; + onDelete?: (id: number) => any; } export const AwardTargetName = () => ({ @@ -84,12 +84,12 @@ export type AwardListProps = Pick, 'store'> & AwardListLayoutProps; export class AwardList extends PureComponent { - onEdit = (id: string) => { + onEdit = (id: number) => { this.props.onEdit?.(id); this.props.store.getOne(id); }; - onDelete = (id: string) => { + onDelete = (id: number) => { if (!confirm(t('sure_delete_this_work'))) return; this.props.onDelete?.(id); diff --git a/components/Activity/EnrollmentList.tsx b/components/Activity/EnrollmentList.tsx index 1cbcf9ea..0a661402 100644 --- a/components/Activity/EnrollmentList.tsx +++ b/components/Activity/EnrollmentList.tsx @@ -13,7 +13,7 @@ const { t } = i18n; export interface EnrollmentListLayoutProps extends XScrollListProps { onPopUp?: (extensions: Enrollment['extensions']) => any; - onVerify?: (userId: string, status: Enrollment['status']) => any; + onVerify?: (userId: number, status: Enrollment['status']) => any; } export const EnrollmentListLayout: FC = ({ diff --git a/components/Git/Card.tsx b/components/Git/Card.tsx index b43d202c..afd9dad3 100644 --- a/components/Git/Card.tsx +++ b/components/Git/Card.tsx @@ -75,7 +75,7 @@ export const GitCard: FC = observer( className="d-flex align-items-center" style={{ gap: '0.5rem' }} type="radio" - id={id} + id={id + ''} name="template" value={name} label={t('select')} diff --git a/components/Message/MessageList.tsx b/components/Message/MessageList.tsx index 505abe58..a6fe65ef 100644 --- a/components/Message/MessageList.tsx +++ b/components/Message/MessageList.tsx @@ -19,8 +19,8 @@ const { t } = i18n; export interface MessageListLayoutProps extends XScrollListProps { hideControls?: boolean; - onEdit?: (id: string) => any; - onDelete?: (id: string) => any; + onEdit?: (id: number) => any; + onDelete?: (id: number) => any; } export const MessageListLayout: FC = ({ @@ -53,7 +53,7 @@ export const MessageListLayout: FC = ({ onSelect?.( selectedIds.length === defaultData.length ? [] - : defaultData.map(({ id }) => id + ''), + : defaultData.map(({ id }) => id), ) } /> @@ -72,13 +72,13 @@ export const MessageListLayout: FC = ({ inline type="checkbox" name="announcementId" - checked={selectedIds?.includes(id + '')} + checked={selectedIds?.includes(id)} onClick={ onSelect && (({ currentTarget: { checked } }) => { - if (checked) return onSelect([...selectedIds, id + '']); + if (checked) return onSelect([...selectedIds, id]); - const index = selectedIds.indexOf(id + ''); + const index = selectedIds.indexOf(id); onSelect([ ...selectedIds.slice(0, index), @@ -116,17 +116,17 @@ export type MessageListProps = Pick, 'store'> & @observer export class MessageList extends PureComponent { @observable - accessor selectedIds: string[] = []; + accessor selectedIds: number[] = []; - onSelect = (list: string[]) => + onSelect = (list: number[]) => (this.selectedIds = list) && this.props.onSelect?.(list); - onEdit = (id: string) => { + onEdit = (id: number) => { this.props.onEdit?.(id); this.props.store.getOne(id); }; - onDelete = (id: string) => { + onDelete = (id: number) => { if (!confirm(t('sure_delete_this_message'))) return; this.props.onDelete?.(id); diff --git a/components/Organization/OrganizationList.tsx b/components/Organization/OrganizationList.tsx index 7646719d..22f2ab1c 100644 --- a/components/Organization/OrganizationList.tsx +++ b/components/Organization/OrganizationList.tsx @@ -51,7 +51,7 @@ export const OrganizationTableLayout: FC> = ({ onSelect?.( selectedIds.length === defaultData.length ? [] - : defaultData.map(({ id }) => String(id)), + : defaultData.map(({ id }) => id), ) } /> @@ -70,13 +70,13 @@ export const OrganizationTableLayout: FC> = ({ inline type="checkbox" name="organizationId" - checked={selectedIds?.includes(String(id))} + checked={selectedIds?.includes(id)} onClick={ onSelect && (({ currentTarget: { checked } }) => { - if (checked) return onSelect([...selectedIds, String(id)]); + if (checked) return onSelect([...selectedIds, id]); - const index = selectedIds.indexOf(String(id)); + const index = selectedIds.indexOf(id); onSelect([ ...selectedIds.slice(0, index), diff --git a/components/PlatformAdmin/PlatformAdminModal.tsx b/components/PlatformAdmin/PlatformAdminModal.tsx index ab50badb..fc454d8f 100644 --- a/components/PlatformAdmin/PlatformAdminModal.tsx +++ b/components/PlatformAdmin/PlatformAdminModal.tsx @@ -19,7 +19,7 @@ export interface PlatformAdminModalProps @observer export class PlatformAdminModal extends PureComponent { @observable - accessor userId = ''; + accessor userId = 0; increaseId = async (event: FormEvent) => { event.preventDefault(); diff --git a/components/Team/TeamAdministratorTable.tsx b/components/Team/TeamAdministratorTable.tsx index 006dd1ec..1f5e2b2b 100644 --- a/components/Team/TeamAdministratorTable.tsx +++ b/components/Team/TeamAdministratorTable.tsx @@ -13,8 +13,8 @@ const { t } = i18n; export interface TeamAdministratorTableLayoutProps extends XScrollListProps { - onUpdateRole?: (userId: string, role: 'admin' | 'member') => any; - onPopUpUpdateRoleModal?: (userId: string) => any; + onUpdateRole?: (userId: number, role: 'admin' | 'member') => any; + onPopUpUpdateRoleModal?: (userId: number) => any; } const TableHeads = () => [ diff --git a/components/Team/TeamAwardCard.tsx b/components/Team/TeamAwardCard.tsx index 45d1862a..50f2cac5 100644 --- a/components/Team/TeamAwardCard.tsx +++ b/components/Team/TeamAwardCard.tsx @@ -26,8 +26,8 @@ export interface TeamAwardCardProps | 'id' > { className?: string; - onAssign: (id: string) => any; - onDelete?: (id: string) => any; + onAssign: (id: number) => any; + onDelete?: (id: number) => any; } export class TeamAwardCard extends PureComponent { diff --git a/components/Team/TeamAwardList.tsx b/components/Team/TeamAwardList.tsx index 34330654..56a1838b 100644 --- a/components/Team/TeamAwardList.tsx +++ b/components/Team/TeamAwardList.tsx @@ -35,12 +35,12 @@ export type TeamAwardListProps = Pick, 'store'> & TeamAwardListLayoutProps; export class TeamAwardList extends PureComponent { - onAssign = (id: string) => { + onAssign = (id: number) => { this.props.onAssign?.(id); this.props.store.getOne(id); }; - onDelete = (id: string) => { + onDelete = (id: number) => { if (!confirm(t('sure_delete_this_work'))) return; this.props.onDelete?.(id); diff --git a/components/Team/TeamCard.tsx b/components/Team/TeamCard.tsx index bc99eedb..c051b35b 100644 --- a/components/Team/TeamCard.tsx +++ b/components/Team/TeamCard.tsx @@ -7,10 +7,15 @@ import { i18n } from '../../models/Base/Translation'; const { t } = i18n; -export type TeamCardProps = HTMLAttributes & +export type TeamCardProps = Omit, 'id'> & Pick< Team, - 'hackathonName' | 'displayName' | 'creatorId' | 'creator' | 'membersCount' + | 'id' + | 'hackathonName' + | 'displayName' + | 'creatorId' + | 'creator' + | 'membersCount' >; export const TeamCard: FC = ({ diff --git a/components/Team/TeamManageFrame.tsx b/components/Team/TeamManageFrame.tsx index 6188b489..51571845 100644 --- a/components/Team/TeamManageFrame.tsx +++ b/components/Team/TeamManageFrame.tsx @@ -33,7 +33,7 @@ export type TeamManageBaseParams = Record<'name' | 'tid', string>; export type TeamManageBaseProps = RouteProps & JWTProps; export interface TeamManageFrameProps extends ActivityManageFrameProps { - tid: string; + tid: number; } @observer diff --git a/components/Team/TeamParticipantTable.tsx b/components/Team/TeamParticipantTable.tsx index 1b9cabbb..b17b3d2b 100644 --- a/components/Team/TeamParticipantTable.tsx +++ b/components/Team/TeamParticipantTable.tsx @@ -11,7 +11,7 @@ const { t } = i18n; export interface TeamParticipantTableLayoutProps extends XScrollListProps { - onApprove?: (userId: string, status: MembershipStatus) => any; + onApprove?: (userId: number, status: MembershipStatus) => any; } const StatusName: () => Record = () => ({ diff --git a/components/Team/TeamWorkList.tsx b/components/Team/TeamWorkList.tsx index f7e491b7..491ebaad 100644 --- a/components/Team/TeamWorkList.tsx +++ b/components/Team/TeamWorkList.tsx @@ -13,7 +13,9 @@ import { XScrollListProps } from '../layout/ScrollList'; const { t } = i18n; -export interface TeamWorkCardProps extends TeamWork, Omit { +export interface TeamWorkCardProps + extends TeamWork, + Omit { controls?: boolean; onDelete?: (id: TeamWork['id']) => any; } @@ -85,7 +87,7 @@ export interface TeamWorkListLayoutProps extends XScrollListProps, Pick { activity: string; - team: string; + team: number; size?: 'sm' | 'lg'; } @@ -129,7 +131,7 @@ export const TeamWorkListLayout: FC = observer( export class TeamWorkList extends PureComponent { store = activityStore.teamOf(this.props.activity).workOf(this.props.team); - onDelete = (id?: string) => + onDelete = (id?: number) => id && confirm(t('confirm_delete_work')) && this.store.deleteOne(id); render() { diff --git a/components/Team/WorkEditor.tsx b/components/Team/WorkEditor.tsx index 774ccffa..c09b5c69 100644 --- a/components/Team/WorkEditor.tsx +++ b/components/Team/WorkEditor.tsx @@ -12,7 +12,11 @@ import { i18n } from '../../models/Base/Translation'; const { t } = i18n; -export type WorkEditorProps = Record<'name' | 'tid', string> & { wid?: string }; +export interface WorkEditorProps { + name: string; + tid: number; + wid?: string; +} @observer export class WorkEditor extends PureComponent { diff --git a/components/User/ActivityAdministratorModal.tsx b/components/User/ActivityAdministratorModal.tsx index 6822a954..0bb6413a 100644 --- a/components/User/ActivityAdministratorModal.tsx +++ b/components/User/ActivityAdministratorModal.tsx @@ -20,7 +20,7 @@ export interface AdministratorModalProps @observer export class AdministratorModal extends PureComponent { @observable - accessor userId = ''; + accessor userId = 0; increaseId = async (event: FormEvent) => { event.preventDefault(); diff --git a/components/User/TopUserList.tsx b/components/User/TopUserList.tsx index 28ddd772..5655a704 100644 --- a/components/User/TopUserList.tsx +++ b/components/User/TopUserList.tsx @@ -1,16 +1,15 @@ +import { UserRank } from '@kaiyuanshe/openhackathon-service'; import { FC } from 'react'; import { Badge, Col, Image, Row, Table } from 'react-bootstrap'; -import { parseJSON } from 'web-utility'; import { i18n } from '../../models/Base/Translation'; -import { TopUser } from '../../models/User'; import styles from '../../styles/TopUserList.module.less'; import { TopUserAddress } from './TopUserAddress'; const { t } = i18n; export interface TopUserListProps { - value: TopUser[]; + value: UserRank[]; } export const TopUserList: FC = ({ value = [] }) => ( @@ -40,13 +39,8 @@ export const TopUserList: FC = ({ value = [] }) => ( > {
= ({ value = [] }) => ( className="d-block mb-0 stretched-link" href={`/user/${userId}`} > - {user?.nickname || - user?.name || - user?.username || - t('mystery_hacker')} + {user?.name || t('mystery_hacker')} {score}
@@ -93,13 +84,8 @@ export const TopUserList: FC = ({ value = [] }) => ( > { = ({ value = [] }) => ( }} href={`/user/${userId}`} > - {user?.nickname || - user?.name || - user?.username || - t('mystery_hacker')} + {user?.name || t('mystery_hacker')} {score} diff --git a/components/User/UserBar.tsx b/components/User/UserBar.tsx index 58de38b0..06772ed1 100644 --- a/components/User/UserBar.tsx +++ b/components/User/UserBar.tsx @@ -4,7 +4,6 @@ import { Button, Dropdown } from 'react-bootstrap'; import { i18n } from '../../models/Base/Translation'; import sessionStore from '../../models/User/Session'; import LanguageMenu from './LanguageMenu'; -import { SessionBox } from './SessionBox'; const UserBar = observer(() => { const { t } = i18n; @@ -14,34 +13,28 @@ const UserBar = observer(() => { return ( <> - {!user ? ( - - - - ) : ( - <> - + - - {showName} - - - {t('home_page')} - - - {t('edit_profile')} - - sessionStore.signOut(true)}> - {t('sign_out')} - - - - + {user && ( + + {showName} + + + {t('home_page')} + + + {t('edit_profile')} + + sessionStore.signOut(true)}> + {t('sign_out')} + + + )} diff --git a/components/User/UserList.tsx b/components/User/UserList.tsx index 5bebb352..54a50ad7 100644 --- a/components/User/UserList.tsx +++ b/components/User/UserList.tsx @@ -69,10 +69,10 @@ export const UserListLayout: FC = ({ name="userId" required value={id} - aria-label={id} + aria-label={id + ''} checked={selectedIds?.includes(id!)} onChange={({ currentTarget: { form } }) => - onSelect?.([formToJSON<{ userId: string }>(form!).userId]) + onSelect?.([formToJSON<{ userId: number }>(form!).userId]) } /> @@ -92,14 +92,14 @@ export type UserListProps = Pick, 'store'> & UserListLayoutProps; export class UserList extends PureComponent { - onSearch = async (keyword: string) => { + onSearch = async (keywords: string) => { const { store, onSearch } = this.props; store.clear(); - await store.getList({ keyword }); + await store.getList({ keywords }); - onSearch?.(keyword); + onSearch?.(keywords); }; render() { diff --git a/components/layout/ScrollList.tsx b/components/layout/ScrollList.tsx index e4ab0894..c2746740 100644 --- a/components/layout/ScrollList.tsx +++ b/components/layout/ScrollList.tsx @@ -1,8 +1,8 @@ -import { DataObject } from 'mobx-restful'; +import { Base } from '@kaiyuanshe/openhackathon-service'; import { ScrollListProps } from 'mobx-restful-table'; -export interface XScrollListProps +export interface XScrollListProps extends Pick, 'defaultData'> { - selectedIds?: string[]; - onSelect?: (selectedIds: string[]) => any; + selectedIds?: number[]; + onSelect?: (selectedIds: number[]) => any; } diff --git a/models/Activity/Award.ts b/models/Activity/Award.ts index e888cc45..0e7bd20f 100644 --- a/models/Activity/Award.ts +++ b/models/Activity/Award.ts @@ -1,7 +1,8 @@ import { User } from '@authing/native-js-ui-components'; +import { Base, Media } from '@kaiyuanshe/openhackathon-service'; import { ListModel, Stream, toggle } from 'mobx-restful'; -import { Base, createListStream, InputData, Media } from '../Base'; +import { createListStream, InputData } from '../Base'; import sessionStore from '../User/Session'; import { Team } from './Team'; @@ -16,7 +17,7 @@ export interface Award export interface AwardAssignment extends Omit, Omit, - Record<'assignmentId' | 'assigneeId' | 'awardId', string> { + Record<'assignmentId' | 'assigneeId' | 'awardId', number> { user?: User; team?: Team; award: Award; diff --git a/models/Activity/Enrollment.ts b/models/Activity/Enrollment.ts index 850a8f1a..61db7dc5 100644 --- a/models/Activity/Enrollment.ts +++ b/models/Activity/Enrollment.ts @@ -1,8 +1,9 @@ +import { Base } from '@kaiyuanshe/openhackathon-service'; import { computed, observable } from 'mobx'; import { ListModel, Statistic, Stream, toggle } from 'mobx-restful'; import { buildURLData, countBy, groupBy } from 'web-utility'; -import { Base, createListStream, Filter } from '../Base'; +import { createListStream, Filter } from '../Base'; import { i18n } from '../Base/Translation'; import { User } from '../User'; import sessionStore from '../User/Session'; @@ -69,7 +70,7 @@ export class EnrollmentModel extends Stream( } @toggle('uploading') - async verifyOne(userId: string, status: Enrollment['status']) { + async verifyOne(userId: number, status: Enrollment['status']) { await this.client.post( `${this.baseURI}/${userId}/${ status === 'approved' ? 'approve' : 'reject' diff --git a/models/Activity/Log.ts b/models/Activity/Log.ts index 379489d9..c5a1980a 100644 --- a/models/Activity/Log.ts +++ b/models/Activity/Log.ts @@ -1,6 +1,7 @@ +import { Base } from '@kaiyuanshe/openhackathon-service'; import { IDType, ListModel, Stream } from 'mobx-restful'; -import { Base, createListStream } from '../Base'; +import { createListStream } from '../Base'; import sessionStore from '../User/Session'; export interface Log extends Base { diff --git a/models/Activity/Message.ts b/models/Activity/Message.ts index a0c066e8..da9b3e45 100644 --- a/models/Activity/Message.ts +++ b/models/Activity/Message.ts @@ -1,7 +1,8 @@ +import { Base } from '@kaiyuanshe/openhackathon-service'; import { IDType, ListModel, Stream, toggle } from 'mobx-restful'; import { buildURLData } from 'web-utility'; -import { Base, createListStream, Filter, InputData } from '../Base'; +import { createListStream, Filter, InputData } from '../Base'; import { i18n } from '../Base/Translation'; import sessionStore from '../User/Session'; diff --git a/models/Activity/Organization.ts b/models/Activity/Organization.ts index b87e05a0..d54cc2f1 100644 --- a/models/Activity/Organization.ts +++ b/models/Activity/Organization.ts @@ -1,8 +1,9 @@ +import { Base, Media } from '@kaiyuanshe/openhackathon-service'; import { computed } from 'mobx'; import { IDType, ListModel, Stream, toggle } from 'mobx-restful'; import { groupBy } from 'web-utility'; -import { Base, createListStream, InputData, Media } from '../Base'; +import { createListStream, InputData } from '../Base'; import { i18n } from '../Base/Translation'; import sessionStore from '../User/Session'; diff --git a/models/Activity/Staff.ts b/models/Activity/Staff.ts index d56a73ec..2802a6a8 100644 --- a/models/Activity/Staff.ts +++ b/models/Activity/Staff.ts @@ -1,14 +1,16 @@ +import { Base } from '@kaiyuanshe/openhackathon-service'; import { computed } from 'mobx'; import { ListModel, Stream, toggle } from 'mobx-restful'; import { groupBy, mergeStream } from 'web-utility'; -import { Base, createListStream, InputData } from '../Base'; +import { createListStream, InputData } from '../Base'; import { User } from '../User'; import sessionStore from '../User/Session'; export interface HackathonAdmin extends Base, - Record<'hackathonName' | 'description' | 'userId', string> { + Record<'hackathonName' | 'description', string> { + userId: number; user: User; } @@ -60,7 +62,7 @@ export class StaffModel extends Stream(ListModel) { } @toggle('uploading') - async updateOne({ type, ...data }: InputData, userId: string) { + async updateOne({ type, ...data }: InputData, userId: number) { const { body } = await this.client.put( `${this.baseURI}/${type}/${userId}`, data, @@ -73,7 +75,7 @@ export class StaffModel extends Stream(ListModel) { } @toggle('uploading') - async deleteOne(userId: string) { + async deleteOne(userId: number) { const { type } = this.allItems.find(({ userId: id }) => id === userId) || {}; diff --git a/models/Activity/Team.ts b/models/Activity/Team.ts index a5a64c58..dc8a7092 100644 --- a/models/Activity/Team.ts +++ b/models/Activity/Team.ts @@ -1,14 +1,9 @@ +import { Base } from '@kaiyuanshe/openhackathon-service'; import { action, computed, observable } from 'mobx'; import { ListModel, Stream, toggle } from 'mobx-restful'; import { buildURLData } from 'web-utility'; -import { - Base, - createListStream, - Filter, - InputData, - integrateError, -} from '../Base'; +import { createListStream, Filter, InputData, integrateError } from '../Base'; import { WorkspaceModel } from '../Git'; import { User } from '../User'; import sessionStore from '../User/Session'; @@ -34,7 +29,6 @@ export interface Team extends Base, TeamBase, Record<'displayName' | 'creatorId', string> { - id: string; autoApprove: boolean; creator: User; membersCount: number; @@ -57,7 +51,7 @@ export interface TeamWork export interface TeamMember extends Omit, Omit { - userId: string; + userId: number; user: User; role: 'admin' | 'member'; status: MembershipStatus; @@ -210,14 +204,14 @@ export class TeamMemberModel extends Stream>( } @toggle('uploading') - async approveOne(userId: string, status: MembershipStatus) { + async approveOne(userId: number, status: MembershipStatus) { if (status !== MembershipStatus.APPROVED) return; await this.client.post(`${this.baseURI}/${userId}/approve`, {}); this.changeOne({ status }, userId, true); } @toggle('uploading') - async updateRole(userId: string, role: Required['role']) { + async updateRole(userId: number, role: Required['role']) { await this.client.post(`${this.baseURI}/${userId}/updateRole`, { role, }); diff --git a/models/Activity/index.ts b/models/Activity/index.ts index a576630b..8c82fb5b 100644 --- a/models/Activity/index.ts +++ b/models/Activity/index.ts @@ -1,12 +1,16 @@ +import type { + Base, + Hackathon, + HackathonStatus, +} from '@kaiyuanshe/openhackathon-service'; import { action, observable } from 'mobx'; -import { ListModel, Stream, toggle } from 'mobx-restful'; +import { toggle } from 'mobx-restful'; import { buildURLData } from 'web-utility'; -import { Base, createListStream, Filter, InputData, Media } from '../Base'; +import { createListStream, Filter, InputData, TableModel } from '../Base'; import { GitModel } from '../Git'; import { GitTemplateModal } from '../TemplateRepo'; import platformAdmin from '../User/PlatformAdmin'; -import sessionStore from '../User/Session'; import { AwardModel } from './Award'; import { Enrollment, EnrollmentModel } from './Enrollment'; import { LogModel } from './Log'; @@ -16,34 +20,6 @@ import { Extensions, Question } from './Question'; import { StaffModel } from './Staff'; import { TeamModel } from './Team'; -export interface Activity extends Base { - name: string; - displayName: string; - ribbon: string; - summary: string; - detail: string; - location: string; - banners: Media[]; - readOnly: boolean; - status: 'planning' | 'pendingApproval' | 'online' | 'offline'; - creatorId: string; - enrollment: number; - maxEnrollment?: number; - autoApprove: boolean; - tags: string[]; - eventStartedAt: string; - eventEndedAt: string; - enrollmentStartedAt: string; - enrollmentEndedAt: string; - judgeStartedAt: string; - judgeEndedAt: string; - roles: null | { - isAdmin: boolean; - isJudge: boolean; - isEnrolled: boolean; - }; -} - export type ActivityListType = | 'online' | 'admin' @@ -58,13 +34,13 @@ export interface NameAvailability { message: string; } -export interface ActivityFilter extends Filter { - userId?: string; +export interface ActivityFilter extends Filter { + userId?: number; listType?: ActivityListType; orderby?: 'createdAt' | 'updatedAt' | 'hot'; } -export interface ActivityLogsFilter extends Filter { +export interface ActivityLogsFilter extends Filter { name: string; } @@ -73,8 +49,7 @@ export interface Questionnaire extends Base { hackathonName: string; } -export class ActivityModel extends Stream(ListModel) { - client = sessionStore.client; +export class ActivityModel extends TableModel { baseURI = 'hackathon'; indexKey = 'name' as const; @@ -136,7 +111,7 @@ export class ActivityModel extends Stream(ListModel) { listType = 'online', orderby = 'updatedAt', }: ActivityFilter) { - return createListStream( + return createListStream( `${this.baseURI}s?${buildURLData({ userId, listType, orderby, top: 6 })}`, this.client, count => (this.totalCount = count), @@ -144,21 +119,13 @@ export class ActivityModel extends Stream(ListModel) { } @toggle('uploading') - async updateOne(data: InputData, name?: string) { + async updateOne(data: InputData, name?: string) { if (!name) { - const { body } = await this.client.post( - `${this.baseURI}/checkNameAvailability`, - { name: data.name }, - ); - const { nameAvailable, reason, message } = body!; + const [old] = await this.getList({ name: data.name }, 1); - if (!nameAvailable) throw new ReferenceError(`${reason}\n${message}`); + if (old) throw new ReferenceError(`${data.name} is used`); } - const { body } = await (name - ? this.client.patch(`${this.baseURI}/${name}`, data) - : this.client.put(`${this.baseURI}/${data.name}`, data)); - - return (this.currentOne = body!); + return super.updateOne(data, name); } @action @@ -237,7 +204,7 @@ export class ActivityModel extends Stream(ListModel) { await this.client.post( `hackathon/${name}/${isPlatformAdmin ? 'publish' : 'requestPublish'}`, ); - this.changeOne({ status: 'online' }, name, true); + this.changeOne({ status: 'online' as HackathonStatus.Online }, name, true); } @toggle('uploading') diff --git a/models/Base/index.ts b/models/Base/index.ts index 22a4bbd5..904f3505 100644 --- a/models/Base/index.ts +++ b/models/Base/index.ts @@ -1,13 +1,9 @@ +import { Base, ListChunk } from '@kaiyuanshe/openhackathon-service'; import { HTTPError } from 'koajax'; -import { Filter as BaseFilter, RESTClient } from 'mobx-restful'; +import { Filter as BaseFilter, ListModel, RESTClient } from 'mobx-restful'; +import { buildURLData } from 'web-utility'; -export interface Base { - id?: string; - createdAt: string; - updatedAt: string; -} - -export type Media = Record<'name' | 'description' | 'uri', string>; +import sessionStore from '../User/Session'; export interface UploadUrl extends Record<'filename' | 'uploadUrl' | 'url', string> { @@ -67,3 +63,17 @@ export const integrateError = ({ body }: HTTPError) => { message ? `${title || ''}\n${message}` : detail || '', ); }; + +export abstract class TableModel< + D extends Base, + F extends InputData = InputData, +> extends ListModel { + client = sessionStore.client; + + async loadPage(pageIndex: number, pageSize: number, filter: F) { + const { body } = await this.client.get>( + `${this.baseURI}?${buildURLData({ ...filter, pageIndex, pageSize })}`, + ); + return { pageData: body!.list, totalCount: body!.count }; + } +} diff --git a/models/Git.ts b/models/Git.ts index 16599443..7d6f56ad 100644 --- a/models/Git.ts +++ b/models/Git.ts @@ -1,3 +1,4 @@ +import { Base } from '@kaiyuanshe/openhackathon-service'; import { components } from '@octokit/openapi-types'; import { HTTPClient } from 'koajax'; import { memoize } from 'lodash'; @@ -5,7 +6,7 @@ import { ListModel, Stream, toggle } from 'mobx-restful'; import { averageOf } from 'web-utility'; import { TeamWork, TeamWorkType } from './Activity/Team'; -import { Base, createListStream } from './Base'; +import { createListStream } from './Base'; import sessionStore from './User/Session'; type Repository = components['schemas']['repository']; @@ -68,7 +69,7 @@ const getGitRepository = memoize( .sort(([_, a], [__, b]) => b - a); return { - id: id + '', + id, createdAt: created_at || '', updatedAt: updated_at || '', name, diff --git a/models/TemplateRepo.ts b/models/TemplateRepo.ts index 86910d4b..264a82d5 100644 --- a/models/TemplateRepo.ts +++ b/models/TemplateRepo.ts @@ -1,6 +1,7 @@ +import { Base } from '@kaiyuanshe/openhackathon-service'; import { ListModel, Stream, toggle } from 'mobx-restful'; -import { Base, createListStream, InputData } from './Base'; +import { createListStream, InputData } from './Base'; import sessionStore from './User/Session'; export interface GitTemplate extends Base { @@ -41,7 +42,7 @@ export class GitTemplateModal extends Stream(ListModel) { } @toggle('uploading') - async deleteOne(templateRepoId: string) { + async deleteOne(templateRepoId: number) { await this.client.delete(`${this.baseURI}/templateRepo/${templateRepoId}`); await this.removeOne(templateRepoId); } diff --git a/models/User/PlatformAdmin.ts b/models/User/PlatformAdmin.ts index 7233f894..0ed4f880 100644 --- a/models/User/PlatformAdmin.ts +++ b/models/User/PlatformAdmin.ts @@ -46,7 +46,7 @@ export class PlatformAdminModel extends Stream< } @toggle('uploading') - async deleteOne(userId: string) { + async deleteOne(userId: IDType) { await this.client.delete(`${this.baseURI}/${userId}`); await this.removeOne(userId); } diff --git a/models/User/Session.ts b/models/User/Session.ts index a32f78e9..22211649 100644 --- a/models/User/Session.ts +++ b/models/User/Session.ts @@ -1,3 +1,4 @@ +import { Base } from '@kaiyuanshe/openhackathon-service'; import { HTTPClient } from 'koajax'; import { computed, observable } from 'mobx'; import { parseCookie, setCookie } from 'mobx-i18n'; @@ -24,9 +25,10 @@ export const strapiClient = new HTTPClient({ return next(); }); -export interface Base extends Record<'createdAt' | 'updatedAt', string> { - id: number; -} +export const ownClient = new HTTPClient({ + baseURI: process.env.NEXT_PUBLIC_API_HOST, + responseType: 'json', +}); export interface SessionUser extends Base, @@ -37,11 +39,24 @@ export interface SessionUser } export class SessionModel extends BaseModel { + client = ownClient; + constructor() { super(); if (+new Date(this.user?.tokenExpiredAt || '') <= Date.now()) this.signOut(); + + this.client.use(({ request }, next) => { + const { token } = this.user || {}; + + if (token) + request.headers = { + ...request.headers, + Authorization: `Bearer ${token}`, + }; + return next(); + }); } @observable @@ -64,18 +79,6 @@ export class SessionModel extends BaseModel { ) as Record; } - client = new HTTPClient({ - baseURI: process.env.NEXT_PUBLIC_API_HOST, - responseType: 'json', - }).use(({ request }, next) => { - const { token } = this.user || {}; - - if (token) - request.headers = { ...request.headers, Authorization: `token ${token}` }; - - return next(); - }); - @toggle('uploading') async signIn(profile: AuthingUserBase, reload = false) { const { body } = await this.client.post('login', profile); diff --git a/models/User/index.ts b/models/User/index.ts index a975936f..be2b1877 100644 --- a/models/User/index.ts +++ b/models/User/index.ts @@ -1,8 +1,6 @@ -import { ListModel, Stream } from 'mobx-restful'; -import { buildURLData } from 'web-utility'; +import { Base, UserRankListChunk } from '@kaiyuanshe/openhackathon-service'; -import { Base, createListStream, Filter, ListData } from '../Base'; -import sessionStore from './Session'; +import { Filter, TableModel } from '../Base'; export interface UserBase { username: string; @@ -17,7 +15,6 @@ export type GitHubUser = UserBase & Record<'nickname' | 'photo' | 'company' | 'profile' | 'accessToken', string>; export interface AuthingUserBase { - id?: string; _id?: string; openid: string; unionid?: string; @@ -122,27 +119,17 @@ export interface TopUser extends Base { score: number; } export interface UserFilter extends Filter { - keyword?: string; + keywords?: string; } -export class UserModel extends Stream(ListModel) { - client = sessionStore.client; +export class UserModel extends TableModel { baseURI = 'user'; async getUserTopList() { - const { body } = await this.client.get>( - `${this.baseURI}/topUsers`, - ); - return body!.value; - } - - openStream({ keyword = 'x' }: UserFilter) { - return createListStream( - `${this.baseURI}/search?${buildURLData({ keyword })}`, - this.client, - count => (this.totalCount = count), - 'POST', + const { body } = await this.client.get( + `activity-log/user-rank`, ); + return body!.list; } } diff --git a/next.config.mjs b/next.config.mjs index b56887d0..b9bab9ad 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,7 +1,8 @@ import withLess from 'next-with-less'; import setPWA from 'next-pwa'; +import webpack from 'webpack'; -const { NODE_ENV } = process.env; +const { NODE_ENV, CI } = process.env; const withPWA = setPWA({ dest: 'public', @@ -11,9 +12,20 @@ const withPWA = setPWA({ }); /** @type {import('next').NextConfig} */ -export default { - ...withPWA(withLess({ reactStrictMode: true })), - output: 'standalone', - pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], - transpilePackages: ['@sentry/browser'], -}; +export default withPWA( + withLess({ + reactStrictMode: true, + output: CI && 'standalone', + pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], + transpilePackages: ['@sentry/browser'], + + webpack: config => { + config.plugins.push( + new webpack.NormalModuleReplacementPlugin(/^node:/, resource => { + resource.request = resource.request.replace(/^node:/, ''); + }), + ); + return config; + }, + }), +); diff --git a/package.json b/package.json index 4b7a5e43..128856f6 100644 --- a/package.json +++ b/package.json @@ -14,17 +14,19 @@ "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", "@giscus/react": "^3.0.0", + "@sentry/nextjs": "^8.25.0", "array-unique-proposal": "^0.3.4", "classnames": "^2.5.1", "echarts-jsx": "^0.5.4", "idea-react": "^2.0.0-rc.2", + "jsonwebtoken": "^9.0.2", "koajax": "^1.1.2", "leaflet": "^1.9.4", "leaflet.chinatmsproviders": "^3.0.6", "lodash": "^4.17.21", "mobx": "^6.13.1", "mobx-i18n": "^0.5.0", - "mobx-react": "^9.1.0", + "mobx-react": "^9.1.1", "mobx-react-helper": "^0.3.1", "mobx-restful": "^0.7.0-rc.0", "mobx-restful-table": "^2.0.0-rc.1", @@ -36,18 +38,20 @@ "react-bootstrap-editor": "^2.0.4", "react-dom": "^18.3.1", "react-leaflet": "^4.2.1", - "web-utility": "^4.4.0", - "@sentry/nextjs": "^8.24.0" + "undici": "^6.19.7", + "web-utility": "^4.4.0" }, "devDependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-decorators": "^7.24.7", "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", + "@kaiyuanshe/openhackathon-service": "^0.14.0", "@octokit/openapi-types": "^22.2.0", + "@types/jsonwebtoken": "^9.0.6", "@types/leaflet": "^1.9.12", "@types/lodash": "^4.17.7", - "@types/node": "^18.19.43", + "@types/node": "^18.19.44", "@types/react": "^18.3.3", "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", @@ -56,7 +60,7 @@ "husky": "^9.1.4", "less": "^4.2.0", "less-loader": "^12.2.0", - "lint-staged": "^15.2.8", + "lint-staged": "^15.2.9", "next-pwa": "^5.6.0", "next-with-less": "^3.0.1", "prettier": "^3.3.3", @@ -79,7 +83,7 @@ "build": "rm -rf .next && next build", "export": "npm run build && next export", "start": "next start", - "lint": "next lint", + "lint": "next lint && tsc --noEmit", "test": "lint-staged && npm run lint" } } diff --git a/pages/_app.tsx b/pages/_app.tsx index aee68d10..eaa2dea2 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -44,7 +44,7 @@ const MyApp: FC = observer(