Skip to content

Commit

Permalink
feat: team account
Browse files Browse the repository at this point in the history
  • Loading branch information
dreamerblue committed Dec 7, 2024
1 parent 6770c52 commit 238c331
Show file tree
Hide file tree
Showing 19 changed files with 757 additions and 26 deletions.
Binary file modified public/assets/misc/onlinejudge用户导入模板.xlsx
Binary file not shown.
45 changes: 44 additions & 1 deletion src/@types/models.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface ISession {
avatar: string;
permission: number;
permissions: string[];
type: number;
}

interface ISessionStatus {
Expand Down Expand Up @@ -40,6 +41,8 @@ interface IUser {
site?: string;
settings?: any;
verified?: boolean;
type?: number;
status?: number;
coin?: number;
solutionCalendar?: ISolutionCalendar;
defaultLanguage?: string;
Expand Down Expand Up @@ -94,6 +97,46 @@ interface IRatingHistoryItem {

type IRatingHistory = IRatingHistoryItem[];

interface IUserMember {
userId: number;
username: string;
nickname: string;
avatar: string | null;
bannerImage: string;
accepted: number;
submitted: number;
rating: number;
verified: boolean;
status: number;
createdAt: string;
updatedAt: string;
}

interface IUserSelfJoinedTeam {
teamUserId: number;
selfMemberStatus: number;
selfJoinedAt: string;
username: string;
nickname: string;
avatar: string | null;
bannerImage: string;
status: number;
members: {
userId: number;
username: string;
nickname: string;
avatar: string | null;
bannerImage: string;
accepted: number;
submitted: number;
rating: number;
verified: boolean;
status: number;
createdAt: string;
updatedAt: string;
}[];
}

interface IProblem {
problemId: number;
title: string;
Expand All @@ -116,7 +159,7 @@ interface IProblem {
display: boolean;
spj: boolean;
spConfig: any;
alias?: string; /** only in competition */
alias?: string /** only in competition */;
createdAt: ITimestamp;
updatedAt: ITimestamp;
}
Expand Down
2 changes: 1 addition & 1 deletion src/common
95 changes: 95 additions & 0 deletions src/components/AddTeamMemberModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';
import { connect } from 'dva';
import { Form, Modal } from 'antd';
import { FormProps, ReduxProps } from '@/@types/props';
import UserSelect from './UserSelect';

export interface Props extends ReduxProps, FormProps {
invitationCode: string;
confirmLoading: boolean;
onAddMember: (userId: number) => Promise<boolean>;
}

interface State {
visible: boolean;
}

class AddTeamMemberModal extends React.Component<Props, State> {
constructor(props) {
super(props);
this.state = {
visible: false,
};
}

handleOk = () => {
const { form } = this.props;
this.props.form.validateFields((err, values) => {
if (!err) {
const users = values.users;
const userIds = Array.isArray(users) ? users.map((v) => +v.key) : [+users.key];
this.props.onAddMember(userIds[0]).then((success) => {
if (success) {
form.resetFields();
this.handleHideModel();
}
});
}
});
};

handleShowModel = (e) => {
if (e) {
e.stopPropagation();
}
this.setState({ visible: true });
};

handleHideModel = () => {
this.setState({ visible: false });
};

render() {
const { children, confirmLoading, invitationCode, form } = this.props;
const { getFieldDecorator } = form;

return (
<>
<span onClick={this.handleShowModel}>{children}</span>
<Modal
title="Invite Team Member"
visible={this.state.visible}
okText="Submit"
confirmLoading={confirmLoading}
onOk={this.handleOk}
onCancel={this.handleHideModel}
>
<Form layout="vertical" hideRequiredMark={true}>
<Form.Item label="User">
{getFieldDecorator('users', {
rules: [{ required: true, message: 'Please select a user' }],
})(
<UserSelect
placeholder="Input nickname to search"
nameFormat={(u) => `${u.nickname} (UID: ${u.userId})`}
/>,
)}
</Form.Item>
</Form>
<p className="text-secondary mb-sm">
After being invited, the user still need to confirm through your code:
</p>
<p className="text-center text-bold" style={{ fontSize: '18px' }}>
{invitationCode}
</p>
</Modal>
</>
);
}
}

function mapStateToProps(state) {
return {};
}

export default connect(mapStateToProps)(Form.create()(AddTeamMemberModal));
8 changes: 8 additions & 0 deletions src/components/ImportUserModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ExcelSelectParser from './ExcelSelectParser';
import staticUrls from '@/configs/staticUrls';
import constants from '@/configs/constants';
import { withRouter } from 'react-router';
import { EUserType } from '@/common/enums';

export interface Props extends RouteProps, ReduxProps, FormProps {}

Expand All @@ -20,6 +21,7 @@ interface IImportUser {
class: IUser['class'];
grade: string;
password: string;
type: IUser['type'];
}

interface State {
Expand Down Expand Up @@ -49,6 +51,7 @@ class ImportUserModal extends React.Component<Props, State> {
class: `${row[5] || ''}`,
grade: `${row[6] || ''}`,
password: `${row[7] || ''}`,
type: row[8] === 'Y' ? EUserType.team : EUserType.personal,
}));
for (const user of users) {
if (!user.username || !user.nickname || !user.password) {
Expand Down Expand Up @@ -198,6 +201,11 @@ class ImportUserModal extends React.Component<Props, State> {
key="password"
render={(text, record: IImportUser) => <span>{record.password}</span>}
/>
<Table.Column
title="Type"
key="type"
render={(text, record: IImportUser) => <span>{record.type === EUserType.personal ? '个人' : '团队'}</span>}
/>
</Table>
</Collapse.Panel>
</Collapse>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Rating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class Rating extends React.Component<Props, State> {

if (!rating) {
return (
<h3 className="warning-text">No Rating</h3>
<h3 className="warning-text text-secondary">No Rating</h3>
);
}

Expand Down
157 changes: 157 additions & 0 deletions src/components/SelfTeamsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React from 'react';
import { connect } from 'dva';
import { Form, Input, Modal, Table } from 'antd';
import { FormProps, ReduxProps } from '@/@types/props';
import msg from '@/utils/msg';
import tracker from '@/utils/tracker';
import { codeMsgs, Codes } from '@/common/codes';
import TimeBar from './TimeBar';
import UserBar from './UserBar';

export interface Props extends ReduxProps, FormProps {
userId: number;
teams: IUserSelfJoinedTeam[];
confirmJoinLoading: boolean;
}

interface State {
visible: boolean;
invitationCode: string;
}

class SelfTeamsModal extends React.Component<Props, State> {
constructor(props) {
super(props);
this.state = {
visible: false,
invitationCode: '',
};
}

handleConfirmJoinTeam = () => {
const { dispatch, confirmJoinLoading } = this.props;
if (confirmJoinLoading) {
return;
}
const { invitationCode } = this.state;
if (!invitationCode) {
msg.error('Please input invitation code');
return;
}
if (isNaN(+invitationCode)) {
msg.error(codeMsgs[Codes.USER_NOT_INVITED_TO_THIS_TEAM]);
return;
}
dispatch({
type: 'users/confirmJoinTeam',
payload: {
teamUserId: +invitationCode || 0,
},
}).then((ret) => {
msg.auto(ret);
if (ret.success) {
msg.success('Joined successfully');
// this.handleHideModel();
this.setState({ invitationCode: '' });
tracker.event({
category: 'users',
action: 'confirmJoinTeam',
});
dispatch({
type: 'users/getSelfJoinedTeams',
});
}
});
};

handleShowModel = (e) => {
if (e) {
e.stopPropagation();
}
this.setState({ visible: true });
};

handleHideModel = () => {
this.setState({ visible: false });
};

render() {
const { children, loading, teams, form } = this.props;

return (
<>
<span onClick={this.handleShowModel}>{children}</span>
<Modal
title="My Teams"
visible={this.state.visible}
onCancel={this.handleHideModel}
footer={null}
>
<Form layout="vertical" hideRequiredMark={true}>
<Form.Item label="Join an Invited Team">
<Input.Search
enterButton="Join"
placeholder="Invitation code"
className="input-button"
value={this.state.invitationCode}
onChange={(e) => this.setState({ invitationCode: e.target.value })}
onSearch={this.handleConfirmJoinTeam}
/>
</Form.Item>
</Form>

<h4>Joined Teams</h4>
<Table
dataSource={teams}
rowKey="teamUserId"
loading={loading}
pagination={false}
className="responsive-table"
>
<Table.Column
title="Team"
key="team"
render={(text, record: IUserSelfJoinedTeam) => (
<span>
{record.nickname}
<br />
<span className="text-secondary" style={{ fontSize: '12px' }}>Account: {record.username}</span>
</span>
)}
/>
<Table.Column
title="Members"
key="members"
render={(text, record: IUserSelfJoinedTeam) => (
<span>
{record.members.map((member) => (
<UserBar key={member.userId} user={member} hideName useTooltip />
))}
</span>
)}
/>
<Table.Column
title="Joined at"
key="joinedAt"
render={(text, record: IUserSelfJoinedTeam) => (
<span>
<TimeBar time={new Date(record.selfJoinedAt).getTime()} />
</span>
)}
/>
</Table>
</Modal>
</>
);
}
}

function mapStateToProps(state) {
return {
loading: !!state.loading.effects['users/getSelfJoinedTeams'],
teams: state.users.selfJoinedTeams,
confirmJoinLoading: !!state.loading.effects['users/confirmJoinTeam'],
};
}

export default connect(mapStateToProps)(Form.create()(SelfTeamsModal));
Loading

0 comments on commit 238c331

Please sign in to comment.