Skip to content
This repository was archived by the owner on Jul 16, 2024. It is now read-only.

Commit aeda283

Browse files
nichujienichujie
and
nichujie
authoredFeb 21, 2023
feat: record detail page (#121)
* feat: better layout for record list * feat: record cases table * feat: record detail page --------- Co-authored-by: nichujie <chujie@kth.se>
1 parent 69bc5d3 commit aeda283

File tree

28 files changed

+867
-439
lines changed

28 files changed

+867
-439
lines changed
 

‎.eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
"react/sort-comp": "off",
110110
"react/state-in-constructor": "off",
111111
"react/static-property-placement": "off",
112+
"react/no-array-index-key": "off",
112113

113114
"react/boolean-prop-naming": [
114115
"error",

‎public/locales/en/translation.json

+26-17
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,25 @@
4747
"problemUrlNotUnique": "URL already exists"
4848
},
4949
"success": {
50-
"fetchItem": "fetch {data} successfully",
51-
"fetch": "fetch successfully",
52-
"createItem": "create {data} successfully",
53-
"create": "create successfully",
54-
"updateItem": "update {data} successfully",
55-
"update": "update successfully",
56-
"deleteItem": "delete {data} successfully",
57-
"delete": "delete successfully",
50+
"fetchItem": "Fetch {{data}} successfully",
51+
"fetch": "Fetch successfully",
52+
"createItem": "Create {{data}} successfully",
53+
"create": "Create successfully",
54+
"updateItem": "Update {{data}} successfully",
55+
"update": "Update successfully",
56+
"deleteItem": "Delete {{data}} successfully",
57+
"delete": "Delete successfully",
5858
"uploadFile": "Upload success"
5959
},
6060
"error": {
61-
"fetchItem": "failed to fetch {data}",
62-
"fetch": "fetch failed",
63-
"createItem": "failed to create {data}",
64-
"create": "create failed",
65-
"updateItem": "failed to update {data}",
66-
"update": "update failed",
67-
"deleteItem": "failed to delete {data}",
68-
"delete": "delete failed",
61+
"fetchItem": "Failed to fetch {{data}}",
62+
"fetch": "Failed to fetch data",
63+
"createItem": "Failed to create {{data}}",
64+
"create": "Failed to create",
65+
"updateItem": "Failed to update {{data}}",
66+
"update": "Failed to update",
67+
"deleteItem": "Failed to delete {{data}}",
68+
"delete": "Failed to delete",
6969
"uploadFile": "Upload failed"
7070
},
7171
"loading": {
@@ -354,7 +354,9 @@
354354
"language": "Language",
355355
"submitAt": "Submit At",
356356
"problemSet": "Assignment",
357-
"submitter": "Username"
357+
"submitter": "Username",
358+
"submitBy": "Submit By",
359+
"title": "Judge Queue"
358360
},
359361
"PageHeader": {
360362
"problem_detail": "$t(ProblemDetail.menu.detail)",
@@ -409,5 +411,12 @@
409411
"message": "Oops...",
410412
"description": "Error met when sending request."
411413
}
414+
},
415+
"RecordDetail": {
416+
"title": "Submission Detail",
417+
"record": "record",
418+
"state": "Status",
419+
"time": "Time",
420+
"memory": "Memory"
412421
}
413422
}

‎public/locales/zh-CN/translation.json

+19-10
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,24 @@
4747
"problemUrlNotUnique": "URL已经存在"
4848
},
4949
"success": {
50-
"fetchItem": "获取{data}成功",
50+
"fetchItem": "获取{{data}}成功",
5151
"fetch": "获取成功",
52-
"createItem": "创建{data}成功",
52+
"createItem": "创建{{data}}成功",
5353
"create": "创建成功",
54-
"updateItem": "更新{data}成功",
54+
"updateItem": "更新{{data}}成功",
5555
"update": "更新成功",
56-
"deleteItem": "删除{data}成功",
56+
"deleteItem": "删除{{data}}成功",
5757
"delete": "删除成功",
5858
"uploadFile": "上传成功"
5959
},
6060
"error": {
61-
"fetchItem": "获取{data}失败",
62-
"fetch": "获取失败",
63-
"createItem": "创建{data}失败",
61+
"fetchItem": "获取{{data}}失败",
62+
"fetch": "获取数据失败",
63+
"createItem": "创建{{data}}失败",
6464
"create": "创建失败",
65-
"updateItem": "更新{data}失败",
65+
"updateItem": "更新{{data}}失败",
6666
"update": "更新失败",
67-
"deleteItem": "删除{data}失败",
67+
"deleteItem": "删除{{data}}失败",
6868
"delete": "删除失败",
6969
"uploadFile": "上传失败"
7070
},
@@ -353,7 +353,9 @@
353353
"language": "语言",
354354
"submitAt": "提交时间",
355355
"problemSet": "作业",
356-
"submitter": "用户名"
356+
"submitter": "用户名",
357+
"submitBy": "提交者",
358+
"title": "评测队列"
357359
},
358360
"PageHeader": {
359361
"problem_detail": "$t(ProblemDetail.menu.detail)",
@@ -408,5 +410,12 @@
408410
"message": "Oops...",
409411
"description": "请求时出现错误"
410412
}
413+
},
414+
"RecordDetail": {
415+
"title": "评测详情",
416+
"record": "评测记录",
417+
"state": "状态",
418+
"time": "耗时",
419+
"memory": "内存"
411420
}
412421
}

‎src/components/Gravatar/index.tsx

+15-15
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,25 @@ import type React from 'react'
55
import { gravatarImageUrl } from 'utils/gravatar'
66

77
export interface GravatarProps extends AvatarProps {
8-
src?: string
9-
gravatar?: string // If gravatar is set, use gravatar regardless of src
8+
src?: string
9+
gravatar?: string // If gravatar is set, use gravatar regardless of src
1010
}
1111

1212
const Index: React.FC<GravatarProps> = props => {
13-
const { gravatar, src, ...otherProps } = props
14-
const imageUrl: string | undefined = gravatar
15-
? gravatarImageUrl(gravatar, 300)
16-
: src
13+
const { gravatar, src, ...otherProps } = props
14+
const imageUrl: string | undefined = gravatar
15+
? gravatarImageUrl(gravatar, 300)
16+
: src
1717

18-
return (
19-
<Avatar
20-
src={imageUrl}
21-
icon={<UserOutlined />}
22-
alt='avatar'
23-
className='border border-solid border-slate-300'
24-
{...otherProps}
25-
/>
26-
)
18+
return (
19+
<Avatar
20+
src={imageUrl}
21+
icon={<UserOutlined />}
22+
alt='avatar'
23+
className='border border-solid border-slate-300'
24+
{...otherProps}
25+
/>
26+
)
2727
}
2828

2929
export default Index

‎src/components/Profile/ProfileCard.tsx

+63-63
Original file line numberDiff line numberDiff line change
@@ -4,76 +4,76 @@ import Gravatar from 'components/Gravatar'
44
import { useAuth } from 'models'
55
import type React from 'react'
66
import { Link, useNavigate } from 'react-router-dom'
7-
import { VERTICAL_GUTTER } from 'utils/constants'
7+
import { DEFAULT_GUTTER } from 'utils/constants'
88

99
const Index: React.FC = () => {
10-
const auth = useAuth()
11-
const navigate = useNavigate()
10+
const auth = useAuth()
11+
const navigate = useNavigate()
1212

13-
return (
14-
<Row align='middle' justify='center' gutter={VERTICAL_GUTTER}>
15-
<Col span={24}>
16-
<Row justify='center'>
17-
<Tooltip title='Change your avatar' placement='bottom'>
18-
<Link to='/preference/account'>
19-
<Gravatar gravatar={auth.user?.gravatar} size={200} />
20-
</Link>
21-
</Tooltip>
22-
</Row>
23-
</Col>
13+
return (
14+
<Row align='middle' justify='center' gutter={DEFAULT_GUTTER}>
15+
<Col span={24}>
16+
<Row justify='center'>
17+
<Tooltip title='Change your avatar' placement='bottom'>
18+
<Link to='/preference/account'>
19+
<Gravatar gravatar={auth.user?.gravatar} size={200} />
20+
</Link>
21+
</Tooltip>
22+
</Row>
23+
</Col>
2424

25-
<Col span={24}>
26-
<Row align='middle'>
27-
<Col span={24}>
28-
<span className='text-2xl font-semibold'>
29-
{auth.user?.realName ?? auth.user?.username}
30-
</span>
31-
</Col>
32-
<Col span={24}>
33-
<span className='text-lg text-gray-400'>{auth.user?.username}</span>
34-
</Col>
35-
</Row>
36-
</Col>
25+
<Col span={24}>
26+
<Row align='middle'>
27+
<Col span={24}>
28+
<span className='text-2xl font-semibold'>
29+
{auth.user?.realName ?? auth.user?.username}
30+
</span>
31+
</Col>
32+
<Col span={24}>
33+
<span className='text-lg text-gray-400'>{auth.user?.username}</span>
34+
</Col>
35+
</Row>
36+
</Col>
3737

38-
<Col span={24}>
39-
<Button
40-
block
41-
icon={<EditOutlined />}
42-
onClick={(): void => {
43-
navigate('/preference/account')
44-
}}
45-
>
46-
Edit Profile
47-
</Button>
48-
</Col>
38+
<Col span={24}>
39+
<Button
40+
block
41+
icon={<EditOutlined />}
42+
onClick={(): void => {
43+
navigate('/preference/account')
44+
}}
45+
>
46+
Edit Profile
47+
</Button>
48+
</Col>
4949

50-
{auth.user?.email ? (
51-
<Col span={24}>
52-
<Row align='middle' gutter={8}>
53-
<Col>
54-
<MailOutlined className='text-sm' />
55-
</Col>
56-
<Col>
57-
<span className='text-sm'>{auth.user.email}</span>
58-
</Col>
59-
</Row>
60-
</Col>
61-
) : null}
50+
{auth.user?.email ? (
51+
<Col span={24}>
52+
<Row align='middle' gutter={8}>
53+
<Col>
54+
<MailOutlined className='text-sm' />
55+
</Col>
56+
<Col>
57+
<span className='text-sm'>{auth.user.email}</span>
58+
</Col>
59+
</Row>
60+
</Col>
61+
) : null}
6262

63-
{auth.user?.studentId ? (
64-
<Col span={24}>
65-
<Row align='middle' gutter={8}>
66-
<Col>
67-
<ProfileOutlined className='text-sm' />
68-
</Col>
69-
<Col>
70-
<span className='text-sm'>{auth.user.studentId}</span>
71-
</Col>
72-
</Row>
73-
</Col>
74-
) : null}
75-
</Row>
76-
)
63+
{auth.user?.studentId ? (
64+
<Col span={24}>
65+
<Row align='middle' gutter={8}>
66+
<Col>
67+
<ProfileOutlined className='text-sm' />
68+
</Col>
69+
<Col>
70+
<span className='text-sm'>{auth.user.studentId}</span>
71+
</Col>
72+
</Row>
73+
</Col>
74+
) : null}
75+
</Row>
76+
)
7777
}
7878

7979
export default Index
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {
2+
CheckOutlined,
3+
ClockCircleOutlined,
4+
CloseOutlined
5+
} from '@ant-design/icons'
6+
import { Space } from 'antd'
7+
import type React from 'react'
8+
import type { ReactElement } from 'react'
9+
import { RecordCaseResult } from 'utils/service'
10+
11+
interface IProps {
12+
caseResult: RecordCaseResult | undefined
13+
}
14+
15+
interface StatusConfig {
16+
text: string
17+
color: string
18+
icon: ReactElement
19+
}
20+
21+
const RecordCaseStatusConfig: Record<RecordCaseResult, StatusConfig> = {
22+
[RecordCaseResult.Accepted]: {
23+
text: 'Accepted',
24+
color: '#25ad40',
25+
icon: <CheckOutlined />
26+
},
27+
[RecordCaseResult.WrongAnswer]: {
28+
text: 'WA',
29+
color: '#fb5555',
30+
icon: <CloseOutlined />
31+
},
32+
[RecordCaseResult.TimeLimitExceeded]: {
33+
text: 'Time Limit Exceeded',
34+
color: '#fb5555',
35+
icon: <CloseOutlined />
36+
},
37+
[RecordCaseResult.MemoryLimitExceeded]: {
38+
text: 'Memory Limit Exceeded',
39+
color: '#fb5555',
40+
icon: <CloseOutlined />
41+
},
42+
[RecordCaseResult.OutputLimitExceeded]: {
43+
text: 'Output Limit Exceeded',
44+
color: '#fb5555',
45+
icon: <CloseOutlined />
46+
},
47+
[RecordCaseResult.RuntimeError]: {
48+
text: 'Runtime Error',
49+
color: '#fb5555',
50+
icon: <CloseOutlined />
51+
},
52+
[RecordCaseResult.CompileError]: {
53+
text: 'Compile Error',
54+
color: '#fb5555',
55+
icon: <CloseOutlined />
56+
},
57+
[RecordCaseResult.SystemError]: {
58+
text: 'System Error',
59+
color: '#fb5555',
60+
icon: <CloseOutlined />
61+
},
62+
[RecordCaseResult.Canceled]: {
63+
text: 'Cancelled',
64+
color: '#8c8c8c',
65+
icon: <ClockCircleOutlined />
66+
},
67+
[RecordCaseResult.Etc]: {
68+
text: 'ETC',
69+
color: '#8c8c8c',
70+
icon: <ClockCircleOutlined />
71+
}
72+
}
73+
74+
const Index: React.FC<IProps> = ({ caseResult }) => {
75+
const config = caseResult ? RecordCaseStatusConfig[caseResult] : undefined
76+
77+
if (!config) return <span>{caseResult}</span>
78+
79+
return (
80+
<Space style={{ color: config.color }}>
81+
{config.icon}
82+
{config.text}
83+
</Space>
84+
)
85+
}
86+
87+
export default Index

‎src/components/RecordStatus/index.tsx

+18-4
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import { Link } from 'react-router-dom'
1010
import { RecordState } from 'utils/service'
1111

1212
interface IProps {
13-
record: {
13+
record?: {
1414
id: string
1515
state?: RecordState
1616
createdAt?: string
1717
}
1818
domainUrl?: string
19+
size?: 'default' | 'large' | 'small'
1920
}
2021

2122
interface StatusConfig {
@@ -77,13 +78,26 @@ const RecordStatusConfig: Record<RecordState, StatusConfig> = {
7778
}
7879
}
7980

80-
const Index: React.FC<IProps> = ({ record, domainUrl }) => {
81+
const Index: React.FC<IProps> = ({ record, domainUrl, size = 'default' }) => {
82+
const fontClass = {
83+
small: 'text-sm',
84+
large: 'text-lg',
85+
default: undefined
86+
}
87+
const className = fontClass[size]
88+
if (!record) return <div className={className}>N/A</div>
89+
8190
const config = record.state ? RecordStatusConfig[record.state] : undefined
8291
const url = domainUrl ? `/domain/${domainUrl}/record/${record.id}` : record.id
8392

8493
if (!config)
8594
return (
86-
<Link target='_blank' rel='noopener noreferrer' to={url}>
95+
<Link
96+
target='_blank'
97+
rel='noopener noreferrer'
98+
to={url}
99+
className={className}
100+
>
87101
{record.state}
88102
</Link>
89103
)
@@ -95,7 +109,7 @@ const Index: React.FC<IProps> = ({ record, domainUrl }) => {
95109
to={url}
96110
style={{ color: config.color }}
97111
>
98-
<Space>
112+
<Space className={className}>
99113
{config.icon}
100114
{config.text}
101115
</Space>

‎src/components/ShadowCard/index.tsx

+20-14
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,28 @@ import type { CardProps } from 'antd'
22
import { Card } from 'antd'
33
import { merge } from 'lodash-es'
44
import type React from 'react'
5+
import { useMemo } from 'react'
56

6-
const Index: React.FC<CardProps> = props => {
7-
const { children, ...otherProps } = props
7+
const Index: React.FC<CardProps & { noPadding?: boolean }> = props => {
8+
const { children, noPadding = false, ...otherProps } = props
89

9-
return (
10-
<Card
11-
bordered={false}
12-
{...merge(otherProps, {
13-
className: otherProps.className
14-
? `shadow-md ${otherProps.className}`
15-
: 'shadow-md'
16-
})}
17-
>
18-
{children}
19-
</Card>
20-
)
10+
const fullClassName: string = useMemo(() => {
11+
let cls = 'shadow-md overflow-hidden'
12+
if (otherProps.className) cls = `${cls} ${otherProps.className}`
13+
return cls
14+
}, [otherProps.className])
15+
16+
return (
17+
<Card
18+
bordered={false}
19+
{...merge(otherProps, {
20+
className: fullClassName,
21+
bodyStyle: noPadding ? { padding: 0 } : undefined
22+
})}
23+
>
24+
{children}
25+
</Card>
26+
)
2127
}
2228

2329
export default Index

‎src/components/SideMenuPage/index.tsx

+4-10
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { Location } from 'react-router-dom'
1111
import { matchRoutes, useLocation, useNavigate } from 'react-router-dom'
1212
import routes from 'routes'
1313
import type { MenuItemsWithPermission } from 'types'
14-
import { VERTICAL_GUTTER } from 'utils/constants'
14+
import { DEFAULT_GUTTER } from 'utils/constants'
1515

1616
interface IProps {
1717
menuItems: MenuItemsWithPermission
@@ -56,17 +56,11 @@ const Index: React.FC<PropsWithChildren<IProps>> = ({
5656
)
5757

5858
return (
59-
<Row
60-
gutter={[{ xs: 16, sm: 16, lg: 24, xl: 24, xxl: 24 }, VERTICAL_GUTTER[1]]}
61-
>
59+
<Row gutter={DEFAULT_GUTTER}>
6260
<Col xs={24} sm={24} md={8} lg={7} xl={7} xxl={6}>
63-
<Row gutter={VERTICAL_GUTTER}>
61+
<Row gutter={DEFAULT_GUTTER}>
6462
<Col span={24}>
65-
<ShadowCard
66-
bodyStyle={{ padding: 0 }}
67-
style={{ overflow: 'hidden' }}
68-
className='w-full'
69-
>
63+
<ShadowCard noPadding className='w-full'>
7064
<AccessMenu
7165
mode='inline'
7266
items={menuItems}

‎src/components/SidePage/index.tsx

+27-20
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,37 @@
11
import { Col, Row } from 'antd'
2-
import ShadowCard from 'components/ShadowCard'
32
import React from 'react'
4-
import { VERTICAL_GUTTER } from 'utils/constants'
3+
import { DEFAULT_GUTTER } from 'utils/constants'
54

65
interface IProperties {
7-
children: React.ReactNode | React.ReactNode[]
8-
extra: React.ReactNode | React.ReactNode[] // Extra component on the side
9-
position?: 'left' | 'right' // On left or right side extra component will be placed
6+
children: React.ReactNode | React.ReactNode[]
7+
extra: React.ReactNode | React.ReactNode[] // Extra component on the side
8+
position?: 'left' | 'right' // On left or right side extra component will be placed
109
}
1110

1211
const Index: React.FC<IProperties> = ({ children, extra, position }) => {
13-
const childrenOrder = position && position === 'left' ? 1 : 0
14-
return (
15-
<Row gutter={[{ lg: 24, xl: 24, md: 24, sm: 24 }, VERTICAL_GUTTER[1]]}>
16-
<Col xs={24} sm={24} md={16} xl={16} xxl={18} order={childrenOrder}>
17-
{React.Children.map(children, c => (
18-
<ShadowCard>{c}</ShadowCard>
19-
))}
20-
</Col>
21-
<Col xs={24} sm={24} md={8} xl={8} xxl={6}>
22-
{React.Children.map(extra, c => (
23-
<ShadowCard>{c}</ShadowCard>
24-
))}
25-
</Col>
26-
</Row>
27-
)
12+
const childrenOrder = position && position === 'left' ? 1 : 0
13+
return (
14+
<Row gutter={DEFAULT_GUTTER}>
15+
<Col xs={24} sm={24} md={16} xl={16} xxl={18} order={childrenOrder}>
16+
<Row gutter={DEFAULT_GUTTER}>
17+
{React.Children.map(children, (c, index) => (
18+
<Col span={24} key={`main-${index}`}>
19+
{c}
20+
</Col>
21+
))}
22+
</Row>
23+
</Col>
24+
<Col xs={24} sm={24} md={8} xl={8} xxl={6}>
25+
<Row gutter={DEFAULT_GUTTER}>
26+
{React.Children.map(extra, (c, index) => (
27+
<Col span={24} key={`extra-${index}`}>
28+
{c}
29+
</Col>
30+
))}
31+
</Row>
32+
</Col>
33+
</Row>
34+
)
2835
}
2936

3037
export default Index

‎src/components/UserBadge/index.tsx

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useRequest } from 'ahooks'
2+
import { Space } from 'antd'
3+
import Gravatar from 'components/Gravatar'
4+
import type React from 'react'
5+
import { Link } from 'react-router-dom'
6+
import Horse from 'utils/service'
7+
8+
export interface UserBadgeProps {
9+
userId: string
10+
}
11+
12+
const Index: React.FC<UserBadgeProps> = ({ userId }) => {
13+
const { data: user, loading } = useRequest(async () => {
14+
const res = await Horse.user.v1GetUser(userId)
15+
return res.data.data
16+
})
17+
18+
if (loading) return <span>Loading...</span> // TODO: better loading
19+
20+
if (!user) return <span>N/A</span>
21+
22+
return (
23+
<Space>
24+
<Gravatar gravatar={user.gravatar} size={20} />
25+
<Link to={`/user/${user.id}`}>{user.username}</Link>
26+
</Space>
27+
)
28+
}
29+
30+
export default Index

‎src/pages/DomainHome/index.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { ReactElement } from 'react'
1111
import { useMemo } from 'react'
1212
import { useTranslation } from 'react-i18next'
1313
import { useNavigate, useParams } from 'react-router-dom'
14-
import { VERTICAL_GUTTER } from 'utils/constants'
14+
import { DEFAULT_GUTTER } from 'utils/constants'
1515
import { NoDomainUrlError } from 'utils/exception'
1616

1717
const Index: React.FC = () => {
@@ -64,7 +64,7 @@ const Index: React.FC = () => {
6464
return (
6565
<>
6666
<Head title={domain?.name ?? 'Domain Home'} />
67-
<Row align='middle' justify='center' gutter={VERTICAL_GUTTER}>
67+
<Row align='middle' justify='center' gutter={DEFAULT_GUTTER}>
6868
<Col span={24}>
6969
<ShadowCard>{domainInfo}</ShadowCard>
7070
</Col>
@@ -84,7 +84,7 @@ const Index: React.FC = () => {
8484
</Button>
8585
) : null
8686
}
87-
bodyStyle={{ padding: 0 }}
87+
noPadding
8888
>
8989
<ProblemSetList domainUrl={domainUrl} recent={5} />
9090
</ShadowCard>

‎src/pages/DomainList/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const Index: React.FC = () => {
2828
return (
2929
<>
3030
<Head title='Home' />
31-
<ShadowCard bodyStyle={{ padding: 0 }}>
31+
<ShadowCard noPadding>
3232
<List
3333
loading={loading}
3434
itemLayout='horizontal'

‎src/pages/ProblemDetail/Settings/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type React from 'react'
66
import { useCallback, useMemo, useState } from 'react'
77
import { useTranslation } from 'react-i18next'
88
import { useParams } from 'react-router-dom'
9-
import { VERTICAL_GUTTER } from 'utils/constants'
9+
import { DEFAULT_GUTTER } from 'utils/constants'
1010
import { NoDomainUrlError, NoProblemIdError } from 'utils/exception'
1111
import Horse from 'utils/service'
1212
import Config from './Config'
@@ -112,7 +112,7 @@ const Index: React.FC = () => {
112112
}, [tabKey, refresh, listObjectsRequest, configRequest])
113113

114114
return (
115-
<Row gutter={VERTICAL_GUTTER}>
115+
<Row gutter={DEFAULT_GUTTER}>
116116
<Col span={24}>
117117
<Overview
118118
domainUrl={domainUrl}

‎src/pages/ProblemSetDetail/ProblemList/index.tsx

+70-70
Original file line numberDiff line numberDiff line change
@@ -7,84 +7,84 @@ import { NoDomainUrlError, NoProblemSetIdError } from 'utils/exception'
77
import type { ProblemPreviewWithLatestRecord } from 'utils/service'
88

99
interface IProps {
10-
problems: ProblemPreviewWithLatestRecord[] | undefined
10+
problems: ProblemPreviewWithLatestRecord[] | undefined
1111
}
1212

1313
const Index: React.FC<IProps> = ({ problems }) => {
14-
const { t } = useTranslation()
15-
const navigate = useNavigate()
16-
const { domainUrl, problemSetId } =
17-
useParams<{ problemSetId: string; domainUrl: string }>()
14+
const { t } = useTranslation()
15+
const navigate = useNavigate()
16+
const { domainUrl, problemSetId } =
17+
useParams<{ problemSetId: string; domainUrl: string }>()
1818

19-
// const a = (
20-
// <List
21-
// itemLayout="horizontal"
22-
// size="large"
23-
// dataSource={problems ?? []}
24-
// renderItem={(item) => (
25-
// <List.Item>
26-
// <Link to={`/domain/${domainUrl}/problem/${item.id ?? ''}`}>
27-
// <strong>{item.title}</strong>
28-
// </Link>
29-
// </List.Item>
30-
// )}
31-
// />
32-
// );
19+
// const a = (
20+
// <List
21+
// itemLayout="horizontal"
22+
// size="large"
23+
// dataSource={problems ?? []}
24+
// renderItem={(item) => (
25+
// <List.Item>
26+
// <Link to={`/domain/${domainUrl}/problem/${item.id ?? ''}`}>
27+
// <strong>{item.title}</strong>
28+
// </Link>
29+
// </List.Item>
30+
// )}
31+
// />
32+
// );
3333

34-
if (!domainUrl) {
35-
throw new NoDomainUrlError()
36-
}
34+
if (!domainUrl) {
35+
throw new NoDomainUrlError()
36+
}
3737

38-
if (!problemSetId) {
39-
throw new NoProblemSetIdError()
40-
}
38+
if (!problemSetId) {
39+
throw new NoProblemSetIdError()
40+
}
4141

42-
const columns = [
43-
{
44-
title: t('ProblemSetDetail.problem.status'),
45-
dataIndex: 'recordState',
46-
width: 120,
47-
render: (_: unknown, row: ProblemPreviewWithLatestRecord) =>
48-
row.latestRecord?.state
49-
},
50-
{
51-
title: t('ProblemSetDetail.problem'),
52-
dataIndex: 'title',
53-
render: (_: unknown, row: ProblemPreviewWithLatestRecord) => (
54-
<Link
55-
to={`/domain/${domainUrl}/problem-set/${problemSetId}/p/${
56-
row.url ?? row.id
57-
}`}
58-
>
59-
{row.title}
60-
</Link>
61-
)
62-
}
63-
]
42+
const columns = [
43+
{
44+
title: t('ProblemSetDetail.problem.status'),
45+
dataIndex: 'recordState',
46+
width: 120,
47+
render: (_: unknown, row: ProblemPreviewWithLatestRecord) =>
48+
row.latestRecord?.state
49+
},
50+
{
51+
title: t('ProblemSetDetail.problem'),
52+
dataIndex: 'title',
53+
render: (_: unknown, row: ProblemPreviewWithLatestRecord) => (
54+
<Link
55+
to={`/domain/${domainUrl}/problem-set/${problemSetId}/p/${
56+
row.url ?? row.id
57+
}`}
58+
>
59+
{row.title}
60+
</Link>
61+
)
62+
}
63+
]
6464

65-
return isArray(problems) && problems.length > 0 ? (
66-
<Table
67-
rowKey='id'
68-
columns={columns}
69-
dataSource={problems}
70-
pagination={false}
71-
/>
72-
) : (
73-
<Empty description={<span>There are no problems</span>}>
74-
<Space>
75-
<Button
76-
type='primary'
77-
onClick={(): void => {
78-
navigate(
79-
`/domain/${domainUrl}/problem-set/${problemSetId}/settings`
80-
)
81-
}}
82-
>
83-
Add or Clone
84-
</Button>
85-
</Space>
86-
</Empty>
87-
)
65+
return isArray(problems) && problems.length > 0 ? (
66+
<Table
67+
rowKey='id'
68+
columns={columns}
69+
dataSource={problems}
70+
pagination={false}
71+
/>
72+
) : (
73+
<Empty description={<span>There are no problems</span>}>
74+
<Space>
75+
<Button
76+
type='primary'
77+
onClick={(): void => {
78+
navigate(
79+
`/domain/${domainUrl}/problem-set/${problemSetId}/settings`
80+
)
81+
}}
82+
>
83+
Add or Clone
84+
</Button>
85+
</Space>
86+
</Empty>
87+
)
8888
}
8989

9090
export default Index

‎src/pages/ProblemSetDetail/Settings/index.tsx

+90-90
Original file line numberDiff line numberDiff line change
@@ -7,107 +7,107 @@ import { useState } from 'react'
77
import { useParams } from 'react-router-dom'
88
import type { ProTablePagination } from 'types'
99
import { transPagination } from 'utils'
10-
import { VERTICAL_GUTTER } from 'utils/constants'
10+
import { DEFAULT_GUTTER } from 'utils/constants'
1111
import { NoDomainUrlError, NoProblemIdError } from 'utils/exception'
1212
import Horse from 'utils/service'
1313
import AddExistProblem from './AddExistProblem'
1414
import DraggableProblemTable from './DraggableProblemTable'
1515

1616
const Index: React.FC = () => {
17-
const [tab, setTab] = useState('tab1')
18-
const { domainUrl, problemSetId } =
19-
useParams<{ domainUrl: string; problemSetId: string }>()
17+
const [tab, setTab] = useState('tab1')
18+
const { domainUrl, problemSetId } =
19+
useParams<{ domainUrl: string; problemSetId: string }>()
2020

21-
if (!domainUrl) {
22-
throw new NoDomainUrlError()
23-
}
21+
if (!domainUrl) {
22+
throw new NoDomainUrlError()
23+
}
2424

25-
if (!problemSetId) {
26-
throw new NoProblemIdError()
27-
}
25+
if (!problemSetId) {
26+
throw new NoProblemIdError()
27+
}
2828

29-
const {
30-
data: problemSet,
31-
refresh: refreshProblemSet,
32-
loading: fetchingProblemSet
33-
} = useRequest(
34-
async () => {
35-
const res = await Horse.problemSet.v1GetProblemSet(
36-
domainUrl,
37-
problemSetId
38-
)
39-
return res.data.data
40-
},
41-
{
42-
onError: () => {
43-
message.error('failed to fetch domain info')
44-
}
45-
}
46-
)
29+
const {
30+
data: problemSet,
31+
refresh: refreshProblemSet,
32+
loading: fetchingProblemSet
33+
} = useRequest(
34+
async () => {
35+
const res = await Horse.problemSet.v1GetProblemSet(
36+
domainUrl,
37+
problemSetId
38+
)
39+
return res.data.data
40+
},
41+
{
42+
onError: () => {
43+
message.error('failed to fetch domain info')
44+
}
45+
}
46+
)
4747

48-
const {
49-
runAsync: fetchProblems,
50-
refresh: refreshProblems,
51-
loading: fetchingProblems
52-
} = useRequest(
53-
async (parameters: ProTablePagination) => {
54-
const res = await Horse.problem.v1ListProblems(domainUrl, {
55-
...transPagination(parameters),
56-
ordering: '-created_at'
57-
})
58-
return res.data.data ?? { count: 0, results: [] }
59-
},
60-
{
61-
manual: true
62-
}
63-
)
48+
const {
49+
runAsync: fetchProblems,
50+
refresh: refreshProblems,
51+
loading: fetchingProblems
52+
} = useRequest(
53+
async (parameters: ProTablePagination) => {
54+
const res = await Horse.problem.v1ListProblems(domainUrl, {
55+
...transPagination(parameters),
56+
ordering: '-created_at'
57+
})
58+
return res.data.data ?? { count: 0, results: [] }
59+
},
60+
{
61+
manual: true
62+
}
63+
)
6464

65-
return (
66-
<Row gutter={VERTICAL_GUTTER}>
67-
<Col span={24}>
68-
<ShadowCard bodyStyle={{ padding: 0 }}>
69-
<ProCard split='vertical'>
70-
<ProCard
71-
title='Problem List'
72-
colSpan='30%'
73-
bodyStyle={{ padding: 16 }}
74-
>
75-
<DraggableProblemTable
76-
domainUrl={domainUrl}
77-
problemSetId={problemSetId}
78-
problems={problemSet?.problems ?? []}
79-
loading={fetchingProblemSet}
80-
onDeleteSuccess={() => {
81-
refreshProblemSet()
82-
refreshProblems()
83-
}}
84-
onUpdateFinish={() => refreshProblemSet()}
85-
/>
86-
</ProCard>
87-
<ProCard
88-
tabs={{
89-
activeKey: tab,
90-
onChange: setTab,
91-
animated: { inkBar: true, tabPane: true }
92-
}}
93-
>
94-
<ProCard.TabPane key='tab1' tab='Add Existed'>
95-
<AddExistProblem
96-
fetchingProblems={fetchingProblems}
97-
onAddSuccess={refreshProblemSet}
98-
fetchProblems={fetchProblems}
99-
problemIdList={problemSet?.problems?.map(p => p.id) ?? []}
100-
/>
101-
</ProCard.TabPane>
102-
<ProCard.TabPane key='tab2' tab='Clone'>
103-
TODO: Clone
104-
</ProCard.TabPane>
105-
</ProCard>
106-
</ProCard>
107-
</ShadowCard>
108-
</Col>
109-
</Row>
110-
)
65+
return (
66+
<Row gutter={DEFAULT_GUTTER}>
67+
<Col span={24}>
68+
<ShadowCard noPadding>
69+
<ProCard split='vertical'>
70+
<ProCard
71+
title='Problem List'
72+
colSpan='30%'
73+
bodyStyle={{ padding: 16 }}
74+
>
75+
<DraggableProblemTable
76+
domainUrl={domainUrl}
77+
problemSetId={problemSetId}
78+
problems={problemSet?.problems ?? []}
79+
loading={fetchingProblemSet}
80+
onDeleteSuccess={() => {
81+
refreshProblemSet()
82+
refreshProblems()
83+
}}
84+
onUpdateFinish={() => refreshProblemSet()}
85+
/>
86+
</ProCard>
87+
<ProCard
88+
tabs={{
89+
activeKey: tab,
90+
onChange: setTab,
91+
animated: { inkBar: true, tabPane: true }
92+
}}
93+
>
94+
<ProCard.TabPane key='tab1' tab='Add Existed'>
95+
<AddExistProblem
96+
fetchingProblems={fetchingProblems}
97+
onAddSuccess={refreshProblemSet}
98+
fetchProblems={fetchProblems}
99+
problemIdList={problemSet?.problems?.map(p => p.id) ?? []}
100+
/>
101+
</ProCard.TabPane>
102+
<ProCard.TabPane key='tab2' tab='Clone'>
103+
TODO: Clone
104+
</ProCard.TabPane>
105+
</ProCard>
106+
</ProCard>
107+
</ShadowCard>
108+
</Col>
109+
</Row>
110+
)
111111
}
112112

113113
export default Index

‎src/pages/ProblemSetDetail/ViewDetail/index.tsx

+30-36
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,41 @@ import ShadowCard from 'components/ShadowCard'
44
import { useProblemSet } from 'models'
55
import type React from 'react'
66
import { useTranslation } from 'react-i18next'
7-
import { VERTICAL_GUTTER } from 'utils/constants'
7+
import { DEFAULT_GUTTER } from 'utils/constants'
88
import ProblemList from '../ProblemList'
99

1010
const Index: React.FC = () => {
11-
const { t } = useTranslation()
12-
const { problemSet, loading } = useProblemSet()
11+
const { t } = useTranslation()
12+
const { problemSet, loading } = useProblemSet()
1313

14-
return (
15-
<Row gutter={VERTICAL_GUTTER}>
16-
{problemSet?.content ? (
17-
<Col span={24}>
18-
<ShadowCard
19-
loading={loading}
20-
title={t('ProblemSetDetail.introduction')}
21-
>
22-
<Spin spinning={!problemSet}>
23-
<Typography>
24-
<MarkdownRender>{problemSet.content}</MarkdownRender>
25-
</Typography>
26-
</Spin>
27-
</ShadowCard>
28-
</Col>
29-
) : null}
14+
return (
15+
<Row gutter={DEFAULT_GUTTER}>
16+
{problemSet?.content ? (
17+
<Col span={24}>
18+
<ShadowCard
19+
loading={loading}
20+
title={t('ProblemSetDetail.introduction')}
21+
>
22+
<Spin spinning={!problemSet}>
23+
<Typography>
24+
<MarkdownRender>{problemSet.content}</MarkdownRender>
25+
</Typography>
26+
</Spin>
27+
</ShadowCard>
28+
</Col>
29+
) : null}
3030

31-
<Col span={24}>
32-
<ShadowCard
33-
loading={loading}
34-
title={t('ProblemSetDetail.problem')}
35-
bodyStyle={
36-
problemSet?.problems && problemSet.problems.length > 0
37-
? {
38-
padding: 0
39-
}
40-
: undefined
41-
}
42-
>
43-
<ProblemList problems={problemSet?.problems} />
44-
</ShadowCard>
45-
</Col>
46-
</Row>
47-
)
31+
<Col span={24}>
32+
<ShadowCard
33+
loading={loading}
34+
title={t('ProblemSetDetail.problem')}
35+
noPadding={problemSet?.problems && problemSet.problems.length > 0}
36+
>
37+
<ProblemList problems={problemSet?.problems} />
38+
</ShadowCard>
39+
</Col>
40+
</Row>
41+
)
4842
}
4943

5044
export default Index

‎src/pages/ProblemSetList/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const Index: React.FC = () => {
3737
</Button>
3838
) : null
3939
}
40-
bodyStyle={{ padding: 0 }}
40+
noPadding
4141
>
4242
<ProblemSetList domainUrl={domainUrl} />
4343
</ShadowCard>

‎src/pages/RecordDetail/index.tsx

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { useRequest } from 'ahooks'
2+
import { Table } from 'antd'
3+
import type { ColumnsType } from 'antd/es/table'
4+
import Head from 'components/Head'
5+
import RecordCaseStatus from 'components/RecordCaseStatus'
6+
import RecordStatus from 'components/RecordStatus'
7+
import ShadowCard from 'components/ShadowCard'
8+
import SidePage from 'components/SidePage'
9+
import UserBadge from 'components/UserBadge'
10+
import dayjs from 'dayjs'
11+
import { useMessage } from 'hooks'
12+
import { isNumber } from 'lodash-es'
13+
import type React from 'react'
14+
import { useMemo } from 'react'
15+
import { useTranslation } from 'react-i18next'
16+
import { Link, useParams } from 'react-router-dom'
17+
import { memoryKb2String, timeMs2String } from 'utils'
18+
import { NoDomainUrlError, NoRecordIdError } from 'utils/exception'
19+
import type { RecordCase } from 'utils/service'
20+
import Horse from 'utils/service'
21+
22+
const Index: React.FC = () => {
23+
const { t } = useTranslation()
24+
const msg = useMessage()
25+
const { domainUrl, recordId } =
26+
useParams<{ domainUrl: string; recordId: string }>()
27+
28+
if (!domainUrl) {
29+
throw new NoDomainUrlError()
30+
}
31+
32+
if (!recordId) {
33+
throw new NoRecordIdError()
34+
}
35+
36+
const columns: ColumnsType<RecordCase> = [
37+
{
38+
title: '#',
39+
render: (_text, _record, index) => <span>#{index + 1}</span>,
40+
width: 70
41+
},
42+
{
43+
title: t('RecordDetail.state'),
44+
dataIndex: 'state',
45+
key: 'state',
46+
render: (_value, record) => <RecordCaseStatus caseResult={record.state} />
47+
},
48+
{
49+
title: t('RecordDetail.time'),
50+
dataIndex: 'timeMs',
51+
key: 'timeMs',
52+
width: 100,
53+
render: value => (isNumber(value) ? timeMs2String(value) : 'N/A')
54+
},
55+
{
56+
title: t('RecordDetail.memory'),
57+
dataIndex: 'memoryKb',
58+
key: 'memoryKb',
59+
width: 100,
60+
render: value => (isNumber(value) ? memoryKb2String(value) : 'N/A')
61+
}
62+
]
63+
64+
const { data: record, loading } = useRequest(
65+
async () => {
66+
const response = await Horse.record.v1GetRecord(recordId, domainUrl)
67+
return response.data.data
68+
},
69+
{
70+
onError: () => {
71+
msg.error.fetch(t('RecordDetail.record'))
72+
}
73+
}
74+
)
75+
76+
const problemLink = useMemo(() => {
77+
if (record?.problemId && record.problemSetId)
78+
return (
79+
<Link
80+
to={`/domain/${domainUrl}/problem-set/${record.problemSetId}/problem/${record.problemId}`}
81+
>
82+
{record.problemId}
83+
</Link>
84+
)
85+
if (record?.problemId)
86+
return (
87+
<Link to={`/domain/${domainUrl}/problem/${record.problemId}`}>
88+
{record.problemId}
89+
</Link>
90+
)
91+
return 'N/A'
92+
}, [domainUrl, record?.problemId, record?.problemSetId])
93+
94+
const sideInfo = (
95+
<ShadowCard
96+
loading={loading}
97+
title={
98+
<h3 className='m-0 text-lg font-medium leading-6 text-gray-900'>
99+
{t('RecordDetail.title')}
100+
</h3>
101+
}
102+
>
103+
<dl>
104+
<div className='py-1 sm:grid sm:grid-cols-3 sm:gap-4'>
105+
<dt className='text-sm font-medium text-gray-500'>Submit By</dt>
106+
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>
107+
{record?.committerId ? (
108+
<UserBadge userId={record.committerId} />
109+
) : (
110+
'N/A'
111+
)}
112+
</dd>
113+
</div>
114+
<div className='py-1 sm:grid sm:grid-cols-3 sm:gap-4'>
115+
<dt className='text-sm font-medium text-gray-500'>Problem</dt>
116+
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>
117+
{problemLink}
118+
</dd>
119+
</div>
120+
{record?.problemSetId ? (
121+
<div className='py-1 sm:grid sm:grid-cols-3 sm:gap-4'>
122+
<dt className='text-sm font-medium text-gray-500'>Assignment</dt>
123+
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>
124+
<Link
125+
to={`/domain/${domainUrl}/problem-set/${record.problemSetId}`}
126+
>
127+
{record.problemSetId}
128+
</Link>
129+
</dd>
130+
</div>
131+
) : null}
132+
<div className='py-1 sm:grid sm:grid-cols-3 sm:gap-4'>
133+
<dt className='text-sm font-medium text-gray-500'>Language</dt>
134+
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>
135+
{record?.language}
136+
</dd>
137+
</div>
138+
<div className='py-1 sm:grid sm:grid-cols-3 sm:gap-4'>
139+
<dt className='text-sm font-medium text-gray-500'>Submit At</dt>
140+
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>
141+
{record?.createdAt
142+
? dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss')
143+
: 'N/A'}
144+
</dd>
145+
</div>
146+
<div className='py-1 sm:grid sm:grid-cols-3 sm:gap-4'>
147+
<dt className='text-sm font-medium text-gray-500'>Judged At</dt>
148+
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>
149+
{record?.judgedAt
150+
? dayjs(record.judgedAt).format('YYYY-MM-DD HH:mm:ss')
151+
: 'N/A'}
152+
</dd>
153+
</div>
154+
</dl>
155+
<dl className='m-0'>
156+
<div className='py-1 sm:grid sm:grid-cols-3 sm:gap-4'>
157+
<dt className='text-sm font-medium text-gray-500'>Score</dt>
158+
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>
159+
{record?.score ?? 'N/A'}
160+
</dd>
161+
</div>
162+
<div className='py-1 sm:grid sm:grid-cols-3 sm:gap-4'>
163+
<dt className='text-sm font-medium text-gray-500'>Total Time</dt>
164+
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>
165+
{record?.timeMs === undefined
166+
? 'N/A'
167+
: timeMs2String(record.timeMs)}
168+
</dd>
169+
</div>
170+
<div className='py-1 sm:grid sm:grid-cols-3 sm:gap-4'>
171+
<dt className='text-sm font-medium text-gray-500'>Peak Memory</dt>
172+
<dd className='mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0'>
173+
{record?.memoryKb === undefined
174+
? 'N/A'
175+
: memoryKb2String(record.memoryKb)}
176+
</dd>
177+
</div>
178+
</dl>
179+
</ShadowCard>
180+
)
181+
182+
return (
183+
<div>
184+
<Head title={t('RecordDetail.title')} />
185+
<SidePage extra={sideInfo}>
186+
<ShadowCard
187+
noPadding={!loading}
188+
loading={loading}
189+
title={
190+
loading ? (
191+
t('RecordDetail.title')
192+
) : (
193+
<RecordStatus
194+
domainUrl={domainUrl}
195+
record={record}
196+
size='large'
197+
/>
198+
)
199+
}
200+
>
201+
<Table
202+
columns={columns}
203+
dataSource={record?.cases}
204+
loading={loading}
205+
/>
206+
</ShadowCard>
207+
</SidePage>
208+
</div>
209+
)
210+
}
211+
212+
export default Index

‎src/pages/RecordList/index.tsx

+31-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ActionType, ProColumns } from '@ant-design/pro-table'
22
import ProTable from '@ant-design/pro-table'
33
import { useRequest } from 'ahooks'
44
import { message } from 'antd'
5+
import Head from 'components/Head'
56
import RecordStatus from 'components/RecordStatus'
67
import { isNumber, omit, omitBy } from 'lodash-es'
78
import type React from 'react'
@@ -52,13 +53,14 @@ const Index: React.FC = () => {
5253
{
5354
title: t('RecordList.status'),
5455
dataIndex: 'state',
55-
width: 120,
56+
width: 140,
5657
search: false,
58+
className: 'record-first-col',
5759
render: (text, record) => <RecordStatus record={record} />
5860
},
5961
{
6062
title: t('RecordList.problem'),
61-
dataIndex: 'problemTitle',
63+
dataIndex: 'problemId',
6264
ellipsis: true,
6365
search: false,
6466
render: (_, record) => {
@@ -88,29 +90,49 @@ const Index: React.FC = () => {
8890
dataIndex: 'timeMs',
8991
width: 80,
9092
search: false,
93+
responsive: ['lg'],
9194
render: (_, record) =>
92-
isNumber(record.timeMs) ? `${record.timeMs} ms` : 'N/A'
95+
isNumber(record.timeMs) ? `${record.timeMs} ms` : 'N/A' // TODO: calculation
9396
},
9497
{
9598
title: t('RecordList.memory'),
9699
dataIndex: 'memoryKb',
97100
width: 80,
98101
search: false,
102+
responsive: ['lg'],
99103
render: (_, record) =>
100-
isNumber(record.memoryKb) ? `${record.memoryKb} KB` : 'N/A'
104+
isNumber(record.memoryKb) ? `${record.memoryKb} KB` : 'N/A' // TODO: calculation
101105
},
102106
{
103107
title: t('RecordList.language'),
104108
dataIndex: 'language',
109+
width: 90,
110+
search: false,
111+
responsive: ['md']
112+
},
113+
{
114+
title: t('RecordList.submitBy'),
115+
dataIndex: 'committerId',
116+
ellipsis: true,
117+
search: false,
105118
width: 100,
106-
search: false
119+
render: (_, record) => {
120+
if (!record.committerId) return record.committerUsername ?? '-'
121+
122+
return (
123+
<Link to={`/user/${record.committerId}`}>
124+
{record.committerUsername ?? record.committerId}
125+
</Link>
126+
)
127+
}
107128
},
108129
{
109130
title: t('RecordList.submitAt'),
110131
dataIndex: 'createdAt',
111132
valueType: 'dateTime',
112133
width: 180,
113-
search: false
134+
search: false,
135+
responsive: ['md']
114136
},
115137
{
116138
title: t('RecordList.problem'),
@@ -133,6 +155,7 @@ const Index: React.FC = () => {
133155

134156
return (
135157
<div id='record-list-table'>
158+
<Head title={t('RecordList.title')} />
136159
<ProTable<RecordListDetail, QueryParams>
137160
columns={columns}
138161
actionRef={actionRef}
@@ -153,6 +176,8 @@ const Index: React.FC = () => {
153176
form={{ syncToUrl: true }}
154177
pagination={{ pageSize: 20 }}
155178
dateFormatter='string'
179+
toolBarRender={false}
180+
size='small'
156181
/>
157182
</div>
158183
)

‎src/pages/RecordList/style.css

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
border-radius: 6px;
55
}
66

7-
#record-list-table .ant-card {
7+
#record-list-table .ant-pro-card {
88
@apply shadow-md;
99
}
10+
11+
.record-first-col {
12+
padding-left: 16px !important;
13+
}

‎src/pages/UserSettings/Account/AvatarUpload.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useAuth } from 'models'
77
import type React from 'react'
88
import type { ChangeEvent } from 'react'
99
import { useMemo, useState } from 'react'
10-
import { VERTICAL_GUTTER } from 'utils/constants'
10+
import { DEFAULT_GUTTER } from 'utils/constants'
1111
import Horse, { ErrorCode } from 'utils/service'
1212

1313
const AvatarUpload: React.FC = () => {
@@ -77,7 +77,7 @@ const AvatarUpload: React.FC = () => {
7777

7878
return (
7979
<>
80-
<Row justify='center' gutter={VERTICAL_GUTTER}>
80+
<Row justify='center' gutter={DEFAULT_GUTTER}>
8181
<Col span={24}>
8282
<Row justify='center'>
8383
<Gravatar gravatar={user?.gravatar} size={150} />
+29-29
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,40 @@
11
import { Button, Card, Col, Form, Input, Row } from 'antd'
22
import { useAuth } from 'models'
33
import type React from 'react'
4-
import { VERTICAL_GUTTER } from 'utils/constants'
4+
import { DEFAULT_GUTTER } from 'utils/constants'
55
import AvatarUpload from './AvatarUpload'
66

77
const Index: React.FC = () => {
8-
const { user } = useAuth()
8+
const { user } = useAuth()
99

10-
return (
11-
<Card
12-
title={<span className='text-2xl font-semibold'>Basic Information</span>}
13-
>
14-
<Row gutter={VERTICAL_GUTTER}>
15-
<Col span={12}>
16-
<Form layout='vertical' initialValues={user}>
17-
<Form.Item
18-
name='realName'
19-
label='Real Name'
20-
rules={[{ required: true }]}
21-
>
22-
<Input />
23-
</Form.Item>
24-
<Form.Item>
25-
<Button type='primary' htmlType='submit'>
26-
Update
27-
</Button>
28-
</Form.Item>
29-
</Form>
30-
</Col>
10+
return (
11+
<Card
12+
title={<span className='text-2xl font-semibold'>Basic Information</span>}
13+
>
14+
<Row gutter={DEFAULT_GUTTER}>
15+
<Col span={12}>
16+
<Form layout='vertical' initialValues={user}>
17+
<Form.Item
18+
name='realName'
19+
label='Real Name'
20+
rules={[{ required: true }]}
21+
>
22+
<Input />
23+
</Form.Item>
24+
<Form.Item>
25+
<Button type='primary' htmlType='submit'>
26+
Update
27+
</Button>
28+
</Form.Item>
29+
</Form>
30+
</Col>
3131

32-
<Col span={12}>
33-
<AvatarUpload />
34-
</Col>
35-
</Row>
36-
</Card>
37-
)
32+
<Col span={12}>
33+
<AvatarUpload />
34+
</Col>
35+
</Row>
36+
</Card>
37+
)
3838
}
3939

4040
export default Index
+9-9
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { Col, Row } from 'antd'
22
import type React from 'react'
3-
import { VERTICAL_GUTTER } from 'utils/constants'
3+
import { DEFAULT_GUTTER } from 'utils/constants'
44
import ChangePassword from './ChangePassword'
55
import EditProfile from './EditProfile'
66

77
const Index: React.FC = () => (
8-
<Row gutter={VERTICAL_GUTTER}>
9-
<Col span={24}>
10-
<EditProfile />
11-
</Col>
12-
<Col span={24}>
13-
<ChangePassword />
14-
</Col>
15-
</Row>
8+
<Row gutter={DEFAULT_GUTTER}>
9+
<Col span={24}>
10+
<EditProfile />
11+
</Col>
12+
<Col span={24}>
13+
<ChangePassword />
14+
</Col>
15+
</Row>
1616
)
1717

1818
export default Index

‎src/routes.tsx

+11-5
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const SiteAdmin = {
5858
}
5959

6060
const RecordList = lazy(async () => import('pages/RecordList'))
61+
const RecordDetail = lazy(async () => import('pages/RecordDetail'))
6162

6263
const NotFound = lazy(async () => import('pages/NotFound'))
6364

@@ -245,11 +246,16 @@ const children: RouteObject[] = [
245246
},
246247
{
247248
path: 'record',
248-
element: <RecordList />
249-
},
250-
{
251-
path: 'record/:recordId',
252-
element: <RecordList />
249+
children: [
250+
{
251+
index: true,
252+
element: <RecordList />
253+
},
254+
{
255+
path: ':recordId',
256+
element: <RecordDetail />
257+
}
258+
]
253259
}
254260
]
255261
}

‎src/utils/constants.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ export const MAIN_CONTENT_GRID = {
1313
xxl: 14
1414
}
1515

16-
export const VERTICAL_GUTTER: [Gutter, Gutter] = [
17-
0,
18-
{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }
16+
export const DEFAULT_GUTTER: [Gutter, Gutter] = [
17+
{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }, // Horizontal
18+
{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 } // Vertical
1919
]
2020

2121
export const SUPPORT_PROGRAMMING_LANGUAGE = [

‎src/utils/exception.ts

+28-13
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,45 @@
1010
* Throw when no domainUrl is found in the current path.
1111
*/
1212
class NoDomainUrlError extends Error {
13-
public constructor() {
14-
super('No domain url found')
15-
this.name = 'NoDomainUrlError'
16-
}
13+
public constructor() {
14+
super('No domain url found')
15+
this.name = 'NoDomainUrlError'
16+
}
1717
}
1818

1919
/**
2020
* Throw when no problemSetId is found in the current path.
2121
*/
2222
class NoProblemSetIdError extends Error {
23-
public constructor() {
24-
super('No problem set id found')
25-
this.name = 'NoProblemSetIdError'
26-
}
23+
public constructor() {
24+
super('No problem set id found')
25+
this.name = 'NoProblemSetIdError'
26+
}
2727
}
2828

2929
/**
3030
* Throw when no problemId is found in the current path.
3131
*/
3232
class NoProblemIdError extends Error {
33-
public constructor() {
34-
super('No problem id found')
35-
this.name = 'NoProblemIdError'
36-
}
33+
public constructor() {
34+
super('No problem id found')
35+
this.name = 'NoProblemIdError'
36+
}
3737
}
3838

39-
export { NoDomainUrlError, NoProblemSetIdError, NoProblemIdError }
39+
/**
40+
* Throw when no recordId is found in the current path.
41+
*/
42+
class NoRecordIdError extends Error {
43+
public constructor() {
44+
super('No record id found')
45+
this.name = 'NoRecordIdError'
46+
}
47+
}
48+
49+
export {
50+
NoDomainUrlError,
51+
NoProblemSetIdError,
52+
NoProblemIdError,
53+
NoRecordIdError
54+
}

‎src/utils/index.ts

+30-15
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,38 @@ import type { RouteMatch } from 'react-router-dom'
33
import type { HorsePagination, ProTablePagination } from 'types'
44

55
export function transPagination(
6-
parameters: ProTablePagination
6+
parameters: ProTablePagination
77
): HorsePagination {
8-
const { current, pageSize } = parameters
9-
const offset =
10-
!isNil(current) && !isNil(pageSize) ? (current - 1) * pageSize : undefined
11-
return omitBy(
12-
{
13-
limit: pageSize,
14-
offset
15-
},
16-
isNil
17-
)
8+
const { current, pageSize } = parameters
9+
const offset =
10+
!isNil(current) && !isNil(pageSize) ? (current - 1) * pageSize : undefined
11+
return omitBy(
12+
{
13+
limit: pageSize,
14+
offset
15+
},
16+
isNil
17+
)
1818
}
1919

2020
export function joinRoutes(matches: RouteMatch[]): string {
21-
return matches
22-
.map(match => match.route.path) // extract all path patterns
23-
.join('/') // join with '/'
24-
.replace(/\/{2,}/, '/') // replace all redundant slash ('/')
21+
return matches
22+
.map(match => match.route.path) // extract all path patterns
23+
.join('/') // join with '/'
24+
.replace(/\/{2,}/, '/') // replace all redundant slash ('/')
25+
}
26+
27+
export function timeMs2String(timeMs: number): string {
28+
// @Chujie: Can you imagine we can judge AI/ML on this platform?
29+
if (timeMs > 1000 * 60) return `${(timeMs / 1000 / 60).toFixed(3)}min`
30+
if (timeMs > 1000) return `${(timeMs / 1000).toFixed(3)}s`
31+
return `${timeMs}ms` // Usually you would only need this
32+
}
33+
34+
export function memoryKb2String(memoryKb: number): string {
35+
// @Chujie: If memory usage is larger than 1 GiB, I would suggest give up learning coding
36+
if (memoryKb > 1024 * 1024)
37+
return `${(memoryKb / 1024 / 1024).toFixed(3)} GiB`
38+
if (memoryKb > 1024) return `${(memoryKb / 1024).toFixed(3)} MiB`
39+
return `${memoryKb} KiB`
2540
}

0 commit comments

Comments
 (0)
This repository has been archived.