From cdaed81cd8b21b5d449a86e6d903a6baa2d8c47f Mon Sep 17 00:00:00 2001 From: vishalmishraa Date: Thu, 5 Sep 2024 18:12:05 +0530 Subject: [PATCH] butify problem page --- apps/problems/Calculate-Fibonacci/Problem.md | 12 +- .../(problem)/problem/[problemId]/page.tsx | 41 ++- apps/web/app/globals.css | 12 + apps/web/components/ProblemStatement.tsx | 4 +- apps/web/components/ProblemSubmitBar.tsx | 284 ++---------------- .../problem-page/LanguageSelector.tsx | 34 +++ .../problem-page/RenderTestcase.tsx | 29 ++ .../components/problem-page/Submissions.tsx | 42 +++ .../components/problem-page/SubmitProblem.tsx | 186 ++++++++++++ .../components/problem-page/TestStatus.tsx | 43 +++ apps/web/components/ui/resizable.tsx | 45 +++ apps/web/package.json | 1 + 12 files changed, 455 insertions(+), 278 deletions(-) create mode 100644 apps/web/components/problem-page/LanguageSelector.tsx create mode 100644 apps/web/components/problem-page/RenderTestcase.tsx create mode 100644 apps/web/components/problem-page/Submissions.tsx create mode 100644 apps/web/components/problem-page/SubmitProblem.tsx create mode 100644 apps/web/components/problem-page/TestStatus.tsx create mode 100644 apps/web/components/ui/resizable.tsx diff --git a/apps/problems/Calculate-Fibonacci/Problem.md b/apps/problems/Calculate-Fibonacci/Problem.md index 955d1fe..8646e14 100644 --- a/apps/problems/Calculate-Fibonacci/Problem.md +++ b/apps/problems/Calculate-Fibonacci/Problem.md @@ -1,18 +1,18 @@ -# Problem Statement: Calculate Fibonacci Number +## Problem Statement: Calculate Fibonacci Number -## Objective +### Objective Write a function `calculateFibonacci` that calculates the nth Fibonacci number. A Fibonacci number is a number that appears in the Fibonacci sequence, where each number is the sum of the two preceding ones, starting from 0 and 1. -## Function Signature +### Function Signature `function calculateFibonacci(n)` -## Input +### Input - `n` (int): A non-negative integer to calculate the nth Fibonacci number. -## Output +### Output - Returns the nth Fibonacci number. -## Examples +### Examples - **Input:** `n = 5` - **Output:** `5` - **Explanation:** The 5th Fibonacci number is 5 (0, 1, 1, 2, 3, 5). diff --git a/apps/web/app/(problem)/problem/[problemId]/page.tsx b/apps/web/app/(problem)/problem/[problemId]/page.tsx index ef6705c..6974437 100644 --- a/apps/web/app/(problem)/problem/[problemId]/page.tsx +++ b/apps/web/app/(problem)/problem/[problemId]/page.tsx @@ -1,6 +1,12 @@ import { ProblemStatement } from "../../../../components/ProblemStatement"; import { ProblemSubmitBar } from "../../../../components/ProblemSubmitBar"; import { getProblem } from "../../../../controllers/problem"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable" + export default async function ProblemPage({ params: { problemId }, @@ -26,15 +32,36 @@ export default async function ProblemPage({ ) } -
-
-
- -
-
- +
+ + + +
+
+ +
+
+
+ + + + + +
+ +
+
+ + +
); } + export const dynamic = "force-dynamic"; + + diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 99a7b0c..6abd40e 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -66,4 +66,16 @@ body { @apply bg-background text-foreground; } +} + +@layer utilities { + /* Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } } \ No newline at end of file diff --git a/apps/web/components/ProblemStatement.tsx b/apps/web/components/ProblemStatement.tsx index 8b17f7e..3bd206b 100644 --- a/apps/web/components/ProblemStatement.tsx +++ b/apps/web/components/ProblemStatement.tsx @@ -12,8 +12,8 @@ interface IProblemStatement { export function ProblemStatement({ description, title, tags }: IProblemStatement) { return ( -
- {description} +
+ {description}
); } diff --git a/apps/web/components/ProblemSubmitBar.tsx b/apps/web/components/ProblemSubmitBar.tsx index 877cb67..c05fc6a 100644 --- a/apps/web/components/ProblemSubmitBar.tsx +++ b/apps/web/components/ProblemSubmitBar.tsx @@ -1,35 +1,10 @@ "use client"; -import Editor from "@monaco-editor/react"; +import { useState } from "react"; +import { Submissions } from "./problem-page/Submissions"; +import { SubmitProblem } from "./problem-page/SubmitProblem"; +import { LanguageSelector } from "./problem-page/LanguageSelector"; import { Tabs, TabsList, TabsTrigger } from "./ui/tabs"; -import { Button } from "./ui/button"; -import { Label } from "./ui/label"; -import { - Select, - SelectTrigger, - SelectValue, - SelectContent, - SelectItem, -} from "./ui/select"; -import { useEffect, useState } from "react"; import { LANGUAGE_MAPPING } from "@repo/common/language"; -import axios from "axios"; -import { ISubmission, SubmissionTable } from "./SubmissionTable"; -import { CheckIcon, CircleX, ClockIcon } from "lucide-react"; -import { toast } from "react-toastify"; -import { signIn, useSession } from "next-auth/react"; -import { submissions as SubmissionsType } from "@prisma/client"; -import { Turnstile } from '@marsidev/react-turnstile'; - - -const CLOUD_FLARE_TURNSTILE_SITE_KEY: string = process.env.NEXT_PUBLIC_CLOUD_FLARE_TURNSTILE_SITE_KEY || "0x4AAAAAAAgNypsUam0USvCa"; - -enum SubmitStatus { - SUBMIT = "SUBMIT", - PENDING = "PENDING", - ACCEPTED = "ACCEPTED", - FAILED = "FAILED", -} - export interface IProblem { id: string; @@ -51,247 +26,30 @@ export const ProblemSubmitBar = ({ contestId?: string; }) => { const [activeTab, setActiveTab] = useState("problem"); - - return ( -
-
-
-
- - - Submit - Submissions - - -
-
-
- -
- {activeTab === "submissions" && } -
-
- ) -}; - -function SubmitProblem({ - problem, - contestId, -}: { - problem: IProblem; - contestId?: string; -}) { const [language, setLanguage] = useState( Object.keys(LANGUAGE_MAPPING)[0] as string ); - const [code, setCode] = useState>({}); - const [status, setStatus] = useState(SubmitStatus.SUBMIT); - const [testcases, setTestcases] = useState([]); - const [token, setToken] = useState(""); - const session = useSession(); - - useEffect(() => { - const defaultCode: { [key: string]: string } = {}; - problem.defaultCode.forEach((code) => { - const language = Object.keys(LANGUAGE_MAPPING).find( - (language) => LANGUAGE_MAPPING[language]?.internal === code.languageId - ); - if (!language) return; - defaultCode[language] = code.code; - }); - setCode(defaultCode); - }, [problem]); - - async function pollWithBackoff(id: string, retries: number) { - - if (retries === 0) { - setStatus(SubmitStatus.SUBMIT); - toast.error("Not able to get status "); - return; - } - - const response = await axios.get(`/api/submission/?id=${id}`); - - if (response.data.submission.status === "PENDING") { - setTestcases(response.data.submission.testcases); - await new Promise((resolve) => setTimeout(resolve, 2.5 * 1000)); - pollWithBackoff(id, retries - 1); - } else { - if (response.data.submission.status === "AC") { - setStatus(SubmitStatus.ACCEPTED); - setTestcases(response.data.submission.testcases); - return toast.success("Accepted!"); - } else { - setStatus(SubmitStatus.FAILED); - toast.error("Failed :("); - setTestcases(response.data.submission.testcases); - return; - } - } - } - - async function submit() { - setStatus(SubmitStatus.PENDING); - setTestcases((t) => t.map((tc) => ({ ...tc, status: "PENDING" }))); - try { - const response = await axios.post(`/api/submission/`, { - code: code[language], - languageId: language, - problemId: problem.id, - activeContestId: contestId, - token: token, - }); - - console.log(response.data) - - if (response.data.status === 429) { - setStatus(SubmitStatus.FAILED); - toast.error("Try again after sometime"); - return; - }; - - if (response.data.status === 400) { - setStatus(SubmitStatus.FAILED); - toast.error(`error : ${response.data.message}`); - return; - } - - if (response.data.status != 200) { - setStatus(SubmitStatus.FAILED); - toast.error("Something went wrong"); - return; - }; - - pollWithBackoff(response.data.id, 30); - } catch (e) { - //@ts-ignore - toast.error(e.response.statusText); - setStatus(SubmitStatus.SUBMIT); - } - } - return ( -
- - -
- { }} - options={{ - fontSize: 14, - scrollBeyondLastLine: false, - }} - language={LANGUAGE_MAPPING[language]?.monaco} - onChange={(value) => { - //@ts-ignore - setCode({ ...code, [language]: value }); - }} - defaultLanguage="cpp" - /> +
+
+ + + Submit + Submissions + + + {activeTab === "problem" && }
-
- { - process.env.NODE_ENV === "production" && ( - { - setToken(token) - }} siteKey={CLOUD_FLARE_TURNSTILE_SITE_KEY} /> - ) - } - +
+
- -
- ); -}; - -function RenderTestcase({ testcases }: { testcases: SubmissionsType[] }) { - return ( -
- {testcases.map((testcase, index) => ( -
-
-
Test #{index + 1}
-
-
- {testStatus(testcase.status_id)} -
-
- ))} + {activeTab === "submissions" && }
- ); + ) }; -function testStatus(status: number | null) { - switch (status) { - case 1: - return ; - case 2: - return ; - case 3: - return ; - case 4: - return ; - case 5: - return ; - case 6: - return ; - case 13: - return
Internal Error!
; - case 14: - return
Exec Format Error!
; - default: - return
Runtime Error!
; - } -} - -function Submissions({ problem }: { problem: IProblem }) { - const [submissions, setSubmissions] = useState([]); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - const fetchData = async () => { - const response = await axios.get( - `/api/submission/bulk?problemId=${problem.id}` - ); - setSubmissions(response.data.submissions || []); - }; - fetchData(); - }, []); - - return ( -
- -
- ); -} \ No newline at end of file diff --git a/apps/web/components/problem-page/LanguageSelector.tsx b/apps/web/components/problem-page/LanguageSelector.tsx new file mode 100644 index 0000000..36425ee --- /dev/null +++ b/apps/web/components/problem-page/LanguageSelector.tsx @@ -0,0 +1,34 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { LANGUAGE_MAPPING } from "@repo/common/language"; + + + +const LanguageSelector = ({ language, setLanguage }: { language: string; setLanguage: (language: string) => void }) => { + + return ( + + ); +}; + +export { LanguageSelector }; diff --git a/apps/web/components/problem-page/RenderTestcase.tsx b/apps/web/components/problem-page/RenderTestcase.tsx new file mode 100644 index 0000000..f60badf --- /dev/null +++ b/apps/web/components/problem-page/RenderTestcase.tsx @@ -0,0 +1,29 @@ +import { submissions as SubmissionsType } from "@prisma/client"; +import { testStatus } from "./TestStatus"; +export interface IProblem { + id: string; + title: string; + description: string; + slug: string; + defaultCode: { + languageId: number; + code: string; + }[]; +}; + +export function RenderTestcase({ testcases }: { testcases: SubmissionsType[] }) { + return ( +
+ {testcases.map((testcase, index) => ( +
+
+
Test #{index + 1}
+
+
+ {testStatus(testcase.status_id)} +
+
+ ))} +
+ ); +}; \ No newline at end of file diff --git a/apps/web/components/problem-page/Submissions.tsx b/apps/web/components/problem-page/Submissions.tsx new file mode 100644 index 0000000..7b58755 --- /dev/null +++ b/apps/web/components/problem-page/Submissions.tsx @@ -0,0 +1,42 @@ +import axios from "axios"; +import { useEffect, useState } from "react"; +import { ISubmission, SubmissionTable } from "../SubmissionTable"; +import { Loader2 } from "lucide-react"; +import { SmallPageLoading } from "../Loading"; + +export interface IProblem { + id: string; + title: string; + description: string; + slug: string; + defaultCode: { + languageId: number; + code: string; + }[]; +}; + +export function Submissions({ problem }: { problem: IProblem }) { + const [submissions, setSubmissions] = useState([]); + const [loading, setLoading] = useState(true); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => { + const fetchData = async () => { + const response = await axios.get( + `/api/submission/bulk?problemId=${problem.id}` + ); + setSubmissions(response.data.submissions || []); + setLoading(false); + }; + fetchData(); + }, []); + + return ( +
+ {loading ? ( + + ) : ( + + )} +
+ ); +} \ No newline at end of file diff --git a/apps/web/components/problem-page/SubmitProblem.tsx b/apps/web/components/problem-page/SubmitProblem.tsx new file mode 100644 index 0000000..b097136 --- /dev/null +++ b/apps/web/components/problem-page/SubmitProblem.tsx @@ -0,0 +1,186 @@ +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Turnstile } from '@marsidev/react-turnstile'; +import Editor from "@monaco-editor/react"; +import { LANGUAGE_MAPPING } from "@repo/common/language"; +import axios from "axios"; +import { signIn, useSession } from "next-auth/react"; +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import { Button } from "../ui/button"; +import { RenderTestcase } from "./RenderTestcase"; + + +const CLOUD_FLARE_TURNSTILE_SITE_KEY: string = process.env.NEXT_PUBLIC_CLOUD_FLARE_TURNSTILE_SITE_KEY || "0x4AAAAAAAgNypsUam0USvCa"; + +enum SubmitStatus { + SUBMIT = "SUBMIT", + PENDING = "PENDING", + ACCEPTED = "ACCEPTED", + FAILED = "FAILED", +} + + +export interface IProblem { + id: string; + title: string; + description: string; + slug: string; + defaultCode: { + languageId: number; + code: string; + }[]; +}; + + + + + +export function SubmitProblem({ + problem, + contestId, + language +}: { + problem: IProblem; + contestId?: string; + language:string; +}) { + + const [code, setCode] = useState>({}); + const [status, setStatus] = useState(SubmitStatus.SUBMIT); + const [testcases, setTestcases] = useState([]); + const [token, setToken] = useState(""); + const session = useSession(); + + useEffect(() => { + const defaultCode: { [key: string]: string } = {}; + problem.defaultCode.forEach((code) => { + const language = Object.keys(LANGUAGE_MAPPING).find( + (language) => LANGUAGE_MAPPING[language]?.internal === code.languageId + ); + if (!language) return; + defaultCode[language] = code.code; + }); + setCode(defaultCode); + }, [problem]); + + async function pollWithBackoff(id: string, retries: number) { + + if (retries === 0) { + setStatus(SubmitStatus.SUBMIT); + toast.error("Not able to get status "); + return; + } + + const response = await axios.get(`/api/submission/?id=${id}`); + + if (response.data.submission.status === "PENDING") { + setTestcases(response.data.submission.testcases); + await new Promise((resolve) => setTimeout(resolve, 2.5 * 1000)); + pollWithBackoff(id, retries - 1); + } else { + if (response.data.submission.status === "AC") { + setStatus(SubmitStatus.ACCEPTED); + setTestcases(response.data.submission.testcases); + return toast.success("Accepted!"); + } else { + setStatus(SubmitStatus.FAILED); + toast.error("Failed :("); + setTestcases(response.data.submission.testcases); + return; + } + } + } + + async function submit() { + setStatus(SubmitStatus.PENDING); + setTestcases((t) => t.map((tc) => ({ ...tc, status: "PENDING" }))); + try { + const response = await axios.post(`/api/submission/`, { + code: code[language], + languageId: language, + problemId: problem.id, + activeContestId: contestId, + token: token, + }); + + console.log(response.data) + + if (response.data.status === 429) { + setStatus(SubmitStatus.FAILED); + toast.error("Try again after sometime"); + return; + }; + + if (response.data.status === 400) { + setStatus(SubmitStatus.FAILED); + toast.error(`error : ${response.data.message}`); + return; + } + + if (response.data.status != 200) { + setStatus(SubmitStatus.FAILED); + toast.error("Something went wrong"); + return; + }; + + pollWithBackoff(response.data.id, 30); + } catch (e) { + //@ts-ignore + toast.error(e.response.statusText); + setStatus(SubmitStatus.SUBMIT); + } + } + + return ( +
+
+ { }} + options={{ + fontSize: 14, + scrollBeyondLastLine: false, + + }} + language={LANGUAGE_MAPPING[language]?.monaco} + onChange={(value) => { + //@ts-ignore + setCode({ ...code, [language]: value }); + }} + defaultLanguage="cpp" + + /> +
+
+ { + process.env.NODE_ENV === "production" && ( + { + setToken(token) + }} siteKey={CLOUD_FLARE_TURNSTILE_SITE_KEY} /> + ) + } + +
+ +
+ ); +}; diff --git a/apps/web/components/problem-page/TestStatus.tsx b/apps/web/components/problem-page/TestStatus.tsx new file mode 100644 index 0000000..5e07435 --- /dev/null +++ b/apps/web/components/problem-page/TestStatus.tsx @@ -0,0 +1,43 @@ +import { CheckIcon, CircleX, ClockIcon } from "lucide-react"; + +enum SubmitStatus { + SUBMIT = "SUBMIT", + PENDING = "PENDING", + ACCEPTED = "ACCEPTED", + FAILED = "FAILED", +} + + +export interface IProblem { + id: string; + title: string; + description: string; + slug: string; + defaultCode: { + languageId: number; + code: string; + }[]; +}; + +export function testStatus(status: number | null) { + switch (status) { + case 1: + return ; + case 2: + return ; + case 3: + return ; + case 4: + return ; + case 5: + return ; + case 6: + return ; + case 13: + return
Internal Error!
; + case 14: + return
Exec Format Error!
; + default: + return
Runtime Error!
; + } +} \ No newline at end of file diff --git a/apps/web/components/ui/resizable.tsx b/apps/web/components/ui/resizable.tsx new file mode 100644 index 0000000..f4bc558 --- /dev/null +++ b/apps/web/components/ui/resizable.tsx @@ -0,0 +1,45 @@ +"use client" + +import { GripVertical } from "lucide-react" +import * as ResizablePrimitive from "react-resizable-panels" + +import { cn } from "@/lib/utils" + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps) => ( + +) + +const ResizablePanel = ResizablePrimitive.Panel + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean +}) => ( + div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+) + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle } diff --git a/apps/web/package.json b/apps/web/package.json index ce44175..a505fa1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -33,6 +33,7 @@ "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", + "react-resizable-panels": "^2.1.2", "react-toastify": "^10.0.5", "redis": "^4.7.0", "tailwind-merge": "^2.4.0",