Skip to content

Commit

Permalink
feat(fe): edit client contest list and table (#2043)
Browse files Browse the repository at this point in the history
* feat(fe): redesign layout

* chore(fe): hide badge if contest

* feat(fe): redesign contest list

* fix(fe): hide score when isJudgeResultVisible is false

* fix(fe): fix type
  • Loading branch information
youznn authored Aug 31, 2024
1 parent 68a5c7d commit 87d4ecf
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export default function RegisterButton({
} else {
toast.success(`Registered ${state} test successfully`)
router.push(`/contest/${contestId}/problem`)
router.refresh() // to update register state
}
})
.catch((err) => console.log(err))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,55 @@
'use client'

import { Badge } from '@/components/ui/badge'
import { convertToLetter } from '@/lib/utils'
import type { ContestProblem, Level } from '@/types/type'
import { convertToLetter, dateFormatter } from '@/lib/utils'
import CheckIcon from '@/public/check-green.svg'
import type { ContestProblem } from '@/types/type'
import type { ColumnDef } from '@tanstack/react-table'
import Image from 'next/image'

export const columns: ColumnDef<ContestProblem>[] = [
{
header: '#',
accessorKey: 'order',
cell: ({ row }) => (
<div className="h-full">{convertToLetter(row.original.order)}</div>
<div className="h-full font-medium">
{convertToLetter(row.original.order)}
</div>
)
},
{
header: 'Title',
accessorKey: 'title',
cell: ({ row }) => {
return (
<p className="text-left text-sm md:text-base">{`${row.original.title}`}</p>
<p className="text-left font-medium md:text-base">{`${row.original.title}`}</p>
)
}
},
{
header: 'Level',
accessorKey: 'difficulty',
cell: ({ row }) => (
<Badge className="rounded-md" variant={row.original.difficulty as Level}>
{row.original.difficulty}
</Badge>
)
header: 'Submit',
accessorKey: 'submit',
cell: ({ row }) =>
row.original.submissionTime && (
<div className="flex items-center justify-center">
<Image src={CheckIcon} alt="check" width={24} height={24} />
</div>
)
},
{
header: () => 'Submission',
accessorKey: 'submissionCount',
cell: ({ row }) => row.original.submissionCount
header: () => 'Submission Time',
accessorKey: 'submissionTime',
cell: ({ row }) =>
row.original.submissionTime &&
dateFormatter(row.original.submissionTime, 'YYYY-MM-DD HH:mm:ss')
},
{
header: () => 'Solved',
accessorKey: 'acceptedRate',
cell: ({ row }) => `${row.original.acceptedRate.toFixed(2)}%`
header: () => 'Score',
accessorKey: 'score',
cell: ({ row }) =>
row.original.maxScore != null ? (
`${row.original.score ?? '-'} / ${row.original.maxScore}`
) : (
<></>
)
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ export default async function ContestProblem({ params }: ContestProblemProps) {
data={problems.data}
columns={columns}
headerStyle={{
order: 'w-[8%]',
order: 'w-[8%] ',
title: 'text-left w-[50%]',
difficulty: 'w-[14%]',
submissionCount: 'w-[14%]',
acceptedRate: 'w-[14%]'
submit: 'w-[11%]',
submissionTime: 'w-[20%]',
score: 'w-[11%]'
}}
linked
/>
Expand Down
72 changes: 62 additions & 10 deletions apps/frontend/app/(main)/contest/[contestId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import ContestStatusTimeDiff from '@/components/ContestStatusTimeDiff'
import { fetcher } from '@/lib/utils'
import { auth } from '@/lib/auth'
import { fetcher, fetcherWithAuth } from '@/lib/utils'
import { dateFormatter } from '@/lib/utils'
import Calendar from '@/public/20_calendar.svg'
import CheckIcon from '@/public/check_blue.svg'
import type { Contest } from '@/types/type'
import Image from 'next/image'
import ContestTabs from '../_components/ContestTabs'
import { calculateContestScore } from '../utils'

interface ContestDetailProps {
params: {
Expand All @@ -17,21 +23,67 @@ export default async function Layout({
tabs: React.ReactNode
}) {
const { contestId } = params
const res = await fetcher.get(`contest/${contestId}`)
const session = await auth()

const res = await (session ? fetcherWithAuth : fetcher).get(
`contest/${contestId}`
)
if (res.ok) {
const contest: Contest = await res.json()
const formattedStartTime = dateFormatter(
contest.startTime,
'YYYY-MM-DD HH:mm:ss'
)
const formattedEndTime = dateFormatter(
contest.endTime,
'YYYY-MM-DD HH:mm:ss'
)
const isJudgeResultVisible = contest.isJudgeResultVisible
const isRegistered = contest.isRegistered
let totalScore = 0
let totalMaxScore = 0
if (isRegistered && isJudgeResultVisible) {
const [score, maxScore] = await calculateContestScore({ contestId })
totalScore = score
totalMaxScore = maxScore
}

return (
<article>
<header className="flex justify-between p-5 py-8">
<h2 className="break-words text-2xl font-extrabold">
{contest?.title}
</h2>
<ContestStatusTimeDiff
contest={contest}
textStyle="text-gray-500"
inContestEditor={false}
/>
<div className="flex flex-col gap-3">
<h2 className="break-words text-[28px] font-medium">
{contest?.title}
</h2>
<div className="flex items-center gap-2">
{isRegistered && (
<>
<Image src={CheckIcon} alt="check" width={24} height={24} />
<p className="text-primary-light text-sm font-bold">
Total score
</p>
<p className="text-primary-strong font-bold">
{isJudgeResultVisible
? `${totalScore} / ${totalMaxScore}`
: '-'}
</p>
</>
)}
</div>
</div>
<div className="flex flex-col items-end gap-4">
<div className="flex gap-2">
<Image src={Calendar} alt="calendar" width={24} height={24} />
<p className="font-medium text-[#333333]">
{formattedStartTime} ~ {formattedEndTime}
</p>
</div>
<ContestStatusTimeDiff
contest={contest}
textStyle="text-netural-900 font-medium"
inContestEditor={false}
/>
</div>
</header>
<ContestTabs contestId={contestId} />
{tabs}
Expand Down
28 changes: 28 additions & 0 deletions apps/frontend/app/(main)/contest/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { fetcherWithAuth } from '@/lib/utils'
import type { ContestProblem } from '@/types/type'

interface ContestProblemsApiRes {
data: ContestProblem[]
}

const calculateContestScore = async ({ contestId }: { contestId: string }) => {
const contestProblems: ContestProblemsApiRes = await fetcherWithAuth
.get(`contest/${contestId}/problem`)
.json()

const { totalScore, totalMaxScore } = contestProblems.data.reduce(
(acc, curr) => {
const score = curr.score ? parseInt(curr.score, 10) : 0
const maxScore = curr.maxScore || 0

acc.totalScore += score
acc.totalMaxScore += maxScore

return acc
},
{ totalScore: 0, totalMaxScore: 0 }
)
return [totalScore, totalMaxScore]
}

export { calculateContestScore }
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default async function DescriptionPage({
<EditorDescription
problem={contestProblem.problem}
contestProblems={contestProblems.problems}
isContest={true}
/>
)
}
18 changes: 11 additions & 7 deletions apps/frontend/components/EditorDescription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@ const useCopy = () => {

export function EditorDescription({
problem,
contestProblems
contestProblems,
isContest = false
}: {
problem: ProblemDetail
contestProblems?: ContestProblem[]
isContest?: boolean
}) {
const { copiedID, copy } = useCopy()

Expand All @@ -80,12 +82,14 @@ export function EditorDescription({
<div className="px-6">
<div className="flex max-h-24 justify-between gap-4">
<h1 className="mb-3 overflow-hidden text-ellipsis whitespace-nowrap text-xl font-bold">{`#${contestProblems ? convertToLetter(contestProblems.find((item) => item.id === problem.id)?.order as number) : problem.id}. ${problem.title}`}</h1>
<Badge
className="h-6 w-[52px] whitespace-nowrap rounded-[4px] bg-neutral-500 p-[6px] text-xs font-medium hover:bg-neutral-500"
textColors={level as Level}
>
{`Level ${levelNumber}`}
</Badge>
{!isContest && (
<Badge
className="h-6 w-[52px] whitespace-nowrap rounded-[4px] bg-neutral-500 p-[6px] text-xs font-medium hover:bg-neutral-500"
textColors={level as Level}
>
{`Level ${levelNumber}`}
</Badge>
)}
</div>
<div className="prose prose-invert max-w-full text-sm leading-relaxed text-slate-300">
{katexContent}
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/public/check-green.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/frontend/types/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export interface WorkbookProblem extends Omit<Problem, 'tags' | 'info'> {

export interface ContestProblem extends Omit<Problem, 'tags' | 'info'> {
order: number
maxScore: number
score?: string
submissionTime?: Date
}

export interface ProblemDetail {
Expand Down Expand Up @@ -81,6 +84,7 @@ export interface Contest {
id: string
groupName: string
}
isJudgeResultVisible: boolean
enableCopyPaste: boolean
status: ContestStatus
participants: number
Expand Down

0 comments on commit 87d4ecf

Please sign in to comment.