Skip to content

Commit

Permalink
fix(fe): resolve 504 timeout error in vercel (#1251)
Browse files Browse the repository at this point in the history
* chore: remove header from main layout

* feat: remove api call from main page

* feat: add header again

* feat: try api call with timeout 3s

* feat: log response time

* feat: use node-fetch

* feat: log fetching timing

* feat: use axios

* feat: disable standalone output

* feat: use nextjs fetch

* feat: use suspense component

* feat: add problem cards

* fix: remove unused component

* feat: use skeleton while waiting for API response

* feat: set API fetch timeout to 5 seconds

* fix: do not show zero time diff

* feat: add error page for timeout

* feat: add loading fallback in notice page

* feat: add loading fallback in problem list page

* fix: ignore timediff hydration warning

* feat: adjust error message width

* feat: add loading fallback in contest page

* fix: resolve build error

* feat(ci): disable linting on nexjts build

* fix(ci): do not perform typecheck for frontend-client

Check: vercel/next.js#53959

* feat(fe): remove images.dt.s

---------

Co-authored-by: Dayong Lee <dayongkr@gmail.com>
  • Loading branch information
2 people authored and cho-to committed Feb 5, 2024
1 parent 47e71b1 commit b2067f2
Show file tree
Hide file tree
Showing 20 changed files with 477 additions and 222 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ jobs:
- name: Check types (frontend)
run: pnpm --filter frontend exec vue-tsc --noEmit

- name: Check types (frontend-client)
run: pnpm --filter frontend-client exec tsc --noEmit
# Typecheck is not performed for frontend-client intentionally.
# Check: https://github.com/vercel/next.js/issues/53959
# Unlike backend, we don't have spec file to check types.
# So typechecking performed in the build step is enough.

lint:
name: Lint
Expand Down
3 changes: 2 additions & 1 deletion frontend-client/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ module.exports = {
{
namedComponents: 'function-declaration'
}
]
],
'func-style': ['off']
}
}
]
Expand Down
58 changes: 58 additions & 0 deletions frontend-client/app/(main)/_components/ContestCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { fetcher } from '@/lib/utils'
import type { Contest } from '@/types/type'
import type { Route } from 'next'
import Link from 'next/link'
import ContestCard from './ContestCard'

const getContests = async () => {
const data: {
ongoing: Contest[]
upcoming: Contest[]
} = await fetcher.get('contest').json()

data.ongoing.forEach((contest) => {
contest.status = 'ongoing'
})
data.upcoming.forEach((contest) => {
contest.status = 'upcoming'
})
let contests = data.ongoing.concat(data.upcoming)

if (contests.length < 3) {
const data: {
finished: Contest[]
} = await fetcher
.get('contest/finished', {
searchParams: {
take: 3
}
})
.json()
data.finished.forEach((contest) => {
contest.status = 'finished'
})
contests = contests.concat(data.finished)
}

return contests.slice(0, 3)
}

export default async function ContestCards() {
const contests = await getContests()

return (
<>
{contests.map((contest) => {
return (
<Link
key={contest.id}
href={`/contest/${contest.id}` as Route}
className="inline-block w-full"
>
<ContestCard contest={contest} />
</Link>
)
})}
</>
)
}
38 changes: 38 additions & 0 deletions frontend-client/app/(main)/_components/ProblemCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { fetcher } from '@/lib/utils'
import type { WorkbookProblem } from '@/types/type'
import type { Route } from 'next'
import Link from 'next/link'
import ProblemCard from './ProblemCard'

const getProblems = async () => {
const problems: WorkbookProblem[] = await fetcher
.get('problem', {
searchParams: {
take: 3,
workbookId: 1
}
})
.json()

return problems
}

export default async function ProblemCards() {
const problems = await getProblems()

return (
<>
{problems.map((problem) => {
return (
<Link
key={problem.problemId}
href={`/problem/${problem.problemId}` as Route}
className="inline-block w-full"
>
<ProblemCard problem={problem} />
</Link>
)
})}
</>
)
}
8 changes: 4 additions & 4 deletions frontend-client/app/(main)/_components/TimeDiff.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface Props {
}

export default function TimeDiff({ timeRef }: Props) {
const [now, setNow] = useState(timeRef)
const [now, setNow] = useState(new Date())

useInterval(() => {
setNow(new Date())
Expand All @@ -25,8 +25,8 @@ export default function TimeDiff({ timeRef }: Props) {
.padStart(2, '0')

return (
<>
{days}D {hours + diff.format(':mm:ss')}
</>
<span suppressHydrationWarning>
{days}d {hours + diff.format(':mm:ss')}
</span>
)
}
56 changes: 27 additions & 29 deletions frontend-client/app/(main)/contest/_components/ContestCardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,33 @@ export default async function Contest({ type }: { type: string }) {
const contests = await getContests()

return (
<div className="flex h-full flex-col ">
<Carousel>
<div className="flex items-center justify-between">
<div className="pb-5 text-2xl font-bold text-gray-700">{type}</div>
<div className="flex items-center justify-end gap-2">
<CarouselPrevious />
<CarouselNext />
</div>
<Carousel>
<div className="flex items-center justify-between">
<div className="pb-5 text-2xl font-bold text-gray-700">{type}</div>
<div className="flex items-center justify-end gap-2">
<CarouselPrevious />
<CarouselNext />
</div>
<CarouselContent className="bottom-30 flex w-full gap-1.5 px-3 py-2">
{contests
.filter(
(contest) => contest.status.toLowerCase() === type.toLowerCase()
)
.map((contest) => (
<CarouselItem key={contest.id}>
<Link
key={contest.id}
href={`/contest/${contest.id}` as Route}
className="inline-block h-[120px] w-[375px]"
>
<ContestCard contest={contest} />
</Link>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevGradient />
<CarouselNextGradient />
</Carousel>
</div>
</div>
<CarouselContent className="bottom-30 flex w-full gap-1.5 px-3 py-2">
{contests
.filter(
(contest) => contest.status.toLowerCase() === type.toLowerCase()
)
.map((contest) => (
<CarouselItem key={contest.id}>
<Link
key={contest.id}
href={`/contest/${contest.id}` as Route}
className="inline-block h-[120px] w-[375px]"
>
<ContestCard contest={contest} />
</Link>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevGradient />
<CarouselNextGradient />
</Carousel>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import DataTable from '@/components/DataTable'
import { fetcher } from '@/lib/utils'
import type { Contest } from '@/types/type'
import { columns } from './Columns'

export default async function FinishedContestTable() {
const data: {
finished: Contest[]
} = await fetcher.get('contest/finished?take=51').json()

data.finished.forEach((contest) => {
contest.status = 'finished'
})

return (
<>
<p className="text-xl font-bold md:text-2xl">Finished</p>
{/* TODO: Add search bar */}
<DataTable
data={data.finished}
columns={columns}
headerStyle={{
title: 'text-left w-2/5 md:w-3/6',
startTime: 'w-1/5 md:w-1/6',
endTime: 'w-1/5 md:w-1/6',
participants: 'w-1/5 md:w-1/6'
}}
name="contest"
/>
</>
)
}
75 changes: 49 additions & 26 deletions frontend-client/app/(main)/contest/page.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,59 @@
import DataTable from '@/components/DataTable'
import { fetcher } from '@/lib/utils'
import type { Contest } from '@/types/type'
import { columns } from './_components/Columns'
import { Skeleton } from '@/components/ui/skeleton'
import { Suspense } from 'react'
import ContestCardList from './_components/ContestCardList'
import FinishedContestTable from './_components/FinishedContestTable'

export default async function Contest() {
const data: {
finished: Contest[]
} = await fetcher.get('contest/finished?take=51').json()
data.finished.forEach((contest) => {
contest.status = 'finished'
})
function ContestCardListFallback() {
return (
<div>
<Skeleton className="mb-8 h-8 w-24" />
<div className="flex gap-8">
<Skeleton className="h-[120px] w-[375px] rounded-xl" />
<Skeleton className="h-[120px] w-[375px] rounded-xl" />
</div>
</div>
)
}

function FinishedContestTableFallback() {
return (
<div>
<Skeleton className="mb-8 h-8 w-24" />
<div className="mt-4 flex">
<span className="w-2/5 md:w-3/6">
<Skeleton className="h-6 w-20" />
</span>
<span className="w-1/5 md:w-1/6">
<Skeleton className="mx-auto h-6 w-20" />
</span>
<span className="w-1/5 md:w-1/6">
<Skeleton className="mx-auto h-6 w-20" />
</span>
<span className="w-1/5 md:w-1/6">
<Skeleton className="mx-auto h-6 w-20" />
</span>
</div>
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="my-10 flex h-8 w-full rounded-xl" />
))}
</div>
)
}

export default function Contest() {
return (
<>
<div className="mb-12 flex flex-col gap-12">
<ContestCardList type="Ongoing" />
<ContestCardList type="Upcoming" />
<Suspense fallback={<ContestCardListFallback />}>
<ContestCardList type="Ongoing" />
</Suspense>
<Suspense fallback={<ContestCardListFallback />}>
<ContestCardList type="Upcoming" />
</Suspense>
</div>
<p className="text-xl font-bold md:text-2xl">Finished</p>
{/* TODO: Add search bar */}
<DataTable
data={data.finished}
columns={columns}
headerStyle={{
title: 'text-left w-2/4 md:w-4/6',
startTime: 'w-1/4 md:w-1/6',
endTime: 'w-1/4 md:w-1/6',
participants: 'w-1/4 md:w-1/6'
}}
name="contest"
/>
<Suspense fallback={<FinishedContestTableFallback />}>
<FinishedContestTable />
</Suspense>
</>
)
}
33 changes: 33 additions & 0 deletions frontend-client/app/(main)/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client'

import { Button } from '@/components/ui/button'
import ErrorImg from '@/public/error.webp'
import Image from 'next/image'
import { useRouter } from 'next/navigation'

interface Props {
error: Error & { digest?: string }
reset: () => void
}

export default function Error({ error }: Props) {
const router = useRouter()

return (
<div className="flex h-full w-full flex-1 flex-col items-center justify-center gap-3 py-12">
<p className="mt-8 text-2xl font-extrabold">Something Went Wrong!</p>
<p className="mb-4 max-w-[36rem] text-lg font-semibold">
{error.message || 'Unknown Error'}
</p>
<Button variant="outline" onClick={router.refresh}>
Reload
</Button>
<Image
src={ErrorImg}
alt="Unexpected Error"
height={240}
className="mt-8"
/>
</div>
)
}
Loading

0 comments on commit b2067f2

Please sign in to comment.