Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(fe): use tanstack query for MySubmission #2250

Merged
merged 12 commits into from
Jan 5, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import { convertToLetter, dateFormatter } from '@/libs/utils'
import type { ContestProblem } from '@/types/type'
import { ErrorBoundary } from '@suspensive/react'
import type { ColumnDef } from '@tanstack/react-table'
import MySubmission from './MySubmission'
import { Suspense } from 'react'
import { MySubmissionFallback, MySubmission } from './MySubmission'

export const columns: ColumnDef<ContestProblem>[] = [
{
Expand All @@ -30,7 +32,11 @@ export const columns: ColumnDef<ContestProblem>[] = [
cell: ({ row }) =>
row.original.submissionTime && (
<div className="flex items-center justify-center">
<MySubmission problem={row.original} />
<ErrorBoundary fallback={null}>
<Suspense fallback={<MySubmissionFallback />}>
<MySubmission problem={row.original} />
</Suspense>
</ErrorBoundary>
</div>
)
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { submissionQueries } from '@/app/(client)/_libs/queries/submission'
import FetchErrorFallback from '@/components/FetchErrorFallback'
import {
Dialog,
DialogTrigger,
Expand All @@ -10,94 +12,86 @@ import {
TooltipProvider,
TooltipTrigger
} from '@/components/shadcn/tooltip'
import { fetcherWithAuth } from '@/libs/utils'
import seeSubmissionIcon from '@/public/icons/see-submission.svg'
import type { SubmissionDetail, Submission, ContestProblem } from '@/types/type'
import type { ContestProblem } from '@/types/type'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { ErrorBoundary } from '@suspensive/react'
import { useSuspenseQuery } from '@tanstack/react-query'
import Image from 'next/image'
import { useParams } from 'next/navigation'
import { useEffect, useState } from 'react'
import SubmissionDetailContent from './SubmissionDetailContent'

interface SubmissionsResponse {
data: Submission[]
total: number
}
import { useState, Suspense } from 'react'
import {
SubmissionDetailContent,
SubmissionDetailContentFallback
} from './SubmissionDetailContent'

export default function MySubmission({ problem }: { problem: ContestProblem }) {
export function MySubmission({ problem }: { problem: ContestProblem }) {
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
const [submission, setSubmission] = useState<SubmissionDetail | null>(null)
const [submissionId, setSubmissionId] = useState<number | null>(null)
const { contestId } = useParams()
const { contestId: contestIdString } = useParams()
const contestId = Number(contestIdString)

useEffect(() => {
const getSubmission = async () => {
const submissions: SubmissionsResponse = await fetcherWithAuth
.get(`contest/${contestId}/submission`, {
searchParams: {
take: 1,
problemId: problem.id
}
})
.json()
const firstSubmission = submissions.data[0]
setSubmissionId(firstSubmission.id)
const { data: latestSubmissionData } = useSuspenseQuery(
submissionQueries.list({
contestId,
problemId: problem.id,
take: 1
})
)

const submission: SubmissionDetail = await fetcherWithAuth
.get(
`submission/${firstSubmission.id}?problemId=${problem.id}&contestId=${contestId}`
)
.json()
setSubmission(submission)
}
getSubmission()
}, [contestId, problem.id])
const latestSubmission = latestSubmissionData?.data?.[0]
const latestSubmissionId = latestSubmission?.id ?? 0

if (!submission || !submissionId) {
return <Skeleton className="size-[25px]" />
if (!latestSubmissionId) {
return null
}

return (
<>
<Dialog onOpenChange={() => setIsTooltipOpen(false)}>
<TooltipProvider>
<Tooltip>
<DialogTrigger asChild>
<TooltipTrigger asChild>
<Image
src={seeSubmissionIcon}
width={20}
height={20}
alt={'See submission'}
onClick={(e) => {
e.stopPropagation()
setIsTooltipOpen(true)
}}
onMouseEnter={() => setIsTooltipOpen(true)}
onMouseLeave={() => setIsTooltipOpen(false)}
/>
</TooltipTrigger>
</DialogTrigger>
{isTooltipOpen && (
<TooltipContent className="mr-4 bg-white">
<p className="text-xs text-neutral-900">
Click to check your latest submission.
</p>
<TooltipPrimitive.Arrow className="fill-white" />
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<div onClick={(e) => e.stopPropagation()}>
<DialogContent className="max-h-[620px] max-w-[800px] justify-center">
<SubmissionDetailContent
submission={submission}
submissionId={submissionId}
problem={problem}
/>
</DialogContent>
</div>
</Dialog>
</>
<Dialog onOpenChange={() => setIsTooltipOpen(false)}>
<TooltipProvider>
<Tooltip>
<DialogTrigger asChild>
<TooltipTrigger asChild>
<Image
src={seeSubmissionIcon}
width={20}
height={20}
alt={'See submission'}
onClick={(e) => {
e.stopPropagation()
setIsTooltipOpen(true)
}}
onMouseEnter={() => setIsTooltipOpen(true)}
onMouseLeave={() => setIsTooltipOpen(false)}
/>
</TooltipTrigger>
</DialogTrigger>
{isTooltipOpen && (
<TooltipContent className="mr-4 bg-white">
<p className="text-xs text-neutral-900">
Click to check your latest submission.
</p>
<TooltipPrimitive.Arrow className="fill-white" />
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<div onClick={(e) => e.stopPropagation()}>
<DialogContent className="max-h-[620px] max-w-[800px] justify-center">
<ErrorBoundary fallback={FetchErrorFallback}>
<Suspense fallback={<SubmissionDetailContentFallback />}>
<SubmissionDetailContent
contestId={contestId}
submissionId={latestSubmissionId}
problem={problem}
/>
</Suspense>
</ErrorBoundary>
</DialogContent>
</div>
</Dialog>
)
}

export function MySubmissionFallback() {
return <Skeleton className="size-[25px]" />
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
'use client'

import { submissionQueries } from '@/app/(client)/_libs/queries/submission'
import CodeEditor from '@/components/CodeEditor'
import { ScrollArea, ScrollBar } from '@/components/shadcn/scroll-area'
import { Skeleton } from '@/components/shadcn/skeleton'
import {
Table,
TableBody,
Expand All @@ -11,17 +13,28 @@ import {
TableRow
} from '@/components/shadcn/table'
import { dateFormatter, getResultColor } from '@/libs/utils'
import type { ContestProblem, Language, SubmissionDetail } from '@/types/type'
import type { ContestProblem, Language } from '@/types/type'
import { useSuspenseQuery } from '@tanstack/react-query'

export default function SubmissionDetailContent({
submissionId,
submission,
problem
}: {
interface SubmissionDetailProps {
contestId: number
submissionId: number
submission: SubmissionDetail
problem: ContestProblem
}) {
}

export function SubmissionDetailContent({
contestId,
submissionId,
problem
}: SubmissionDetailProps) {
const { data: submission } = useSuspenseQuery(
submissionQueries.detail({
contestId,
submissionId,
problemId: problem.id
})
)

return (
<ScrollArea className="mt-5 max-h-[540px] w-[760px]">
<div className="ml-20 flex w-[612px] flex-col gap-4">
Expand Down Expand Up @@ -75,7 +88,7 @@ export default function SubmissionDetailContent({
<Table className="[&_*]:text-center [&_*]:text-xs [&_*]:hover:bg-transparent [&_td]:p-2 [&_tr]:!border-neutral-200">
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead />
<TableHead className="!text-sm text-black">
Result
</TableHead>
Expand Down Expand Up @@ -121,3 +134,25 @@ export default function SubmissionDetailContent({
</ScrollArea>
)
}

export function SubmissionDetailContentFallback() {
return (
<ScrollArea className="mt-5 max-h-[540px] w-[760px]">
<div className="ml-20 flex w-[612px] flex-col gap-4">
<Skeleton className="h-[28px]" />
<div>
<h2 className="font-bold">Summary</h2>
<Skeleton className="h-[76px]" />
</div>
<div>
<h2 className="font-bold">Testcase</h2>
<Skeleton className="h-[76px]" />
</div>
<div>
<h2 className="mb-3 font-bold">Source Code</h2>
<Skeleton className="h-28" />
</div>
</div>
</ScrollArea>
)
}
47 changes: 47 additions & 0 deletions apps/frontend/app/(client)/_libs/apis/submission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { safeFetcherWithAuth } from '@/libs/utils'
import type { Submission, SubmissionDetail } from '@/types/type'
import type { PaginationQueryParams } from './types'

export interface GetSubmissionListRequest extends PaginationQueryParams {
contestId: number
problemId: number
}

export interface GetSubmissionListResponse {
data: Submission[]
total: number
}

export const getSubmissionList = async ({
contestId,
problemId,
...searchParams
}: GetSubmissionListRequest): Promise<GetSubmissionListResponse> => {
const response = await safeFetcherWithAuth.get(
`contest/${contestId}/submission`,
{
searchParams: { ...searchParams, problemId }
}
)
const data = await response.json<GetSubmissionListResponse>()
return data
}

export interface GetSubmissionDetailRequest {
contestId: number
problemId: number
submissionId: number
}

export const getSubmissionDetail = async ({
contestId,
problemId,
submissionId
}: GetSubmissionDetailRequest): Promise<SubmissionDetail> => {
const response = await safeFetcherWithAuth.get(`submission/${submissionId}`, {
searchParams: { problemId, contestId }
})

const data = await response.json<SubmissionDetail>()
return data
}
39 changes: 39 additions & 0 deletions apps/frontend/app/(client)/_libs/queries/submission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { queryOptions } from '@tanstack/react-query'
import {
getSubmissionList,
getSubmissionDetail,
type GetSubmissionListRequest,
type GetSubmissionDetailRequest
} from '../apis/submission'

export const submissionQueries = {
all: ({ contestId, problemId }: { contestId: number; problemId: number }) =>
['submission', 'contest', contestId, { problemId }] as const,

lists: ({ contestId, problemId }: { contestId: number; problemId: number }) =>
[...submissionQueries.all({ contestId, problemId }), 'list'] as const,

list: ({ contestId, problemId, ...searchParams }: GetSubmissionListRequest) =>
queryOptions({
queryKey: [
...submissionQueries.lists({ contestId, problemId }),
{ ...searchParams }
] as const,
queryFn: () =>
getSubmissionList({ contestId, problemId, ...searchParams })
}),

detail: ({
contestId,
submissionId,
problemId
}: GetSubmissionDetailRequest) =>
queryOptions({
queryKey: [
...submissionQueries.all({ contestId, problemId }),
'detail',
submissionId
] as const,
queryFn: () => getSubmissionDetail({ contestId, submissionId, problemId })
})
}
Loading