-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(fe): enable export contest overall excel V2 (#2210)
* feat(fe): add react-csv * feat(fe): add major * feat(fe): enabel excel export * feat(fe): pnpm add react-csv * feat(fe): edit import queries * feat(fe): change logo * feat(fe): fix build error * feat(fe): check coding conventions * feat(fe): check coding conventions * feat(fe): delete unused file * feat(fe): fix icon hoover
- Loading branch information
1 parent
91466cf
commit 7cb4f7a
Showing
6 changed files
with
259 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
172 changes: 146 additions & 26 deletions
172
apps/frontend/app/admin/contest/[contestId]/_components/ContestOverallTabs.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,163 @@ | ||
'use client' | ||
|
||
import { | ||
GET_CONTEST_SCORE_SUMMARIES, | ||
GET_CONTEST_SUBMISSION_SUMMARIES_OF_USER, | ||
GET_CONTESTS | ||
} from '@/graphql/contest/queries' | ||
import { cn } from '@/lib/utils' | ||
import type { Route } from 'next' | ||
import excelIcon from '@/public/icons/excel.svg' | ||
import { useQuery } from '@apollo/client' | ||
import Image from 'next/image' | ||
import Link from 'next/link' | ||
import { usePathname } from 'next/navigation' | ||
import { CSVLink } from 'react-csv' | ||
|
||
interface ScoreSummary { | ||
realName: string | ||
studentId: string | ||
userContestScore: number | ||
contestPerfectScore: number | ||
submittedProblemCount: number | ||
totalProblemCount: number | ||
problemScores: { problemId: number; score: number; maxScore: number }[] | ||
major: string | ||
} | ||
interface SubmissionSummary { | ||
problemId: number | ||
} | ||
|
||
export default function ContestOverallTabs({ | ||
contestId | ||
contestId, | ||
userId | ||
}: { | ||
contestId: string | ||
userId: number | ||
}) { | ||
const id = contestId | ||
const id = parseInt(contestId, 10) | ||
const pathname = usePathname() | ||
|
||
const isCurrentTab = (tab: string) => { | ||
if (tab === '') return pathname === `/admin/contest/${id}` | ||
return pathname.startsWith(`/admin/contest/${id}/${tab}`) | ||
} | ||
const { data: scoreData } = useQuery<{ | ||
getContestScoreSummaries: ScoreSummary[] | ||
}>(GET_CONTEST_SCORE_SUMMARIES, { | ||
variables: { contestId: id, take: 300 }, | ||
skip: !contestId | ||
}) | ||
|
||
useQuery<{ | ||
getContestSubmissionSummaryByUserId: { submissions: SubmissionSummary[] } | ||
}>(GET_CONTEST_SUBMISSION_SUMMARIES_OF_USER, { | ||
variables: { contestId: id, userId, take: 300 }, | ||
skip: !contestId || !userId | ||
}) | ||
|
||
const { data: contestData } = useQuery(GET_CONTESTS, { | ||
variables: { groupId: 1, take: 100 }, | ||
skip: !contestId | ||
}) | ||
|
||
const contestTitle = contestData?.getContests.find( | ||
(contest) => contest.id === contestId | ||
)?.title | ||
|
||
const fileName = contestTitle | ||
? `${contestTitle.replace(/\s+/g, '_')}.csv` | ||
: `contest-${id}-participants.csv` | ||
|
||
const uniqueProblems = Array.from( | ||
new Set( | ||
scoreData?.getContestScoreSummaries.flatMap((user) => | ||
user.problemScores.map((score) => score.problemId) | ||
) || [] | ||
) | ||
) | ||
|
||
const problemHeaders = uniqueProblems.flatMap((problemId, index) => { | ||
const problemLabel = String.fromCharCode(65 + index) | ||
return [ | ||
{ | ||
label: `${problemLabel}`, | ||
key: `problems[${index}].maxScore` | ||
} | ||
] | ||
}) | ||
|
||
const headers = [ | ||
{ label: '전공', key: 'major' }, | ||
{ label: '이름', key: 'realName' }, | ||
{ label: '학번', key: 'studentId' }, | ||
{ label: '총 획득 점수/총점', key: 'scoreRatio' }, | ||
{ label: '제출 문제 수/총 문제 수', key: 'problemRatio' }, | ||
|
||
...problemHeaders | ||
] | ||
|
||
const csvData = | ||
scoreData?.getContestScoreSummaries.map((user) => { | ||
const userProblemScores = uniqueProblems.map((problemId) => { | ||
const scoreData = user.problemScores.find( | ||
(ps) => ps.problemId === problemId | ||
) | ||
|
||
return { | ||
maxScore: `${scoreData ? scoreData.score : 0}/${scoreData ? scoreData.maxScore : 0}`, | ||
score: `${scoreData ? scoreData.score : 0}/${scoreData ? scoreData.maxScore : 0}` | ||
} | ||
}) | ||
|
||
return { | ||
major: user.major, | ||
realName: user.realName, | ||
studentId: user.studentId, | ||
scoreRatio: `${user.userContestScore}/${user.contestPerfectScore}`, | ||
problemRatio: | ||
user.submittedProblemCount === user.totalProblemCount | ||
? 'Submit' | ||
: `${user.submittedProblemCount}/${user.totalProblemCount}`, | ||
problems: userProblemScores | ||
} | ||
}) || [] | ||
|
||
const isCurrentTab = (tab: string) => | ||
pathname.startsWith(`/admin/contest/${id}/${tab}`) | ||
|
||
return ( | ||
<span className="flex w-max gap-1 rounded-lg bg-slate-200 p-1"> | ||
<Link | ||
href={`/admin/contest/${id}` as Route} | ||
className={cn( | ||
'rounded-md px-3 py-1.5 text-lg font-semibold', | ||
isCurrentTab('') && 'text-primary bg-white font-bold' | ||
)} | ||
> | ||
Participant | ||
</Link> | ||
<Link | ||
href={`/admin/contest/${id}/submission` as Route} | ||
className={cn( | ||
'rounded-md px-3 py-1.5 text-lg font-semibold', | ||
isCurrentTab('submission') && 'text-primary bg-white font-bold' | ||
)} | ||
<div className="flex items-center justify-between"> | ||
<div className="flex w-max gap-1 rounded-lg bg-slate-200 p-1"> | ||
<Link | ||
href={`/admin/contest/${id}`} | ||
className={cn( | ||
'rounded-md px-3 py-1.5 text-lg font-semibold', | ||
isCurrentTab('') && 'text-primary bg-white font-bold' | ||
)} | ||
> | ||
Participant | ||
</Link> | ||
<Link | ||
href={`/admin/contest/${id}/submission`} | ||
className={cn( | ||
'rounded-md px-3 py-1.5 text-lg font-semibold', | ||
isCurrentTab('submission') && 'text-primary bg-white font-bold' | ||
)} | ||
> | ||
All Submission | ||
</Link> | ||
</div> | ||
<CSVLink | ||
data={csvData} | ||
headers={headers} | ||
filename={fileName} | ||
className="flex items-center gap-2 rounded-lg bg-blue-400 px-3 py-1.5 text-lg font-semibold text-white transition-opacity hover:opacity-85" | ||
> | ||
All Submission | ||
</Link> | ||
</span> | ||
Export | ||
<Image | ||
src={excelIcon} | ||
alt="Excel Icon" | ||
width={20} | ||
height={20} | ||
className="ml-1" | ||
/> | ||
</CSVLink> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,6 +67,7 @@ const GET_CONTEST_SCORE_SUMMARIES = | |
username | ||
studentId | ||
realName | ||
major | ||
} | ||
}`) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.