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
96 changes: 96 additions & 0 deletions components/Team/EvaluationForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Loading } from 'idea-react';
import { computed } from 'mobx';
import { observer } from 'mobx-react';
import { ObservedComponent } from 'mobx-react-helper';
import { FormField, RangeInput } from 'mobx-restful-table';
import { FormEvent } from 'react';
import { Button, Form } from 'react-bootstrap';
import { formToJSON } from 'web-utility';

import { EvaluationModel } from '../../models/Activity/Evaluation';
import { i18n, I18nContext } from '../../models/Base/Translation';
import sessionStore from '../../models/User/Session';

export interface EvaluationFormProps {
activityName: string;
teamId: number;
}

@observer
export class EvaluationForm extends ObservedComponent<EvaluationFormProps, typeof i18n> {
static contextType = I18nContext;

evaluationStore = new EvaluationModel(this.props.activityName, this.props.teamId);

@computed
get evaluatable() {
return !!sessionStore.user && !this.evaluationStore.currentPage[0];
}

async componentDidMount() {
await this.evaluationStore.getStandard();

const { user } = sessionStore;

if (user)
await this.evaluationStore.getList({ createdBy: user.id, team: this.props.teamId }, 1);
}

handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
event.stopPropagation();

const form = formToJSON(event.currentTarget);

await this.evaluationStore.updateOne(form);

alert('Evaluation submitted successfully!');
};

render() {
const { t } = this.observedContext,
{ evaluatable } = this;
const { downloading, uploading, currentStandard, currentPage } = this.evaluationStore;
const { dimensions } = currentStandard,
[{ scores } = {}] = currentPage;
const loading = downloading > 0 || uploading > 0;

return (
<Form className="d-flex flex-column gap-3" onSubmit={this.handleSubmit}>
{loading && <Loading />}

{dimensions.map(({ name, description, maximuScore }) => {
const { score, reason } = scores?.find(({ dimension }) => dimension === name) || {};

return (
<fieldset key={name} name="scores">
<legend>{name}</legend>
<p>{description}</p>

<input type="hidden" name="dimension" value={name} />
<RangeInput
className="text-warning"
icon={value => (value ? '★' : '☆')}
name="score"
max={maximuScore}
required
disabled={!evaluatable}
defaultValue={score?.toString()}
/>
<FormField
label={t('judges_review')}
as="textarea"
rows={3}
disabled={!evaluatable}
defaultValue={reason}
/>
</fieldset>
);
})}
<Button type="submit" variant="primary" disabled={!evaluatable}>
{t('submit')}
</Button>
</Form>
);
}
}
2 changes: 1 addition & 1 deletion components/Team/TeamAwardList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Team } from '@kaiyuanshe/openhackathon-service';
import { ScrollList, ScrollListProps } from 'mobx-restful-table';
import { FC, PureComponent } from 'react';
import { Col, Row } from 'react-bootstrap';

import { Team } from '../../models/Activity/Team';
import { i18n } from '../../models/Base/Translation';
import { XScrollListProps } from '../layout/ScrollList';
import { TeamAwardCard } from './TeamAwardCard';
Expand Down
7 changes: 2 additions & 5 deletions components/Team/TeamList.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { Team } from '@kaiyuanshe/openhackathon-service';
import { ScrollListProps } from 'mobx-restful-table';
import { Col, Row } from 'react-bootstrap';

import { TeamModel } from '../../models/Activity/Team';
import { Team, TeamModel } from '../../models/Activity/Team';
import { TeamCard } from './TeamCard';

export interface TeamListProps extends ScrollListProps<Team> {
store: TeamModel;
}

export const TeamListLayout = ({
defaultData = [],
}: Pick<TeamListProps, 'defaultData'>) => (
export const TeamListLayout = ({ defaultData = [] }: Pick<TeamListProps, 'defaultData'>) => (
<Row className="g-4" xs={1} md={2} lg={2} xxl={2}>
{defaultData.map(item => (
<Col key={item.id}>
Expand Down
47 changes: 47 additions & 0 deletions components/Team/TeamRank.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { UserRankView } from 'idea-react';
import { observer } from 'mobx-react';
import { ObservedComponent } from 'mobx-react-helper';

import { TeamModel } from '../../models/Activity/Team';
import { i18n, I18nContext } from '../../models/Base/Translation';

export interface TeamRankProps {
activityName: string;
teamStore: TeamModel;
}

@observer
export class TeamRank extends ObservedComponent<TeamRankProps, typeof i18n> {
static contextType = I18nContext;

componentDidMount() {
this.props.teamStore.getAll();
}

render() {
const { t } = this.observedContext,
{ activityName, teamStore } = this.props;
const { allItems } = teamStore;

return (
<UserRankView
style={{
// @ts-expect-error remove in React 19
'--logo-image':
'url(https://hackathon-api.static.kaiyuanshe.cn/6342619375fa1817e0f56ce1/2022/10/09/logo22.jpg)',
}}
title={t('hacker_pavilion')}
rank={allItems.map(
({ id, displayName: name, createdBy: { avatar, email }, score = 0 }) => ({
id,
name,
avatar,
email,
score,
}),
)}
linkOf={({ id }) => `/hackathon/${activityName}/team/${id}`}
/>
);
}
}
32 changes: 32 additions & 0 deletions models/Activity/Evaluation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Evaluation, Standard } from '@kaiyuanshe/openhackathon-service';
import { observable } from 'mobx';
import { persist, restore, toggle } from 'mobx-restful';

import { isServer } from '../../configuration';
import { TableModel } from '../Base';

export class EvaluationModel extends TableModel<Evaluation> {
baseURI = '';

constructor(activityName: string, teamId: number) {
super();
this.baseURI = `hackathon/${activityName}/team/${teamId}/evaluation`;
}

@persist()
@observable
accessor currentStandard = {} as Standard;

restored = !isServer() && restore(this, 'Evaluation');

@toggle('downloading')
async getStandard() {
await this.restored;

if (this.currentStandard) return this.currentStandard;

const { body } = await this.client.get<Standard>(`${this.baseURI}/../../standard`);

return (this.currentStandard = body!);
}
}
17 changes: 16 additions & 1 deletion models/Activity/Team.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {
BaseFilter,
Team,
Team as _Team,
TeamMember,
TeamMemberFilter,
TeamWork,
TeamWorkFilter,
Score,
} from '@kaiyuanshe/openhackathon-service';
import { action, computed, observable } from 'mobx';
import { ListModel, persist, restore, Stream, toggle } from 'mobx-restful';
Expand All @@ -15,6 +16,13 @@ import { createListStream, Filter, InputData, TableModel } from '../Base';
import { WorkspaceModel } from '../Git';
import sessionStore from '../User/Session';
import { AwardAssignment } from './Award';
import { EvaluationModel } from './Evaluation';

export interface Team extends _Team {
scores?: Score[];
score?: number;
rank?: number;
}

export type TeamFilter = Filter<Team> & BaseFilter;

Expand Down Expand Up @@ -63,6 +71,13 @@ export class TeamModel extends TableModel<Team, TeamFilter> {
return (this.currentWorkspace = new WorkspaceModel(`${this.baseURI}/${tid}`));
}

@computed
get currentEvaluation() {
const { hackathon, id } = this.currentOne;

return new EvaluationModel(hackathon.name, id);
}

assignmentOf(tid = this.currentOne.id) {
return (this.currentAssignment = new TeamAssignmentModel(`${this.baseURI}/${tid}`));
}
Expand Down
6 changes: 6 additions & 0 deletions models/Activity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ export class ActivityModel extends TableModel<Hackathon, ActivityFilter> {
return (this.currentOrganization = new OrganizerModel(`hackathon/${name}`));
}

static isEvaluatable({ judgeStartedAt, judgeEndedAt }: Hackathon) {
const now = Date.now();

return +new Date(judgeStartedAt) <= now && now <= +new Date(judgeEndedAt);
}

@toggle('uploading')
async updateOne(data: InputData<Hackathon>, name?: string) {
if (!name) {
Expand Down
38 changes: 19 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@giscus/react": "^3.1.0",
"@sentry/nextjs": "^9.24.0",
"@sentry/nextjs": "^9.33.0",
"array-unique-proposal": "^0.3.4",
"classnames": "^2.5.1",
"echarts-jsx": "^0.5.4",
Expand All @@ -31,8 +31,8 @@
"mobx-react-helper": "^0.4.1",
"mobx-restful": "^2.1.0",
"mobx-restful-table": "^2.5.2",
"next": "^15.3.3",
"next-ssr-middleware": "^1.0.0",
"next": "^15.3.4",
"next-ssr-middleware": "^1.0.1",
"open-react-map": "^0.9.0",
"react": "^19.1.0",
"react-bootstrap": "^2.10.10",
Expand All @@ -42,28 +42,28 @@
"web-utility": "^4.4.3"
},
"devDependencies": {
"@babel/core": "^7.27.4",
"@babel/core": "^7.27.7",
"@babel/plugin-proposal-decorators": "^7.27.1",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@cspell/eslint-plugin": "^9.0.2",
"@eslint/compat": "^1.2.9",
"@cspell/eslint-plugin": "^9.1.2",
"@eslint/compat": "^1.3.1",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.28.0",
"@kaiyuanshe/openhackathon-service": "^0.22.0",
"@next/eslint-plugin-next": "^15.3.3",
"@eslint/js": "^9.30.0",
"@kaiyuanshe/openhackathon-service": "^1.0.0-rc.0",
"@next/eslint-plugin-next": "^15.3.4",
"@octokit/openapi-types": "^25.1.0",
"@softonus/prettier-plugin-duplicate-remover": "^1.1.2",
"@stylistic/eslint-plugin": "^4.4.0",
"@stylistic/eslint-plugin": "^5.1.0",
"@types/eslint-config-prettier": "^6.11.3",
"@types/jsonwebtoken": "^9.0.9",
"@types/jsonwebtoken": "^9.0.10",
"@types/koa": "^2.15.0",
"@types/leaflet": "^1.9.18",
"@types/leaflet": "^1.9.19",
"@types/next-pwa": "^5.6.9",
"@types/node": "^22.15.29",
"@types/react": "^19.1.6",
"eslint": "^9.28.0",
"eslint-config-next": "^15.3.3",
"@types/node": "^22.15.34",
"@types/react": "^19.1.8",
"eslint": "^9.30.0",
"eslint-config-next": "^15.3.4",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-simple-import-sort": "^12.1.1",
Expand All @@ -73,13 +73,13 @@
"jiti": "^2.4.2",
"less": "^4.3.0",
"less-loader": "^12.3.0",
"lint-staged": "^16.1.0",
"lint-staged": "^16.1.2",
"next-pwa": "^5.6.0",
"next-with-less": "^3.0.1",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"prettier-plugin-css-order": "^2.1.2",
"typescript": "~5.8.3",
"typescript-eslint": "^8.33.0",
"typescript-eslint": "^8.35.0",
"webpack": "^5.99.9"
},
"pnpm": {
Expand Down
15 changes: 13 additions & 2 deletions pages/activity/[name]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import { cache, compose, errorLogger } from 'next-ssr-middleware';
import { Button, Carousel, Col, Container, Image, Row, Tab, Tabs } from 'react-bootstrap';

import { getActivityStatusText } from '../../../components/Activity/ActivityEntry';
import { AwardList } from '../../../components/Activity/AwardList';
import { CommentBox } from '../../../components/CommentBox';
import { PageHead } from '../../../components/layout/PageHead';
import { AnnouncementList } from '../../../components/Message/MessageList';
import { OrganizationCard } from '../../../components/Organization/OrganizationCard';
import { TeamCard } from '../../../components/Team/TeamCard';
import { TeamCreateModal } from '../../../components/Team/TeamCreateModal';
import { TeamListLayout } from '../../../components/Team/TeamList';
import { TeamRank } from '../../../components/Team/TeamRank';
import { isServer } from '../../../configuration';
import activityStore, { ActivityModel } from '../../../models/Activity';
import { i18n, I18nContext } from '../../../models/Base/Translation';
Expand Down Expand Up @@ -64,6 +66,7 @@ const StatusName = ({ t }: typeof i18n): Record<EnrollmentStatus, string> => ({
export default class ActivityPage extends ObservedComponent<ActivityPageProps, typeof i18n> {
static contextType = I18nContext;

awardStore = activityStore.awardOf(this.props.activity.name);
logStore = activityStore.logOf(this.props.activity.id);
enrollmentStore = activityStore.enrollmentOf(this.props.activity.name);
teamStore = activityStore.teamOf(this.props.activity.name);
Expand Down Expand Up @@ -231,9 +234,9 @@ export default class ActivityPage extends ObservedComponent<ActivityPageProps, t

render() {
const { t } = this.observedContext,
{ name, displayName, tags, banners, location, detail } = this.props.activity,
{ activity, organizationList } = this.props;
const { name, displayName, tags, banners, location, detail } = activity,
{ showCreateTeam, loading } = this,
{ organizationList } = this.props,
myTeam = activityStore.currentTeam?.sessionOne,
myMessage = this.messageStore;

Expand Down Expand Up @@ -302,6 +305,14 @@ export default class ActivityPage extends ObservedComponent<ActivityPageProps, t
/>
</Tab>
</Tabs>
<Tab eventKey="award" title={t('award')} className="pt-2">
<AwardList store={this.awardStore} />
</Tab>
{ActivityModel.isEvaluatable(activity) && (
<Tab eventKey="team-rank" title={t('works_awards')} className="pt-2">
<TeamRank activityName={name} teamStore={this.teamStore} />
</Tab>
)}
</Col>
<Col className="d-flex flex-column">
{organizationList.length > 0 && (
Expand Down
Loading