Skip to content

Commit cbb052a

Browse files
committed
feat(review): enhance team review page
1 parent 7a73a1a commit cbb052a

File tree

3 files changed

+137
-44
lines changed

3 files changed

+137
-44
lines changed

src/GZCTF/ClientApp/src/pages/admin/Teams.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { useInputState } from '@mantine/hooks'
1616
import { showNotification } from '@mantine/notifications'
1717
import {
18+
mdiAccountGroupOutline,
1819
mdiArrowLeftBold,
1920
mdiArrowRightBold,
2021
mdiCheck,
@@ -166,6 +167,7 @@ const Teams: FC = () => {
166167
onKeyDown={(e) => {
167168
!searching && e.key === 'Enter' && onSearch()
168169
}}
170+
rightSection={<Icon path={mdiAccountGroupOutline} size={1} />}
169171
/>
170172
<Group position="right">
171173
<Text fw="bold" size="sm">

src/GZCTF/ClientApp/src/pages/admin/Users.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useClipboard, useInputState } from '@mantine/hooks'
1717
import { useModals } from '@mantine/modals'
1818
import { showNotification } from '@mantine/notifications'
1919
import {
20+
mdiAccountOutline,
2021
mdiArrowLeftBold,
2122
mdiArrowRightBold,
2223
mdiCheck,
@@ -206,6 +207,7 @@ const Users: FC = () => {
206207
onKeyDown={(e) => {
207208
!searching && e.key === 'Enter' && onSearch()
208209
}}
210+
rightSection={<Icon path={mdiAccountOutline} size={1} />}
209211
/>
210212
<Group position="right">
211213
<Text fw="bold" size="sm">

src/GZCTF/ClientApp/src/pages/admin/games/[id]/Review.tsx

Lines changed: 133 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,29 @@ import {
44
Badge,
55
Box,
66
Center,
7+
Grid,
78
Group,
9+
Input,
10+
Pagination,
811
ScrollArea,
912
Select,
1013
Stack,
1114
Text,
15+
TextInput,
1216
Title,
17+
createStyles,
1318
useMantineTheme,
1419
} from '@mantine/core'
20+
import { useInputState } from '@mantine/hooks'
1521
import { showNotification } from '@mantine/notifications'
1622
import {
23+
mdiAccountGroupOutline,
1724
mdiAccountOutline,
1825
mdiBadgeAccountHorizontalOutline,
1926
mdiCheck,
2027
mdiClose,
2128
mdiEmailOutline,
29+
mdiIdentifier,
2230
mdiPhoneOutline,
2331
mdiStar,
2432
} from '@mdi/js'
@@ -44,43 +52,76 @@ const iconProps = {
4452
color: 'gray',
4553
}
4654

55+
const useGridStyles = createStyles((theme) => ({
56+
root: {
57+
flexDirection: 'row',
58+
flexGrow: 1,
59+
gap: 0,
60+
},
61+
62+
col: {
63+
display: 'flex',
64+
flexDirection: 'row',
65+
alignItems: 'center',
66+
justifyContent: 'flex-start',
67+
gap: theme.spacing.xs,
68+
boxSizing: 'border-box',
69+
padding: `0 ${theme.spacing.xs}`,
70+
height: '1.5rem',
71+
},
72+
73+
input: {
74+
userSelect: 'none',
75+
lineHeight: 1,
76+
fontSize: '1rem',
77+
},
78+
}))
79+
4780
const MemberItem: FC<MemberItemProps> = (props) => {
4881
const { user, isCaptain, isRegistered } = props
4982
const theme = useMantineTheme()
5083

5184
const { t } = useTranslation()
85+
const { classes } = useGridStyles()
5286

5387
return (
5488
<Group noWrap spacing="xl" position="apart">
5589
<Group noWrap w="calc(100% - 16rem)" miw="500px">
5690
<Avatar alt="avatar" src={user.avatar}>
5791
{user.userName?.slice(0, 1) ?? 'U'}
5892
</Avatar>
59-
<Group noWrap>
60-
<Stack spacing={2} w="15rem">
61-
<Group noWrap spacing="xs">
62-
<Icon path={mdiAccountOutline} {...iconProps} />
63-
<Group noWrap>
64-
<Text fw={700}>{user.userName}</Text>
65-
<Text>{!user.realName ? '' : user.realName}</Text>
66-
</Group>
67-
</Group>
68-
<Group noWrap spacing="xs">
69-
<Icon path={mdiBadgeAccountHorizontalOutline} {...iconProps} />
70-
<Text>{!user.stdNumber ? t('admin.placeholder.empty') : user.stdNumber}</Text>
71-
</Group>
72-
</Stack>
73-
<Stack spacing={2}>
74-
<Group noWrap spacing="xs">
75-
<Icon path={mdiEmailOutline} {...iconProps} />
76-
<Text>{!user.email ? t('admin.placeholder.empty') : user.email}</Text>
77-
</Group>
78-
<Group noWrap spacing="xs">
79-
<Icon path={mdiPhoneOutline} {...iconProps} />
80-
<Text>{!user.phone ? t('admin.placeholder.empty') : user.phone}</Text>
81-
</Group>
82-
</Stack>
83-
</Group>
93+
<Grid className={classes.root}>
94+
<Grid.Col span={3} className={classes.col}>
95+
<Icon path={mdiIdentifier} {...iconProps} />
96+
<Text fw={700}>{user.userName}</Text>
97+
</Grid.Col>
98+
<Grid.Col span={3} className={classes.col}>
99+
<Icon path={mdiBadgeAccountHorizontalOutline} {...iconProps} />
100+
<Input
101+
variant="unstyled"
102+
value={user.stdNumber || t('admin.placeholder.empty')}
103+
readOnly
104+
classNames={{ input: classes.input }}
105+
/>
106+
</Grid.Col>
107+
<Grid.Col span={6} className={classes.col}>
108+
<Icon path={mdiEmailOutline} {...iconProps} />
109+
<Text>{user.email || t('admin.placeholder.empty')}</Text>
110+
</Grid.Col>
111+
<Grid.Col span={6} className={classes.col}>
112+
<Icon path={mdiAccountOutline} {...iconProps} />
113+
<Input
114+
variant="unstyled"
115+
value={user.realName || t('admin.placeholder.empty')}
116+
readOnly
117+
classNames={{ input: classes.input }}
118+
/>
119+
</Grid.Col>
120+
<Grid.Col span={6} className={classes.col}>
121+
<Icon path={mdiPhoneOutline} {...iconProps} />
122+
<Text>{user.phone || t('admin.placeholder.empty')}</Text>
123+
</Grid.Col>
124+
</Grid>
84125
</Group>
85126
<Group noWrap position="right">
86127
{isCaptain && (
@@ -176,17 +217,22 @@ const ParticipationItem: FC<ParticipationItemProps> = (props) => {
176217
)
177218
}
178219

220+
const PART_NUM_PER_PAGE = 10
221+
179222
const GameTeamReview: FC = () => {
180223
const navigate = useNavigate()
181224
const { id } = useParams()
182225
const numId = parseInt(id ?? '-1')
183226
const [disabled, setDisabled] = useState(false)
184227
const [selectedStatus, setSelectedStatus] = useState<ParticipationStatus | null>(null)
228+
const [selectedOrg, setSelectedOrg] = useState<string | null>(null)
185229
const [participations, setParticipations] = useState<ParticipationInfoModel[]>()
230+
const [search, setSearch] = useInputState('')
186231
const { classes } = useAccordionStyles()
187232
const participationStatusMap = useParticipationStatusMap()
188233

189234
const { t } = useTranslation()
235+
const [activePage, setPage] = useState(1)
190236

191237
const setParticipationStatus = async (id: number, status: ParticipationStatus) => {
192238
setDisabled(true)
@@ -207,6 +253,10 @@ const GameTeamReview: FC = () => {
207253
}
208254
}
209255

256+
useEffect(() => {
257+
setPage(1)
258+
}, [selectedStatus, selectedOrg, search])
259+
210260
useEffect(() => {
211261
if (numId < 0) {
212262
showNotification({
@@ -223,21 +273,57 @@ const GameTeamReview: FC = () => {
223273
})
224274
}, [])
225275

276+
const orgs = Array.from(new Set(participations?.map((p) => p.organization ?? '') ?? [])).filter(
277+
(org) => !!org
278+
)
279+
280+
const filteredParticipations = participations?.filter(
281+
(participation) =>
282+
(selectedStatus === null || participation.status === selectedStatus) &&
283+
(selectedOrg === null || participation.organization === selectedOrg) &&
284+
(search === '' || participation.team?.name?.toLowerCase().includes(search.toLowerCase()))
285+
)
286+
287+
const pagedParticipations = filteredParticipations?.slice(
288+
(activePage - 1) * PART_NUM_PER_PAGE,
289+
activePage * PART_NUM_PER_PAGE
290+
)
291+
226292
return (
227293
<WithGameEditTab
228294
headProps={{ position: 'apart' }}
229295
isLoading={!participations}
230296
head={
231-
<Select
232-
placeholder={t('admin.content.show_all')}
233-
clearable
234-
data={Array.from(participationStatusMap, (v) => ({ value: v[0], label: v[1].title }))}
235-
value={selectedStatus}
236-
onChange={(value: ParticipationStatus) => setSelectedStatus(value)}
237-
/>
297+
<Group position="apart" noWrap w="100%">
298+
<TextInput
299+
w="20rem"
300+
placeholder={t('admin.placeholder.teams.search')}
301+
value={search}
302+
onChange={setSearch}
303+
rightSection={<Icon path={mdiAccountGroupOutline} size={1} />}
304+
/>
305+
<Group position="right" noWrap>
306+
{orgs.length && (
307+
<Select
308+
placeholder={t('admin.content.show_all')}
309+
clearable
310+
data={orgs.map((org) => ({ value: org, label: org }))}
311+
value={selectedOrg}
312+
onChange={(value: string) => setSelectedOrg(value)}
313+
/>
314+
)}
315+
<Select
316+
placeholder={t('admin.content.show_all')}
317+
clearable
318+
data={Array.from(participationStatusMap, (v) => ({ value: v[0], label: v[1].title }))}
319+
value={selectedStatus}
320+
onChange={(value: ParticipationStatus) => setSelectedStatus(value)}
321+
/>
322+
</Group>
323+
</Group>
238324
}
239325
>
240-
<ScrollArea type="auto" pos="relative" h="calc(100vh - 180px)" offsetScrollbars>
326+
<ScrollArea type="never" pos="relative" h="calc(100vh - 250px)">
241327
{!participations || participations.length === 0 ? (
242328
<Center h="calc(100vh - 200px)">
243329
<Stack spacing={0}>
@@ -252,20 +338,23 @@ const GameTeamReview: FC = () => {
252338
classNames={classes}
253339
className={classes.root}
254340
>
255-
{participations?.map(
256-
(participation) =>
257-
(selectedStatus === null || participation.status === selectedStatus) && (
258-
<ParticipationItem
259-
key={participation.id}
260-
participation={participation}
261-
disabled={disabled}
262-
setParticipationStatus={setParticipationStatus}
263-
/>
264-
)
265-
)}
341+
{pagedParticipations?.map((participation) => (
342+
<ParticipationItem
343+
key={participation.id}
344+
participation={participation}
345+
disabled={disabled}
346+
setParticipationStatus={setParticipationStatus}
347+
/>
348+
))}
266349
</Accordion>
267350
)}
268351
</ScrollArea>
352+
<Pagination
353+
position="right"
354+
value={activePage}
355+
onChange={setPage}
356+
total={(filteredParticipations?.length ?? 0) / PART_NUM_PER_PAGE + 1}
357+
/>
269358
</WithGameEditTab>
270359
)
271360
}

0 commit comments

Comments
 (0)