Skip to content

Commit

Permalink
refactor(fe): use tanstack query for client-side data fetching (#2232)
Browse files Browse the repository at this point in the history
* chore(fe): add react query dev tool

* chore(fe): update contest problem and problem detail type

* fix(fe): fix params type

* chore(fe): add todo comment

* feat(fe): add contest problem api call functions and queries

* refactor(fe): use api call function

* refactor(fe): refactor contest problem dropdown component

* refactor(fe): refactor register button component

* refactor(fe): not use suspense query for contest problem dropdown
  • Loading branch information
eunnbi authored Nov 25, 2024
1 parent 611846d commit eef6b0e
Show file tree
Hide file tree
Showing 19 changed files with 319 additions and 293 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,63 +6,61 @@ import {
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/shadcn/dropdown-menu'
import { cn, convertToLetter, fetcherWithAuth } from '@/libs/utils'
import { cn, convertToLetter, isHttpError } from '@/libs/utils'
import checkIcon from '@/public/icons/check-green.svg'
import type { ContestProblem, ProblemDetail } from '@/types/type'
import type { ProblemDetail } from '@/types/type'
import { useQuery } from '@tanstack/react-query'
import Image from 'next/image'
import Link from 'next/link'
import { FaSortDown } from 'react-icons/fa'

interface ContestProblemsResponse {
data: ContestProblem[]
total: number
}
import { contestProblemQueries } from '../../_libs/queries/contestProblem'

interface ContestProblemDropdownProps {
problem: ProblemDetail
problemId: number
problem: Required<ProblemDetail>
contestId: number
}

export default function ContestProblemDropdown({
problem,
problemId,
contestId
}: ContestProblemDropdownProps) {
const { data: contestProblems } = useQuery<
ContestProblemsResponse | undefined
>({
queryKey: ['contest', contestId, 'problems'],
queryFn: () =>
fetcherWithAuth.get(`contest/${contestId}/problem?take=20`).json()
const { data: contestProblems, error } = useQuery({
...contestProblemQueries.list({ contestId, take: 20 }),
throwOnError: false
})

return (
<DropdownMenu>
<DropdownMenuTrigger className="flex gap-1 text-lg text-white outline-none">
<h1>{`${convertToLetter(contestProblems?.data.find((item) => item.id === Number(problemId))?.order as number)}. ${problem.title}`}</h1>
<h1>{`${convertToLetter(problem.order)}. ${problem.title}`}</h1>
<FaSortDown />
</DropdownMenuTrigger>
<DropdownMenuContent className="border-slate-700 bg-slate-900">
{contestProblems?.data.map((p) => (
<Link key={p.id} href={`/contest/${contestId}/problem/${p.id}`}>
<DropdownMenuItem
className={cn(
'flex justify-between text-white hover:cursor-pointer focus:bg-slate-800 focus:text-white',
problem.id === p.id &&
'text-primary-light focus:text-primary-light'
)}
>
{`${convertToLetter(p.order)}. ${p.title}`}
{p.submissionTime && (
<div className="flex items-center justify-center pl-2">
<Image src={checkIcon} alt="check" width={16} height={16} />
</div>
)}
</DropdownMenuItem>
</Link>
))}
{error && isHttpError(error)
? 'Failed to load the contest problem'
: contestProblems?.data.map((p) => (
<Link key={p.id} href={`/contest/${contestId}/problem/${p.id}`}>
<DropdownMenuItem
className={cn(
'flex justify-between text-white hover:cursor-pointer focus:bg-slate-800 focus:text-white',
problem.id === p.id &&
'text-primary-light focus:text-primary-light'
)}
>
{`${convertToLetter(p.order)}. ${p.title}`}
{p.submissionTime && (
<div className="flex items-center justify-center pl-2">
<Image
src={checkIcon}
alt="check"
width={16}
height={16}
/>
</div>
)}
</DropdownMenuItem>
</Link>
))}
</DropdownMenuContent>
</DropdownMenu>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import {
} from '@/components/shadcn/dialog'
import { convertToLetter } from '@/libs/utils'
import compileIcon from '@/public/icons/compile-version.svg'
import type { ContestProblem, ProblemDetail } from '@/types/type'
import type { Level } from '@/types/type'
import type { ProblemDetail } from '@/types/type'
import { sanitize } from 'isomorphic-dompurify'
import { FileText } from 'lucide-react'
import Image from 'next/image'
Expand All @@ -27,24 +26,23 @@ import { WhitespaceVisualizer } from './WhitespaceVisualizer'

export function EditorDescription({
problem,
contestProblems,
isContest = false
}: {
problem: ProblemDetail
contestProblems?: ContestProblem[]
isContest?: boolean
}) {
const level = problem.difficulty
const levelNumber = level.slice(-1)

return (
<div className="dark flex h-full flex-col gap-6 bg-[#222939] py-6 text-lg">
<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>
<h1 className="mb-3 overflow-hidden text-ellipsis whitespace-nowrap text-xl font-bold">{`#${problem?.order !== undefined ? convertToLetter(problem.order) : problem.id}. ${problem.title}`}</h1>
{!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}
textColors={level}
>
{`Level ${levelNumber}`}
</Badge>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { contestProblemQueries } from '@/app/(client)/_libs/queries/contestProblem'
import {
AlertDialog,
AlertDialogAction,
Expand Down Expand Up @@ -189,9 +190,11 @@ export default function Editor({
storeCodeToLocalStorage(code)
const submission: Submission = await res.json()
setSubmissionId(submission.id)
queryClient.refetchQueries({
queryKey: ['contest', contestId, 'problems']
})
if (contestId) {
queryClient.invalidateQueries({
queryKey: contestProblemQueries.lists(contestId)
})
}
} else {
setIsSubmitting(false)
if (res.status === 401) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { auth } from '@/libs/auth'
import { fetcher, fetcherWithAuth } from '@/libs/utils'
import codedangLogo from '@/public/logos/codedang-editor.svg'
import type { Contest, ProblemDetail } from '@/types/type'
import type { Route } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import { redirect } from 'next/navigation'
import type { GetContestProblemDetailResponse } from '../../_libs/apis/contestProblem'
import ContestProblemDropdown from './ContestProblemDropdown'
import EditorMainResizablePanel from './EditorResizablePanel'

Expand All @@ -23,27 +23,28 @@ export default async function EditorLayout({
children
}: EditorLayoutProps) {
let contest: Contest | undefined
let problem: ProblemDetail
let problem: Required<ProblemDetail>

if (contestId) {
// for getting contest info and problems list

// TODO: use `getContestProblemDetail` from _libs/apis folder & use error boundary
const res = await fetcherWithAuth(
`contest/${contestId}/problem/${problemId}`
)

if (!res.ok && res.status === 403) {
redirect(`/contest/${contestId}/finished/problem/${problemId}`)
}
const ContestProblem: { problem: ProblemDetail } = await res.json()
problem = ContestProblem.problem

const contestProblem = await res.json<GetContestProblemDetailResponse>()
problem = { ...contestProblem.problem, order: contestProblem.order }

contest = await fetcher(`contest/${contestId}`).json()
contest ? (contest.status = 'ongoing') : null // TODO: refactor this after change status interactively
} else {
problem = await fetcher(`problem/${problemId}`).json()
}

// for getting problem detail

const session = await auth()

return (
Expand All @@ -56,16 +57,13 @@ export default async function EditorLayout({
<div className="flex items-center gap-1 font-medium">
{contest ? <>Contest</> : <Link href="/problem">Problem</Link>}
<p className="mx-2"> / </p>
{contest && contestId ? (
{contest ? (
<>
<Link href={`/contest/${contestId}` as Route}>
{contest.title}
</Link>
<Link href={`/contest/${contest.id}`}>{contest.title}</Link>
<p className="mx-2"> / </p>
<ContestProblemDropdown
problem={problem}
problemId={problemId}
contestId={contestId}
contestId={contest.id}
/>
</>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ export default async function layout({
params,
children
}: {
params: { problemId: number; contestId: number }
params: { problemId: string; contestId: string }
children: React.ReactNode
}) {
const { problemId, contestId } = params

return (
<EditorLayout problemId={problemId} contestId={contestId}>
<EditorLayout problemId={Number(problemId)} contestId={Number(contestId)}>
{children}
</EditorLayout>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
import { EditorDescription } from '@/app/(client)/(code-editor)/_components/EditorDescription'
import type { GetContestProblemDetailResponse } from '@/app/(client)/_libs/apis/contestProblem'
import { fetcherWithAuth } from '@/libs/utils'
import type { ContestProblem, ProblemDetail } from '@/types/type'
import { redirect } from 'next/navigation'

export default async function DescriptionPage({
params
}: {
params: { problemId: number; contestId: number }
params: { problemId: string; contestId: string }
}) {
const { problemId, contestId } = params
const res = await fetcherWithAuth(`contest/${contestId}/problem/${problemId}`)

// TODO: use `getContestProblemDetail` from _libs/apis folder & use error boundary
const res = await fetcherWithAuth(`contest/${contestId}/problem/${problemId}`)
if (!res.ok && res.status === 403) {
redirect(`/contest/${contestId}/finished/problem/${problemId}`)
}

const contestProblem: { problem: ProblemDetail } = await res.json()
const { problem, order } = await res.json<GetContestProblemDetailResponse>()

const contestProblems: { problems: ContestProblem[] } = await fetcherWithAuth(
`contest/${params.contestId}/problem`
).json()
return (
<EditorDescription
problem={contestProblem.problem}
contestProblems={contestProblems.problems}
isContest={true}
/>
)
return <EditorDescription problem={{ ...problem, order }} isContest={true} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ export default async function layout({
params,
children
}: {
params: { problemId: number }
params: { problemId: string }
children: React.ReactNode
}) {
const { problemId } = params

return <EditorLayout problemId={problemId}>{children}</EditorLayout>
return <EditorLayout problemId={Number(problemId)}>{children}</EditorLayout>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client'

import { contestProblemQueries } from '@/app/(client)/_libs/queries/contestProblem'
import { Button } from '@/components/shadcn/button'
import { Skeleton } from '@/components/shadcn/skeleton'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'

interface GoToFirstProblemButtonProps {
contestId: number
}

export function GoToFirstProblemButton({
contestId
}: GoToFirstProblemButtonProps) {
const router = useRouter()
const { data: firstProblemId } = useSuspenseQuery({
...contestProblemQueries.list({ contestId, take: 1 }),
select: (data) => data.data.at(0)?.id
})

return (
<Button
className="px-12 py-6 text-lg font-light"
onClick={() =>
router.push(`/contest/${contestId}/problem/${firstProblemId}`)
}
>
Go To First Problem!
</Button>
)
}

export function GoToFirstProblemButtonFallback() {
return <Skeleton className="h-12 w-60" />
}
Loading

0 comments on commit eef6b0e

Please sign in to comment.