diff --git a/apps/frontend/app/admin/_components/table/DataTableFallback.tsx b/apps/frontend/app/admin/_components/table/DataTableFallback.tsx index a9819687b2..42cecccf99 100644 --- a/apps/frontend/app/admin/_components/table/DataTableFallback.tsx +++ b/apps/frontend/app/admin/_components/table/DataTableFallback.tsx @@ -41,14 +41,14 @@ export default function DataTableFallback({ ...props }: DataTableFallbackProps) { return ( - <> +
{withSearchBar && } - +
) } -export function TableFallback({ +function TableFallback({ columns, headerStyle = {}, rowCount = 10 diff --git a/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/_components/Columns.tsx b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/_components/Columns.tsx index f5f64e381d..007d9e40c7 100644 --- a/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/_components/Columns.tsx +++ b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/_components/Columns.tsx @@ -1,12 +1,16 @@ 'use client' +import DataTableColumnHeader from '@/app/admin/_components/table/DataTableColumnHeader' import type { ScoreSummary, ProblemData } from '@/app/admin/contest/utils' -import { DataTableColumnHeader } from '@/components/DataTableColumnHeader' import type { ColumnDef, Row } from '@tanstack/react-table' -export const columns = ( +interface DataTableScoreSummary extends ScoreSummary { + id: number +} + +export const createColumns = ( problemData: ProblemData[] -): ColumnDef[] => { +): ColumnDef[] => { return [ { accessorKey: 'studentId', @@ -79,7 +83,7 @@ export const columns = ( {String.fromCharCode(Number(65 + i))}

), - cell: ({ row }: { row: Row }) => { + cell: ({ row }: { row: Row }) => { const problemScore = row.original.problemScores.find( (ps) => ps.problemId === problem.problemId ) diff --git a/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/_components/ParticipantTable.tsx b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/_components/ParticipantTable.tsx new file mode 100644 index 0000000000..7ac6638d54 --- /dev/null +++ b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/_components/ParticipantTable.tsx @@ -0,0 +1,57 @@ +'use client' + +import DataTable from '@/app/admin/_components/table/DataTable' +import DataTableFallback from '@/app/admin/_components/table/DataTableFallback' +import DataTablePagination from '@/app/admin/_components/table/DataTablePagination' +import DataTableRoot from '@/app/admin/_components/table/DataTableRoot' +import DataTableSearchBar from '@/app/admin/_components/table/DataTableSearchBar' +import { Skeleton } from '@/components/ui/skeleton' +import { GET_CONTEST_SCORE_SUMMARIES } from '@/graphql/contest/queries' +import { GET_CONTEST_PROBLEMS } from '@/graphql/problem/queries' +import { useSuspenseQuery } from '@apollo/client' +import { createColumns } from './Columns' + +export function ParticipantTable({ contestId }: { contestId: number }) { + const summaries = useSuspenseQuery(GET_CONTEST_SCORE_SUMMARIES, { + variables: { contestId, take: 300 } + }) + const summariesData = summaries.data.getContestScoreSummaries.map((item) => ({ + ...item, + id: item.userId + })) + + const problems = useSuspenseQuery(GET_CONTEST_PROBLEMS, { + variables: { groupId: 1, contestId } + }) + + const problemData = problems.data.getContestProblems + .slice() + .sort((a, b) => a.order - b.order) + + return ( +
+

+ {summariesData.length}{' '} + Participants +

+ + + + `/admin/contest/${contestId}/participant/${data.id}` + } + /> + + +
+ ) +} + +export function ParticipantTableFallback() { + return ( +
+ + +
+ ) +} diff --git a/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/page.tsx b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/page.tsx index 6b9ce11825..6860223d64 100644 --- a/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/page.tsx +++ b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/page.tsx @@ -1,67 +1,13 @@ -'use client' - -import { DataTableAdmin } from '@/components/DataTableAdmin' -import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' -import { Skeleton } from '@/components/ui/skeleton' -import { GET_CONTEST_SCORE_SUMMARIES } from '@/graphql/contest/queries' -import { GET_CONTEST_PROBLEMS } from '@/graphql/problem/queries' -import { useQuery } from '@apollo/client' -import { useParams } from 'next/navigation' -import type { ScoreSummary, ProblemData } from '../../../utils' -import { columns } from './_components/Columns' - -export default function Submission() { - const { id } = useParams() - - const summaries = useQuery(GET_CONTEST_SCORE_SUMMARIES, { - variables: { contestId: Number(id), take: 300 } - }) - const summariesData = summaries.data?.getContestScoreSummaries - const summariesLoading = summaries.loading - - const problems = - useQuery(GET_CONTEST_PROBLEMS, { - variables: { groupId: 1, contestId: Number(id) } - }) || [] - const problemData = problems.data?.getContestProblems - .slice() - .sort((a, b) => a.order - b.order) - const problemLoading = problems.loading +import { Suspense } from 'react' +import { + ParticipantTable, + ParticipantTableFallback +} from './_components/ParticipantTable' +export default function Submission({ params }: { params: { id: string } }) { return ( -
- {summariesLoading || problemLoading ? ( - <> -
- - - -
- {[...Array(8)].map((_, i) => ( - - ))} - - ) : ( - <> -

- - {summariesData?.length} - {' '} - Participants -

- - -
- - - - )} -
+ }> + + ) } diff --git a/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/_components/Columns.tsx b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/_components/Columns.tsx index 44dcacd3c3..298ce303a1 100644 --- a/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/_components/Columns.tsx +++ b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/_components/Columns.tsx @@ -8,12 +8,14 @@ import dayjs from 'dayjs' export const columns: ColumnDef[] = [ { accessorKey: 'title', + id: 'problemTitle', header: () => (
Problem Title
), cell: ({ row }) => (
- {String.fromCharCode(65 + row.original.order)}. {row.getValue('title')} + {String.fromCharCode(65 + (row.original.order ?? 0))}.{' '} + {row.getValue('problemTitle')}
) }, diff --git a/apps/frontend/app/admin/contest/[id]/_components/SubmissionDetailAdmin.tsx b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/_components/SubmissionDetailAdmin.tsx similarity index 100% rename from apps/frontend/app/admin/contest/[id]/_components/SubmissionDetailAdmin.tsx rename to apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/_components/SubmissionDetailAdmin.tsx diff --git a/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/_components/SubmissionTable.tsx b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/_components/SubmissionTable.tsx new file mode 100644 index 0000000000..6f96d538bb --- /dev/null +++ b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/_components/SubmissionTable.tsx @@ -0,0 +1,62 @@ +'use client' + +import DataTable from '@/app/admin/_components/table/DataTable' +import DataTableFallback from '@/app/admin/_components/table/DataTableFallback' +import DataTablePagination from '@/app/admin/_components/table/DataTablePagination' +import DataTableProblemFilter from '@/app/admin/_components/table/DataTableProblemFilter' +import DataTableRoot from '@/app/admin/_components/table/DataTableRoot' +import DataTableSearchBar from '@/app/admin/_components/table/DataTableSearchBar' +import { Dialog, DialogContent } from '@/components/ui/dialog' +import { GET_CONTEST_SUBMISSIONS } from '@/graphql/submission/queries' +import { useSuspenseQuery } from '@apollo/client' +import { useState } from 'react' +import { columns } from './Columns' +import SubmissionDetailAdmin from './SubmissionDetailAdmin' + +export function SubmissionTable({ contestId }: { contestId: number }) { + const { data } = useSuspenseQuery(GET_CONTEST_SUBMISSIONS, { + variables: { + input: { + contestId + }, + take: 1000 + } + }) + + const [isSubmissionDialogOpen, setIsSubmissionDialogOpen] = useState(false) + const [submissionId, setSubmissionId] = useState(0) + + return ( + <> + +
+ + +
+ { + setSubmissionId(row.original.id) + setIsSubmissionDialogOpen(true) + }} + /> + +
+ + + + + + + ) +} + +export function SubmissionTableFallback() { + return +} diff --git a/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/page.tsx b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/page.tsx index 2fc2832ddf..ab18cf2914 100644 --- a/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/page.tsx +++ b/apps/frontend/app/admin/contest/[id]/(overall)/@tabs/submission/page.tsx @@ -1,49 +1,13 @@ -'use client' +import { Suspense } from 'react' +import { + SubmissionTable, + SubmissionTableFallback +} from './_components/SubmissionTable' -import type { OverallSubmission } from '@/app/admin/contest/utils' -import { DataTableAdmin } from '@/components/DataTableAdmin' -import { Skeleton } from '@/components/ui/skeleton' -import { GET_CONTEST_SUBMISSIONS } from '@/graphql/submission/queries' -import { useQuery } from '@apollo/client' -import { useParams } from 'next/navigation' -import { columns } from './_components/Columns' - -export default function Submission() { - const { id } = useParams() - const { data, loading } = useQuery(GET_CONTEST_SUBMISSIONS, { - variables: { - input: { - contestId: Number(id) - }, - take: 5000 - } - }) - const submissions = data?.getContestSubmissions || [] +export default function Submission({ params }: { params: { id: string } }) { return ( -
- {loading ? ( - <> -
- - - -
- {[...Array(8)].map((_, i) => ( - - ))} - - ) : ( - - )} -
+ }> + + ) } diff --git a/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/ScoreColumns.tsx b/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/ScoreColumns.tsx index 4db0c333d5..f3bc5bde92 100644 --- a/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/ScoreColumns.tsx +++ b/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/ScoreColumns.tsx @@ -3,9 +3,24 @@ import type { ProblemData, ScoreSummary } from '@/app/admin/contest/utils' import type { ColumnDef, Row } from '@tanstack/react-table' -export const scoreColumns = ( +interface DataTableScoreSummary + extends Pick< + ScoreSummary, + | 'contestPerfectScore' + | 'submittedProblemCount' + | 'userContestScore' + | 'totalProblemCount' + > { + id: number + problemScores: { + problemId: number + score: number + }[] +} + +export const createColumns = ( problemData: ProblemData[] -): ColumnDef[] => { +): ColumnDef[] => { return [ { accessorKey: 'submittedProblemCount', @@ -32,7 +47,7 @@ export const scoreColumns = ( {String.fromCharCode(Number(65 + i))}

), - cell: ({ row }: { row: Row }) => { + cell: ({ row }: { row: Row }) => { const problemScore = row.original.problemScores.find( (ps) => ps.problemId === problem.problemId ) diff --git a/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/ScoreTable.tsx b/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/ScoreTable.tsx new file mode 100644 index 0000000000..fa9b659dd0 --- /dev/null +++ b/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/ScoreTable.tsx @@ -0,0 +1,45 @@ +'use client' + +import DataTable from '@/app/admin/_components/table/DataTable' +import DataTableFallback from '@/app/admin/_components/table/DataTableFallback' +import DataTableRoot from '@/app/admin/_components/table/DataTableRoot' +import { GET_CONTEST_SUBMISSION_SUMMARIES_OF_USER } from '@/graphql/contest/queries' +import { GET_CONTEST_PROBLEMS } from '@/graphql/problem/queries' +import { useSuspenseQuery } from '@apollo/client' +import { createColumns } from './ScoreColumns' + +interface ScoreTableProps { + contestId: number + userId: number +} + +export function ScoreTable({ userId, contestId }: ScoreTableProps) { + const submissions = useSuspenseQuery( + GET_CONTEST_SUBMISSION_SUMMARIES_OF_USER, + { + variables: { contestId, userId, take: 1000 } + } + ) + const scoreData = + submissions.data.getContestSubmissionSummaryByUserId.scoreSummary + + const problems = useSuspenseQuery(GET_CONTEST_PROBLEMS, { + variables: { groupId: 1, contestId } + }) + const problemData = problems.data.getContestProblems + .slice() + .sort((a, b) => a.order - b.order) + + return ( + + + + ) +} + +export function ScoreTableFallback() { + return +} diff --git a/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/SubmissionColumns.tsx b/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/SubmissionColumns.tsx index afd339a018..3d3ffb9f75 100644 --- a/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/SubmissionColumns.tsx +++ b/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/SubmissionColumns.tsx @@ -11,7 +11,7 @@ export const submissionColumns: ColumnDef[] = [ header: () =>
Problem Title
, cell: ({ row }) => (
- {String.fromCharCode(65 + row.original.order)}.{' '} + {String.fromCharCode(65 + (row.original.order ?? 0))}.{' '} {row.getValue('problemTitle')}
) diff --git a/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/SubmissionTable.tsx b/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/SubmissionTable.tsx new file mode 100644 index 0000000000..8414716c6e --- /dev/null +++ b/apps/frontend/app/admin/contest/[id]/participant/[userId]/_components/SubmissionTable.tsx @@ -0,0 +1,44 @@ +'use client' + +import DataTable from '@/app/admin/_components/table/DataTable' +import DataTableFallback from '@/app/admin/_components/table/DataTableFallback' +import DataTablePagination from '@/app/admin/_components/table/DataTablePagination' +import DataTableProblemFilter from '@/app/admin/_components/table/DataTableProblemFilter' +import DataTableRoot from '@/app/admin/_components/table/DataTableRoot' +import { GET_CONTEST_SUBMISSION_SUMMARIES_OF_USER } from '@/graphql/contest/queries' +import { useSuspenseQuery } from '@apollo/client' +import { submissionColumns } from './SubmissionColumns' + +export function SubmissionTable({ + contestId, + userId +}: { + contestId: number + userId: number +}) { + const submissions = useSuspenseQuery( + GET_CONTEST_SUBMISSION_SUMMARIES_OF_USER, + { + variables: { contestId, userId, take: 1000 } + } + ) + + const submissionsData = + submissions.data.getContestSubmissionSummaryByUserId.submissions + + return ( + + + + + + ) +} + +export function SubmissionTableFallback() { + return +} diff --git a/apps/frontend/app/admin/contest/[id]/participant/[userId]/page.tsx b/apps/frontend/app/admin/contest/[id]/participant/[userId]/page.tsx index 5b7437ff63..1bac184488 100644 --- a/apps/frontend/app/admin/contest/[id]/participant/[userId]/page.tsx +++ b/apps/frontend/app/admin/contest/[id]/participant/[userId]/page.tsx @@ -1,55 +1,36 @@ 'use client' -import { DataTableAdmin } from '@/components/DataTableAdmin' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' -import { Skeleton } from '@/components/ui/skeleton' -import { GET_CONTEST_SUBMISSION_SUMMARIES_OF_USER } from '@/graphql/contest/queries' -import { GET_CONTEST_PROBLEMS } from '@/graphql/problem/queries' import { GET_GROUP_MEMBER } from '@/graphql/user/queries' import { useQuery } from '@apollo/client' import Link from 'next/link' +import { Suspense } from 'react' import { FaAngleLeft } from 'react-icons/fa6' -import type { ScoreSummary, ProblemData, UserSubmission } from '../../../utils' -import { scoreColumns } from './_components/ScoreColumns' -import { submissionColumns } from './_components/SubmissionColumns' +import { ScoreTable, ScoreTableFallback } from './_components/ScoreTable' +import { + SubmissionTable, + SubmissionTableFallback +} from './_components/SubmissionTable' export default function Page({ params }: { params: { id: string; userId: string } }) { - const { id, userId } = params + const contestId = Number(params.id) + const userId = Number(params.userId) const user = useQuery(GET_GROUP_MEMBER, { - variables: { groupId: 1, userId: Number(userId) } + variables: { groupId: 1, userId } }) const userData = user.data?.getGroupMember - const submissions = useQuery(GET_CONTEST_SUBMISSION_SUMMARIES_OF_USER, { - variables: { contestId: Number(id), userId: Number(userId), take: 5000 } - }) - const submissionsLoading = submissions.loading - const scoreData = - submissions.data?.getContestSubmissionSummaryByUserId.scoreSummary || [] - const submissionsData = - submissions.data?.getContestSubmissionSummaryByUserId.submissions || [] - - const problems = - useQuery(GET_CONTEST_PROBLEMS, { - variables: { groupId: 1, contestId: Number(id) }, - onCompleted: (data) => console.log(data.getContestProblems) - }) || [] - const problemData = problems.data?.getContestProblems - .slice() - .sort((a, b) => a.order - b.order) - const problemLoading = problems.loading - return (
- + @@ -72,37 +53,13 @@ export default function Page({
-
- {submissionsLoading || problemLoading ? ( - <> -
- - - -
- {[...Array(8)].map((_, i) => ( - - ))} - - ) : ( -
- - -
- )} +
+ }> + + + }> + +
diff --git a/apps/frontend/app/admin/contest/_components/DuplicateContest.tsx b/apps/frontend/app/admin/contest/_components/DuplicateContest.tsx deleted file mode 100644 index 0702d654e8..0000000000 --- a/apps/frontend/app/admin/contest/_components/DuplicateContest.tsx +++ /dev/null @@ -1,124 +0,0 @@ -'use client' - -import { - AlertDialog, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogCancel, - AlertDialogAction -} from '@/components/ui/alert-dialog' -import { Button } from '@/components/ui/button' -import { DUPLICATE_CONTEST } from '@/graphql/contest/mutations' -import { useMutation } from '@apollo/client' -import { CopyIcon } from 'lucide-react' -import { useRouter } from 'next/navigation' -import { toast } from 'sonner' - -export default function DuplicateContest({ - groupId, - contestId, - contestStatus -}: { - groupId: number - contestId: number - contestStatus: string -}) { - const router = useRouter() - const [duplicateContest, { error }] = useMutation(DUPLICATE_CONTEST) - - const duplicateContestById = async () => { - if (error) { - console.error(error) - return - } - const toastId = toast.loading('Duplicating contest...') - - const res = await duplicateContest({ - variables: { - groupId, - contestId - } - }) - - if (res.data?.duplicateContest.contest) { - toast.success( - `Contest duplicated completed.\n Duplicated contest title: ${res.data.duplicateContest.contest.title}`, - { - id: toastId - } - ) - } - router.refresh() - } - - return ( -
- - - - - - - - Duplicate {contestStatus === 'ongoing' ? 'Ongoing ' : ''}Contest - - -

Contents That Will Be Copied:

-
    -
  • Title
  • -
  • Start Time & End Time
  • -
  • Description
  • -
  • - Contest Security Settings (invitation code, allow copy/paste) -
  • -
  • Contest Problems
  • -
  • - Participants of the selected contest
    - - (All participants of the selected contest will be - automatically registered for the duplicated contest.) - -
  • -
-

Contents That Will Not Be Copied:

-
    -
  • Users' Submissions
  • -
- {contestStatus === 'ongoing' ? ( -

- Caution: The new contest will be set to visible. -

- ) : ( -

- Caution: The new contest will be set to invisible -

- )} -

- Are you sure you want to proceed with duplicating the selected - contest? -

-
-
- - - Cancel - - - Duplicate - - -
-
-
- ) -} diff --git a/apps/frontend/app/admin/contest/utils.ts b/apps/frontend/app/admin/contest/utils.ts index 2a1cceff34..2af0e021c8 100644 --- a/apps/frontend/app/admin/contest/utils.ts +++ b/apps/frontend/app/admin/contest/utils.ts @@ -65,7 +65,8 @@ export interface OverallSubmission { codeSize?: number | null ip?: string | null id: number - order: number + order?: number | null + problemId: number } export interface UserSubmission { @@ -76,5 +77,5 @@ export interface UserSubmission { codeSize?: number | null ip?: string | null id: number - order: number + order?: number | null } diff --git a/apps/frontend/components/DataTableAdmin.tsx b/apps/frontend/components/DataTableAdmin.tsx deleted file mode 100644 index 7141b6c708..0000000000 --- a/apps/frontend/components/DataTableAdmin.tsx +++ /dev/null @@ -1,566 +0,0 @@ -'use client' - -import SubmissionDetailAdmin from '@/app/admin/contest/[id]/_components/SubmissionDetailAdmin' -import DuplicateContest from '@/app/admin/contest/_components/DuplicateContest' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle -} from '@/components/ui/alert-dialog' -import { Button } from '@/components/ui/button' -import { Dialog, DialogContent } from '@/components/ui/dialog' -import { - Table, - TableBody, - TableCell, - TableFooter, - TableHead, - TableHeader, - TableRow -} from '@/components/ui/table' -import { - Tooltip, - TooltipContent, - TooltipTrigger, - TooltipProvider -} from '@/components/ui/tooltip' -import { DELETE_CONTEST } from '@/graphql/contest/mutations' -import { GET_BELONGED_CONTESTS } from '@/graphql/contest/queries' -import { DELETE_PROBLEM } from '@/graphql/problem/mutations' -import { getStatusWithStartEnd } from '@/lib/utils' -import { useLazyQuery, useMutation } from '@apollo/client' -import type { ColumnDef, SortingState } from '@tanstack/react-table' -import { - flexRender, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable -} from '@tanstack/react-table' -import { CopyIcon } from 'lucide-react' -import type { Route } from 'next' -import { usePathname, useRouter } from 'next/navigation' -import { useEffect, useState, Suspense } from 'react' -import { IoSearch } from 'react-icons/io5' -import { PiTrashLight } from 'react-icons/pi' -import { toast } from 'sonner' -import DataTableLangFilter from './DataTableLangFilter' -import DataTableLevelFilter from './DataTableLevelFilter' -import { DataTablePagination } from './DataTablePagination' -import DataTableProblemFilter from './DataTableProblemFilter' -import { Input } from './ui/input' - -interface DataTableProps { - columns: ColumnDef[] - data: TData[] - enableSearch?: boolean // Enable search title - searchColumn?: string - enableFilter?: boolean // Enable filter for languages and tags - enableProblemFilter?: boolean // Enable filter for problems - enableDelete?: boolean // Enable delete selected rows - enablePagination?: boolean // Enable pagination - enableRowsPerpage?: boolean - enableImport?: boolean // Enable import selected rows - enableDuplicate?: boolean // Enable duplicate selected rows - checkedRows?: ContestProblem[] // Check selected rows - headerStyle?: { - [key: string]: string - } - onSelectedExport?: (selectedRows: ContestProblem[]) => void - defaultSortColumn?: { id: string; desc: boolean } - enableFooter?: boolean - defaultPageSize?: number -} - -interface ContestProblem { - id: number - title: string - difficulty: string -} - -interface SelectedContest { - original: { - id: number - startTime: string - endTime: string - } -} - -const languageOptions = ['C', 'Cpp', 'Java', 'Python3'] -const levels = ['Level1', 'Level2', 'Level3', 'Level4', 'Level5'] - -export function DataTableAdmin({ - columns, - data, - enableSearch = false, - searchColumn = 'title', - enableFilter = false, - enableProblemFilter = false, - enableDelete = false, - enablePagination = false, - enableRowsPerpage = true, - enableImport = false, - checkedRows = [], - headerStyle = {}, - enableDuplicate = false, - onSelectedExport = () => {}, - defaultSortColumn = { id: '', desc: false }, - enableFooter = false, - defaultPageSize = 10 -}: DataTableProps) { - const [rowSelection, setRowSelection] = useState({}) - const [sorting, setSorting] = useState([]) - const [defaultSortExists, setDefaultExists] = useState(defaultSortColumn.id) - useEffect(() => { - if (defaultSortExists) - setSorting([{ id: defaultSortColumn.id, desc: defaultSortColumn.desc }]) - setDefaultExists('') - }, [defaultSortExists, defaultSortColumn]) - const pathname = usePathname() - const page = pathname.split('/').pop() - const router = useRouter() - const selectedRowCount = Object.values(rowSelection).filter(Boolean).length - const table = useReactTable({ - data, - columns, - defaultColumn: { - minSize: 0, - size: Number.MAX_SAFE_INTEGER, - maxSize: Number.MAX_SAFE_INTEGER - }, - state: { - sorting: enableImport - ? [{ id: 'select', desc: true }, ...sorting] - : sorting, - rowSelection, - columnVisibility: { languages: false } - }, - autoResetPageIndex: false, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues() - }) - - useEffect(() => { - table.setPageSize(defaultPageSize) - }, [defaultPageSize, table]) - - let deletingObject - if (pathname === '/admin/contest') { - deletingObject = 'contest' - } else { - deletingObject = 'problem' - } - - const [deleteProblem] = useMutation(DELETE_PROBLEM) - const [deleteContest] = useMutation(DELETE_CONTEST) - const [isDeleteAlertDialogOpen, setIsDeleteAlertDialogOpen] = useState(false) - const [isSubmissionDialogOpen, setIsSubmissionDialogOpen] = useState(false) - const [submissionId, setSubmissionId] = useState(0) - - useEffect(() => { - if (checkedRows.length !== 0) { - const problemIds = checkedRows.map((problem) => problem.id) - const problemIndex = data.reduce((acc: number[], problem, index) => { - if (problemIds.includes((problem as { id: number }).id)) { - acc.push(index as number) - } - return acc - }, []) - setRowSelection( - problemIndex.reduce( - (acc: { [key: number]: boolean }, index: number) => ({ - ...acc, - [index]: true - }), - {} - ) - ) - } - }, [checkedRows, data]) - - const handleImportProblems = async () => { - const selectedProblems = table.getSelectedRowModel().rows as { - original: { id: number; title: string; difficulty: string; score: number } - }[] - const problems = selectedProblems.map((problem) => ({ - id: problem.original.id, - title: problem.original.title, - difficulty: problem.original.difficulty, - score: problem.original.score ?? 0 // Score 기능 완료되면 수정해주세요!! - })) - onSelectedExport(problems) - } - - // TODO: notice도 같은 방식으로 추가 - const handleDeleteRows = async () => { - const selectedRows = table.getSelectedRowModel().rows as { - original: { id: number } - }[] - const deletePromise = selectedRows.map((row) => { - if (page === 'problem') { - return deleteProblem({ - variables: { - groupId: 1, - id: row.original.id - } - }) - } else if (page === 'contest') { - return deleteContest({ - variables: { - groupId: 1, - contestId: row.original.id - } - }) - } else { - return Promise.resolve() - } - }) - await Promise.all(deletePromise) - .then(() => { - setRowSelection({}) - router.refresh() - }) - .catch(() => { - toast.error(`Failed to delete ${page}`) - }) - } - - const [fetchContests] = useLazyQuery(GET_BELONGED_CONTESTS) - - const handleDeleteButtonClick = async () => { - if (page === 'problem') { - const selectedRows = table.getSelectedRowModel().rows as { - original: { id: number } - }[] - const promises = selectedRows.map((row) => - fetchContests({ - variables: { - problemId: Number(row.original.id) - } - }).then((result) => result.data) - ) - const results = await Promise.all(promises) - const isAllSafe = !results.some((data) => data !== undefined) - if (isAllSafe) { - setIsDeleteAlertDialogOpen(true) - } else { - setIsDeleteAlertDialogOpen(false) - toast.error('Failed : Problem included in the contest') - } - } else { - setIsDeleteAlertDialogOpen(true) - } - } - - return ( -
- - {(enableSearch || - enableFilter || - enableImport || - enableDelete || - enableDuplicate) && ( -
-
- {enableSearch && ( -
- - { - table - .getColumn(searchColumn) - ?.setFilterValue(event.target.value) - table.setPageIndex(0) - }} - className="h-10 w-[150px] bg-transparent pl-8 lg:w-[250px]" - /> -
- )} - {enableFilter && ( -
- {table.getColumn('languages') && ( - - )} - {table.getColumn('difficulty') && ( - - )} - {enableProblemFilter && ( - - )} -
- )} -
-
- {enableImport ? ( - - ) : null} - {enableDuplicate ? ( - selectedRowCount === 1 ? ( - - ) : ( - - - - - - -

Select only one contest to duplicate

-
-
-
- ) - ) : null} - {enableDelete ? ( - selectedRowCount !== 0 ? ( -
- - - - - Delete - - Are you sure you want to permanently delete{' '} - {selectedRowCount} {deletingObject}(s)? - - - - Cancel - - - - - - -
- ) : ( - - ) - ) : null} -
-
- )} - -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => { - const participantRegex = /^\/admin\/contest\/\d+$/ - const href = (() => { - if (participantRegex.test(pathname)) { - return `${pathname}/participant/${(row.original as { userId: number }).userId}` as Route - } - if (page === 'contest') { - return `/admin/contest/${(row.original as { id: number }).id}` as Route - } - if (page === 'problem') { - return `/admin/problem/${(row.original as { id: number }).id}` as Route - } - if (page === 'submission') { - const submission = row.original as { - problemId: number - id: number - } - return `/contest/${pathname.split('/')[3]}/problem/${submission.problemId}/submission/${submission.id}` as Route - } - if (pathname.includes('participant') && enableProblemFilter) { - const submission = row.original as { - contestId: number - problemId: number - id: number - } - return `/contest/${submission.contestId}/problem/${submission.problemId}/submission/${submission.id}` as Route - } - return '' - })() - return ( - - {row.getVisibleCells().map((cell) => ( - { - if (enableImport) { - const selectedRowCount = - table.getSelectedRowModel().rows.length - if (selectedRowCount < 20 || row.getIsSelected()) { - row.toggleSelected(!row.getIsSelected()) - } else { - toast.error( - 'You can only import up to 20 problems in a contest' - ) - } - } else { - if (href) { - if (href.includes('submission')) { - const submissionId = Number(href.split('/')[6]) - setSubmissionId(submissionId) - setIsSubmissionDialogOpen(true) - } else { - router.push(href) - } - } - } - }} - > - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ) - }) - ) : ( - - - No results. - - - )} - - {enableFooter && ( - - {table.getFooterGroups().map((footerGroup) => ( - - {footerGroup.headers.map((footer) => { - return ( - - {footer.isPlaceholder - ? null - : flexRender( - footer.column.columnDef.footer, - footer.getContext() - )} - - ) - })} - - ))} - - )} -
-
- {enablePagination && ( - - )} - { - setIsSubmissionDialogOpen(false) - }} - > - - - - -
- ) -} diff --git a/apps/frontend/components/DataTableColumnHeader.tsx b/apps/frontend/components/DataTableColumnHeader.tsx deleted file mode 100644 index bd40ec581c..0000000000 --- a/apps/frontend/components/DataTableColumnHeader.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Button } from '@/components/ui/button' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu' -import { cn } from '@/lib/utils' -import { TriangleDownIcon, TriangleUpIcon } from '@radix-ui/react-icons' -import type { Column } from '@tanstack/react-table' - -interface DataTableColumnHeaderProps - extends React.HTMLAttributes { - column: Column - title: string -} - -export function DataTableColumnHeader({ - column, - title, - className -}: DataTableColumnHeaderProps) { - // Title column - if (!column.getCanSort()) { - return ( -
- {title} -
- ) - } - - return ( -
- - - - - - column.toggleSorting(false)}> - - {title === 'Visible' ? 'Hidden first' : 'Asc'} - - column.toggleSorting(true)}> - - {title === 'Visible' ? 'Visible first' : 'Desc'} - - - -
- ) -} diff --git a/apps/frontend/components/DataTableLangFilter.tsx b/apps/frontend/components/DataTableLangFilter.tsx deleted file mode 100644 index 50ae90a618..0000000000 --- a/apps/frontend/components/DataTableLangFilter.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Checkbox } from '@/components/ui/checkbox' -import { - Command, - CommandEmpty, - CommandGroup, - CommandItem, - CommandList -} from '@/components/ui/command' -import { - Popover, - PopoverContent, - PopoverTrigger -} from '@/components/ui/popover' -import { Separator } from '@/components/ui/separator' -import type { Column } from '@tanstack/react-table' -import { IoFilter } from 'react-icons/io5' - -interface DataTableLangFilterProps { - column?: Column - title?: string - options: string[] -} - -export default function DataTableLangFilter({ - column, - title, - options -}: DataTableLangFilterProps) { - const selectedValues = new Set(column?.getFilterValue() as string[]) - - return ( - - - - - - - - - No language found. - - {options.map((option) => ( - - { - if (selectedValues.has(option)) { - selectedValues.delete(option) - } else { - selectedValues.add(option) - } - const filterValues = Array.from(selectedValues) - column?.setFilterValue( - filterValues.length ? filterValues : undefined - ) - }} - /> - {option} - - ))} - - - - - - ) -} diff --git a/apps/frontend/components/DataTableLevelFilter.tsx b/apps/frontend/components/DataTableLevelFilter.tsx deleted file mode 100644 index ed82730bc5..0000000000 --- a/apps/frontend/components/DataTableLevelFilter.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Checkbox } from '@/components/ui/checkbox' -import { - Command, - CommandEmpty, - CommandGroup, - CommandItem, - CommandList -} from '@/components/ui/command' -import { - Popover, - PopoverContent, - PopoverTrigger -} from '@/components/ui/popover' -import { Separator } from '@/components/ui/separator' -import type { Column } from '@tanstack/react-table' -import { IoFilter } from 'react-icons/io5' - -interface DataTableLevelFilterProps { - column?: Column - title?: string - options: string[] -} - -export default function DataTableLevelFilter({ - column, - title, - options -}: DataTableLevelFilterProps) { - const selectedValues = new Set(column?.getFilterValue() as string) - - return ( - - - - - - - - - No level found. - - {options.map((option) => ( - - { - if (selectedValues.has(option)) { - selectedValues.delete(option) - } else { - selectedValues.add(option) - } - const filterValues = Array.from(selectedValues) - column?.setFilterValue( - filterValues.length ? filterValues : undefined - ) - }} - /> - Level {option.slice(-1)} - - ))} - - - - - - ) -} diff --git a/apps/frontend/components/DataTablePagination.tsx b/apps/frontend/components/DataTablePagination.tsx deleted file mode 100644 index 5322b5e74f..0000000000 --- a/apps/frontend/components/DataTablePagination.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from '@/components/ui/select' -import { cn } from '@/lib/utils' -import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons' -import type { Table } from '@tanstack/react-table' - -interface DataTablePaginationProps { - table: Table - showRowsPerPage?: boolean -} - -function pageArray(m: number, n: number): number[] { - return Array(n - m + 1) - .fill(0) - .map((_, i) => m + i) -} - -export function DataTablePagination({ - table, - showRowsPerPage = true -}: DataTablePaginationProps) { - return ( -
-
- {table.getColumn('select') && - `${table.getFilteredSelectedRowModel().rows.length} of${' '} - ${table.getFilteredRowModel().rows.length} row(s) selected`} -
-
- - {pageArray( - Math.floor(table.getState().pagination.pageIndex / 10) * 10 + 1, - Math.min( - Math.ceil( - table.getFilteredRowModel().rows.length / - table.getState().pagination.pageSize - ), - Math.floor(table.getState().pagination.pageIndex / 10) * 10 + 10 - ) - ).map((pageNumber) => ( - - ))} - -
-
- {showRowsPerPage && ( -
-

Rows per page

- -
- )} -
-
- ) -} diff --git a/apps/frontend/components/DataTableProblemFilter.tsx b/apps/frontend/components/DataTableProblemFilter.tsx deleted file mode 100644 index b496a15818..0000000000 --- a/apps/frontend/components/DataTableProblemFilter.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { Button } from '@/components/ui/button' -import { - Command, - CommandGroup, - CommandItem, - CommandList -} from '@/components/ui/command' -import { - Popover, - PopoverContent, - PopoverTrigger -} from '@/components/ui/popover' -import { GET_CONTEST_PROBLEMS } from '@/graphql/problem/queries' -import { cn } from '@/lib/utils' -import { useQuery } from '@apollo/client' -import type { Column } from '@tanstack/react-table' -import { useParams } from 'next/navigation' -import { useState } from 'react' -import { FaCheck, FaChevronDown } from 'react-icons/fa' - -interface DataTableProblemFilterProps { - column?: Column -} - -export default function DataTableLevelFilter({ - column -}: DataTableProblemFilterProps) { - const { id } = useParams() - const [selectedValue, setSelectedValue] = useState( - undefined - ) - const [problemFilterOpen, setProblemFilterOpen] = useState(false) - const [problems, setProblems] = useState([]) - - useQuery(GET_CONTEST_PROBLEMS, { - variables: { groupId: 1, contestId: Number(id) }, - onCompleted: (problemData) => { - const data = problemData.getContestProblems - const sortedData = data.slice().sort((a, b) => a.order - b.order) - setProblems([ - 'All Problems', - ...sortedData.map( - (problem) => - `${String.fromCharCode(65 + problem.order)}. ${problem.problem.title}` - ) - ]) - } - }) - - return ( - - - - - - - - - - {problems.map((option) => ( - { - option === 'All Problems' - ? column?.setFilterValue(null) - : column?.setFilterValue(option.slice(3)) - setProblemFilterOpen(false) - setSelectedValue(option) - }} - > -

- {option} -

- -
- ))} -
-
-
-
-
- ) -}