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

feat(fe): implement test in code editor #2093

Merged
merged 22 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b9caa59
feat(fe): add test button, implement poll request
B0XERCAT Sep 13, 2024
cfe2fbc
feat(fe): implement testcase panel with dummy data
B0XERCAT Sep 13, 2024
b2ef37b
chore(fe): edit table row style
B0XERCAT Sep 13, 2024
6db14a5
feat(fe): store test result
B0XERCAT Sep 17, 2024
52e1f4f
Merge branch 'main' into t536-implement-test-in-code-editor
Jaehyeon1020 Sep 20, 2024
9608386
chore(fe): delete console.log
B0XERCAT Sep 30, 2024
975982c
feat(fe): show wrong testcase numbers, adjust table style
B0XERCAT Sep 30, 2024
02df895
feat(fe): implement testcase tabs
B0XERCAT Sep 30, 2024
ccec57b
feat(fe): edit tab style
B0XERCAT Oct 1, 2024
af57b5c
feat(fe): implement test result detail
B0XERCAT Oct 1, 2024
c1b0a62
feat(fe): implement delete tab button
B0XERCAT Oct 1, 2024
32d483a
fix(fe): fix testcase tab style
B0XERCAT Oct 1, 2024
0506003
refactor(fe): componentize TestcaseTab TestSummary TestResultDetail
B0XERCAT Oct 1, 2024
0d99dc2
Merge branch 't536-implement-test-in-code-editor' of https://github.c…
B0XERCAT Oct 1, 2024
720e848
feat(fe): add output in testcase result
B0XERCAT Oct 5, 2024
3525952
chore(fe): specify type statement
B0XERCAT Oct 5, 2024
f22576b
Merge branch 'main' into t536-implement-test-in-code-editor
B0XERCAT Oct 5, 2024
c31ac7c
chore(fe): execute prettier
B0XERCAT Oct 5, 2024
8b80d5a
chore(fe): add padding bottom to panel, add ellipsis to long input ou…
B0XERCAT Oct 5, 2024
17009ed
chore(fe): add pre wrap for labeled field
B0XERCAT Oct 5, 2024
117029a
chore(fe): simplify long styles using truncate
B0XERCAT Oct 5, 2024
51bc166
Merge remote-tracking branch 'origin/main' into t536-implement-test-i…
B0XERCAT Oct 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 89 additions & 9 deletions apps/frontend/components/EditorHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import submitIcon from '@/public/submit.svg'
import useAuthModalStore from '@/stores/authModal'
import {
TestResultsContext,
useLanguageStore,
getKey,
setItem,
Expand All @@ -35,7 +36,8 @@
Language,
ProblemDetail,
Submission,
Template
Template,
TestResult
} from '@/types/type'
import JSConfetti from 'js-confetti'
import { Save } from 'lucide-react'
Expand All @@ -44,7 +46,7 @@
import { useRouter } from 'next/navigation'
import { useContext, useEffect, useRef, useState } from 'react'
import { BsTrash3 } from 'react-icons/bs'
//import { IoPlayCircleOutline } from 'react-icons/io5'
import { IoPlayCircleOutline } from 'react-icons/io5'
import { useInterval } from 'react-use'
import { toast } from 'sonner'
import { useStore } from 'zustand'
Expand All @@ -61,9 +63,12 @@
templateString
}: ProblemEditorProps) {
const { language, setLanguage } = useLanguageStore()
const store = useContext(CodeContext)
if (!store) throw new Error('CodeContext is not provided')
const { code, setCode } = useStore(store)
const codeStore = useContext(CodeContext)
if (!codeStore) throw new Error('CodeContext is not provided')
const { code, setCode } = useStore(codeStore)
const testResultStore = useContext(TestResultsContext)
if (!testResultStore) throw new Error('TestResultsContext is not provided')
const { setTestResults } = useStore(testResultStore)
const [loading, setLoading] = useState(false)
const [submissionId, setSubmissionId] = useState<number | null>(null)
const [templateCode, setTemplateCode] = useState<string | null>(null)
Expand Down Expand Up @@ -119,12 +124,12 @@
)
if (filteredTemplate.length === 0) return
setTemplateCode(filteredTemplate[0].code[0].text)
}, [language])

Check warning on line 127 in apps/frontend/components/EditorHeader.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook useEffect has a missing dependency: 'templateString'. Either include it or remove the dependency array

Check warning on line 127 in apps/frontend/components/EditorHeader.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook useEffect has a missing dependency: 'templateString'. Either include it or remove the dependency array

useEffect(() => {
storageKey.current = getKey(language, problem.id, userName, contestId)
getLocalstorageCode()
}, [userName, problem, contestId, language, templateCode])

Check warning on line 132 in apps/frontend/components/EditorHeader.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook useEffect has a missing dependency: 'getLocalstorageCode'. Either include it or remove the dependency array

Check warning on line 132 in apps/frontend/components/EditorHeader.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook useEffect has a missing dependency: 'getLocalstorageCode'. Either include it or remove the dependency array

const submit = async () => {
if (code === '') {
Expand Down Expand Up @@ -165,6 +170,82 @@
}
}

const submitTest = async () => {
if (code === '') {
toast.error('Please write code before test')
return
}
setLoading(true)
const res = await fetcherWithAuth.post('submission/test', {
json: {
language,
code: [
{
id: 1,
text: code,
locked: false
}
]
},
searchParams: {
problemId: problem.id
},
next: {
revalidate: 0
}
})
if (res.ok) {
pollTestResult()
} else {
setLoading(false)
if (res.status === 401) {
showSignIn()
toast.error('Log in first to test your code')
} else toast.error('Please try again later.')
}
}

const pollTestResult = async () => {
let attempts = 0
const maxAttempts = 10
const pollingInterval = 2000

const poll = async () => {
const res = await fetcherWithAuth.get('submission/test', {
next: {
revalidate: 0
}
})

if (res.ok) {
const resultArray: TestResult[] = await res.json()

setTestResults(resultArray)

const allJudged = resultArray.every(
(submission: TestResult) => submission.result !== 'Judging'
)

if (!allJudged) {
if (attempts < maxAttempts) {
attempts += 1
setTimeout(poll, pollingInterval)
} else {
setLoading(false)
toast.error('Judging took too long. Please try again later.')
}
} else {
setLoading(false)
}
} else {
setLoading(false)
toast.error('Please try again later.')
}
}

poll()
}

const saveCode = async () => {
const session = await auth()
if (!session) {
Expand Down Expand Up @@ -242,16 +323,15 @@
<Save className="stroke-1" size={22} />
Save
</Button>
{/* TODO: Add Test function

<Button
variant={'secondary'}
variant="secondary"
className="h-8 shrink-0 gap-1 rounded-[4px] border-none bg-[#D7E5FE] px-2 font-normal text-[#484C4D] hover:bg-[#c6d3ea]"
onClick={submitTest}
disabled={loading}
>
<IoPlayCircleOutline size={22} />
Test
</Button>
*/}
<Button
className="h-8 shrink-0 gap-1 rounded-[4px] px-2 font-normal"
disabled={loading}
Expand Down
8 changes: 4 additions & 4 deletions apps/frontend/components/EditorLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import EditorResizablePanel from '@/components/EditorResizablePanel'
import HeaderAuthPanel from '@/components/auth/HeaderAuthPanel'
import { auth } from '@/lib/auth'
import { convertToLetter, fetcher, fetcherWithAuth } from '@/lib/utils'
Expand All @@ -9,6 +8,7 @@ import Image from 'next/image'
import Link from 'next/link'
import { FaSortDown } from 'react-icons/fa'
import ContestStatusTimeDiff from './ContestStatusTimeDiff'
import EditorResizablePanelWithContext from './EditorResizablePanelWithContext'
import {
DropdownMenu,
DropdownMenuContent,
Expand Down Expand Up @@ -106,13 +106,13 @@ export default async function EditorLayout({
<HeaderAuthPanel session={session} group={'editor'} />
</div>
</header>
<EditorResizablePanel
<EditorResizablePanelWithContext
contest={contest}
problem={problem}
contestId={contestId}
enableCopyPaste={contest ? contest.enableCopyPaste : true}
>
{children}
</EditorResizablePanel>
</EditorResizablePanelWithContext>
</div>
)
}
61 changes: 51 additions & 10 deletions apps/frontend/components/EditorResizablePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import {
ResizablePanel,
ResizablePanelGroup
} from '@/components/ui/resizable'
import { ScrollArea } from '@/components/ui/scroll-area'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { CodeContext, createCodeStore, useLanguageStore } from '@/stores/editor'
import {
CodeContext,
TestResultsContext,
createCodeStore,
useLanguageStore
} from '@/stores/editor'
import type { Language, ProblemDetail } from '@/types/type'
import type { Route } from 'next'
import Link from 'next/link'
Expand All @@ -17,6 +22,7 @@ import { Suspense, useContext, useEffect } from 'react'
import { useStore } from 'zustand'
import Loading from '../app/problem/[problemId]/loading'
import EditorHeader from './EditorHeader'
import TestcasePanel from './TestcasePanel'

interface ProblemEditorProps {
problem: ProblemDetail
Expand All @@ -34,7 +40,21 @@ export default function EditorMainResizablePanel({
const pathname = usePathname()
const base = contestId ? `/contest/${contestId}` : ''
const { language, setLanguage } = useLanguageStore()
const store = createCodeStore()
const codeStore = createCodeStore()
const testResultStore = useContext(TestResultsContext)
if (!testResultStore) throw new Error('TestResultsContext is not provided')
const { testResults } = useStore(testResultStore)
const testcases = problem.problemTestcase
const testResultData =
testResults.length > 0
? testcases.map((testcase, index) => ({
id: testcase.id,
input: testcase.input,
expectedOutput: testcase.output,
output: testResults[index]?.output,
result: testResults[index]?.result
}))
: null
useEffect(() => {
if (!problem.languages.includes(language)) {
setLanguage(problem.languages[0])
Expand Down Expand Up @@ -91,16 +111,38 @@ export default function EditorMainResizablePanel({

<ResizablePanel defaultSize={65} className="bg-[#222939]">
<div className="grid-rows-editor grid h-full">
<CodeContext.Provider value={store}>
<CodeContext.Provider value={codeStore}>
<EditorHeader
problem={problem}
contestId={contestId}
templateString={problem.template[0]}
/>
<CodeEditorInEditorResizablePanel
templateString={problem.template[0]}
enableCopyPaste={enableCopyPaste}
/>

<ResizablePanelGroup direction="vertical" className="h-32">
<ResizablePanel
defaultSize={60}
className="!overflow-x-auto !overflow-y-auto"
>
<ScrollArea className="h-full bg-[#121728]">
<CodeEditorInEditorResizablePanel
enableCopyPaste={enableCopyPaste}
/>
<ScrollBar orientation="horizontal" />
<ScrollBar orientation="vertical" />
</ScrollArea>
</ResizablePanel>
{testResultData && (
<>
<ResizableHandle
withHandle
className="border-[0.5px] border-slate-700"
/>
<ResizablePanel defaultSize={40}>
<TestcasePanel testResult={testResultData} />
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</CodeContext.Provider>
</div>
</ResizablePanel>
Expand All @@ -109,7 +151,6 @@ export default function EditorMainResizablePanel({
}

interface CodeEditorInEditorResizablePanelProps {
templateString: string
enableCopyPaste: boolean
}

Expand All @@ -123,7 +164,7 @@ function CodeEditorInEditorResizablePanel({

return (
<CodeEditor
value={code}
value={code ?? ''}
language={language as Language}
onChange={setCode}
enableCopyPaste={enableCopyPaste}
Expand Down
31 changes: 31 additions & 0 deletions apps/frontend/components/EditorResizablePanelWithContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client'

import EditorResizablePanel from '@/components/EditorResizablePanel'
import { TestResultsContext, createTestResultsStore } from '@/stores/editor'
import type { Contest, ProblemDetail } from '@/types/type'

export default function EditorResizablePanelWithContext({
problem,
contestId,
contest,
children
}: {
problem: ProblemDetail
contestId?: number | undefined
contest?: Contest | undefined
children: React.ReactNode
}) {
const testResultsStore = createTestResultsStore(problem.id, contestId)

return (
<TestResultsContext.Provider value={testResultsStore}>
<EditorResizablePanel
problem={problem}
contestId={contestId}
enableCopyPaste={contest ? contest.enableCopyPaste : true}
>
{children}
</EditorResizablePanel>
</TestResultsContext.Provider>
)
}
Loading