From a8e243204bfe6d8c2e51d84733a0490d994f4609 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Fri, 4 Apr 2025 17:03:55 +0800 Subject: [PATCH] feat(layout): refactor problem layout to use DockView for organizing content sections --- bun.lock | 5 + package.json | 1 + src/app/(app)/problems/[id]/@Code/layout.tsx | 18 + src/app/(app)/problems/[id]/@Code/page.tsx | 9 + .../problems/[id]/@Description/layout.tsx | 26 ++ .../(app)/problems/[id]/@Description/page.tsx | 23 ++ .../(app)/problems/[id]/@Solutions/layout.tsx | 28 ++ .../(app)/problems/[id]/@Solutions/page.tsx | 23 ++ .../(app)/problems/[id]/@Submissions/page.tsx | 3 + .../(app)/problems/[id]/@TestResult/page.tsx | 3 + .../(app)/problems/[id]/@Testcase/page.tsx | 3 + .../[id]/@problem/@description/layout.tsx | 18 - .../[id]/@problem/@description/page.tsx | 27 -- .../[id]/@problem/@solution/layout.tsx | 18 - .../problems/[id]/@problem/@solution/page.tsx | 22 -- .../(app)/problems/[id]/@problem/layout.tsx | 58 --- .../[id]/@terminal/@testcase/layout.tsx | 20 - .../[id]/@terminal/@testcase/page.tsx | 24 -- .../(app)/problems/[id]/@terminal/layout.tsx | 30 -- .../[id]/@workspace/@editor/layout.tsx | 18 - .../problems/[id]/@workspace/@editor/page.tsx | 15 - .../(app)/problems/[id]/@workspace/layout.tsx | 30 -- src/app/(app)/problems/[id]/layout.tsx | 63 ++-- src/app/actions/auth.ts | 69 ---- src/app/actions/judge.ts | 346 ------------------ src/app/actions/language-server.ts | 29 -- src/components/dockview.tsx | 2 +- .../workspace/editor/components/header.tsx | 2 +- src/components/loading.tsx | 2 +- src/components/problem-editor.tsx | 2 +- src/config/editor-option.ts | 3 + src/style/mdx.css | 49 --- 32 files changed, 174 insertions(+), 815 deletions(-) create mode 100644 src/app/(app)/problems/[id]/@Code/layout.tsx create mode 100644 src/app/(app)/problems/[id]/@Code/page.tsx create mode 100644 src/app/(app)/problems/[id]/@Description/layout.tsx create mode 100644 src/app/(app)/problems/[id]/@Description/page.tsx create mode 100644 src/app/(app)/problems/[id]/@Solutions/layout.tsx create mode 100644 src/app/(app)/problems/[id]/@Solutions/page.tsx create mode 100644 src/app/(app)/problems/[id]/@Submissions/page.tsx create mode 100644 src/app/(app)/problems/[id]/@TestResult/page.tsx create mode 100644 src/app/(app)/problems/[id]/@Testcase/page.tsx delete mode 100644 src/app/(app)/problems/[id]/@problem/@description/layout.tsx delete mode 100644 src/app/(app)/problems/[id]/@problem/@description/page.tsx delete mode 100644 src/app/(app)/problems/[id]/@problem/@solution/layout.tsx delete mode 100644 src/app/(app)/problems/[id]/@problem/@solution/page.tsx delete mode 100644 src/app/(app)/problems/[id]/@problem/layout.tsx delete mode 100644 src/app/(app)/problems/[id]/@terminal/@testcase/layout.tsx delete mode 100644 src/app/(app)/problems/[id]/@terminal/@testcase/page.tsx delete mode 100644 src/app/(app)/problems/[id]/@terminal/layout.tsx delete mode 100644 src/app/(app)/problems/[id]/@workspace/@editor/layout.tsx delete mode 100644 src/app/(app)/problems/[id]/@workspace/@editor/page.tsx delete mode 100644 src/app/(app)/problems/[id]/@workspace/layout.tsx delete mode 100644 src/app/actions/auth.ts delete mode 100644 src/app/actions/judge.ts delete mode 100644 src/app/actions/language-server.ts delete mode 100644 src/style/mdx.css diff --git a/bun.lock b/bun.lock index e33e95c..a417029 100644 --- a/bun.lock +++ b/bun.lock @@ -37,6 +37,7 @@ "clsx": "^2.1.1", "devicons-react": "^1.4.0", "dockerode": "^4.0.4", + "dockview": "^4.2.1", "github-markdown-css": "^5.8.1", "lucide-react": "^0.482.0", "monaco-editor": "<=0.36.1", @@ -717,6 +718,10 @@ "dockerode": ["dockerode@4.0.4", "https://registry.npmmirror.com/dockerode/-/dockerode-4.0.4.tgz", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.0.1", "uuid": "^10.0.0" } }, "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w=="], + "dockview": ["dockview@4.2.1", "https://registry.npmmirror.com/dockview/-/dockview-4.2.1.tgz", { "dependencies": { "dockview-core": "^4.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-P6T4JiM5yuHa8JH0E/BuPpCv8EMLDoCXtvS169ITRRKfgi+zF98AUrnhW8F+FOXV6QS/5Dtt9ca5YxWgtcQsOQ=="], + + "dockview-core": ["dockview-core@4.2.1", "https://registry.npmmirror.com/dockview-core/-/dockview-core-4.2.1.tgz", {}, "sha512-KaEOMzMdQvWB9e3iRQf9BqerB1sX43wAIhla5uGzkA+irag9wz0F5bkVZyJ5mVqJgqrQdWh+W8j94+L2wY0AmA=="], + "doctrine": ["doctrine@2.1.0", "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], diff --git a/package.json b/package.json index 44dbd99..c8795d5 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "clsx": "^2.1.1", "devicons-react": "^1.4.0", "dockerode": "^4.0.4", + "dockview": "^4.2.1", "github-markdown-css": "^5.8.1", "lucide-react": "^0.482.0", "monaco-editor": "<=0.36.1", diff --git a/src/app/(app)/problems/[id]/@Code/layout.tsx b/src/app/(app)/problems/[id]/@Code/layout.tsx new file mode 100644 index 0000000..337197a --- /dev/null +++ b/src/app/(app)/problems/[id]/@Code/layout.tsx @@ -0,0 +1,18 @@ +import { WorkspaceEditorHeader } from "@/components/features/playground/workspace/editor/components/header"; +import { WorkspaceEditorFooter } from "@/components/features/playground/workspace/editor/components/footer"; + +interface CodeLayoutProps { + children: React.ReactNode; +} + +export default function CodeLayout({ children }: CodeLayoutProps) { + return ( +
+ +
+ {children} +
+ +
+ ); +} diff --git a/src/app/(app)/problems/[id]/@Code/page.tsx b/src/app/(app)/problems/[id]/@Code/page.tsx new file mode 100644 index 0000000..7b1c9f7 --- /dev/null +++ b/src/app/(app)/problems/[id]/@Code/page.tsx @@ -0,0 +1,9 @@ +import { ProblemEditor } from "@/components/problem-editor"; + +export default function CodePage() { + return ( +
+ +
+ ); +} diff --git a/src/app/(app)/problems/[id]/@Description/layout.tsx b/src/app/(app)/problems/[id]/@Description/layout.tsx new file mode 100644 index 0000000..d94c10e --- /dev/null +++ b/src/app/(app)/problems/[id]/@Description/layout.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { notFound } from "next/navigation"; +import { useProblem } from "@/hooks/use-problem"; +import ProblemDescriptionFooter from "@/components/features/playground/problem/description/footer"; + +interface DescriptionLayoutProps { + children: React.ReactNode; +} + +export default function DescriptionLayout({ children }: DescriptionLayoutProps) { + const { problem } = useProblem(); + + if (!problem) { + notFound(); + } + + return ( +
+
+ {children} +
+ +
+ ); +} diff --git a/src/app/(app)/problems/[id]/@Description/page.tsx b/src/app/(app)/problems/[id]/@Description/page.tsx new file mode 100644 index 0000000..5a9b16a --- /dev/null +++ b/src/app/(app)/problems/[id]/@Description/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { notFound } from "next/navigation"; +import { useProblem } from "@/hooks/use-problem"; +import MdxPreview from "@/components/mdx-preview"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; + +export default function DescriptionPage() { + const { problem } = useProblem(); + + if (!problem) { + notFound(); + } + + return ( +
+ + + + +
+ ); +} diff --git a/src/app/(app)/problems/[id]/@Solutions/layout.tsx b/src/app/(app)/problems/[id]/@Solutions/layout.tsx new file mode 100644 index 0000000..a1aa42a --- /dev/null +++ b/src/app/(app)/problems/[id]/@Solutions/layout.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { notFound } from "next/navigation"; +import { useProblem } from "@/hooks/use-problem"; +import ProblemSolutionFooter from "@/components/features/playground/problem/solution/footer"; + +interface SolutionsLayoutProps { + children: React.ReactNode; +} + +export default function SolutionsLayout({ + children, +}: SolutionsLayoutProps) { + const { problem } = useProblem(); + + if (!problem) { + notFound(); + } + + return ( +
+
+ {children} +
+ +
+ ); +} diff --git a/src/app/(app)/problems/[id]/@Solutions/page.tsx b/src/app/(app)/problems/[id]/@Solutions/page.tsx new file mode 100644 index 0000000..d76b819 --- /dev/null +++ b/src/app/(app)/problems/[id]/@Solutions/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { notFound } from "next/navigation"; +import { useProblem } from "@/hooks/use-problem"; +import MdxPreview from "@/components/mdx-preview"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; + +export default function SolutionsPage() { + const { problem } = useProblem(); + + if (!problem) { + notFound(); + } + + return ( +
+ + + + +
+ ); +} diff --git a/src/app/(app)/problems/[id]/@Submissions/page.tsx b/src/app/(app)/problems/[id]/@Submissions/page.tsx new file mode 100644 index 0000000..6c443b7 --- /dev/null +++ b/src/app/(app)/problems/[id]/@Submissions/page.tsx @@ -0,0 +1,3 @@ +export default function SubmissionsPage() { + return
Submissions
; +} diff --git a/src/app/(app)/problems/[id]/@TestResult/page.tsx b/src/app/(app)/problems/[id]/@TestResult/page.tsx new file mode 100644 index 0000000..f71967c --- /dev/null +++ b/src/app/(app)/problems/[id]/@TestResult/page.tsx @@ -0,0 +1,3 @@ +export default function TestResultPage() { + return
Test Result
; +} diff --git a/src/app/(app)/problems/[id]/@Testcase/page.tsx b/src/app/(app)/problems/[id]/@Testcase/page.tsx new file mode 100644 index 0000000..53b838e --- /dev/null +++ b/src/app/(app)/problems/[id]/@Testcase/page.tsx @@ -0,0 +1,3 @@ +export default function TestcasePage() { + return
Testcase
; +} diff --git a/src/app/(app)/problems/[id]/@problem/@description/layout.tsx b/src/app/(app)/problems/[id]/@problem/@description/layout.tsx deleted file mode 100644 index c5072f7..0000000 --- a/src/app/(app)/problems/[id]/@problem/@description/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Suspense } from "react"; -import { Loading } from "@/components/loading"; - -interface ProblemDescriptionLayoutProps { - children: React.ReactNode; -} - -export default function ProblemDescriptionLayout({ - children, -}: ProblemDescriptionLayoutProps) { - return ( -
- }> - {children} - -
- ); -} diff --git a/src/app/(app)/problems/[id]/@problem/@description/page.tsx b/src/app/(app)/problems/[id]/@problem/@description/page.tsx deleted file mode 100644 index 95bac59..0000000 --- a/src/app/(app)/problems/[id]/@problem/@description/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { notFound } from "next/navigation"; -import { useProblem } from "@/hooks/use-problem"; -import MdxPreview from "@/components/mdx-preview"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; -import ProblemDescriptionFooter from "@/components/features/playground/problem/description/footer"; - -export default function ProblemDescriptionPage() { - const { problem } = useProblem(); - - if (!problem) { - notFound(); - } - - return ( - <> -
- - - - -
- - - ); -} diff --git a/src/app/(app)/problems/[id]/@problem/@solution/layout.tsx b/src/app/(app)/problems/[id]/@problem/@solution/layout.tsx deleted file mode 100644 index 0fad412..0000000 --- a/src/app/(app)/problems/[id]/@problem/@solution/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Suspense } from "react"; -import { Loading } from "@/components/loading"; - -interface ProblemSolutionLayoutProps { - children: React.ReactNode; -} - -export default function ProblemSolutionLayout({ - children, -}: ProblemSolutionLayoutProps) { - return ( -
- }> - {children} - -
- ); -} diff --git a/src/app/(app)/problems/[id]/@problem/@solution/page.tsx b/src/app/(app)/problems/[id]/@problem/@solution/page.tsx deleted file mode 100644 index 2776527..0000000 --- a/src/app/(app)/problems/[id]/@problem/@solution/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import { useProblem } from "@/hooks/use-problem"; -import MdxPreview from "@/components/mdx-preview"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; -import ProblemSolutionFooter from "@/components/features/playground/problem/solution/footer"; - -export default function ProblemSolutionPage() { - const { problem } = useProblem(); - - return ( - <> -
- - - - -
- - - ); -} diff --git a/src/app/(app)/problems/[id]/@problem/layout.tsx b/src/app/(app)/problems/[id]/@problem/layout.tsx deleted file mode 100644 index c691e09..0000000 --- a/src/app/(app)/problems/[id]/@problem/layout.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { CircleCheckBigIcon, FileTextIcon, FlaskConicalIcon } from "lucide-react"; - -interface ProblemLayoutProps { - description: React.ReactNode; - solution: React.ReactNode; - submission: React.ReactNode; -} - -export default function ProblemLayout({ - description, - solution, - submission, -}: ProblemLayoutProps) { - return ( - - - - - - - - - - - - - - {description} - - - {solution} - - - {submission} - - - ); -} diff --git a/src/app/(app)/problems/[id]/@terminal/@testcase/layout.tsx b/src/app/(app)/problems/[id]/@terminal/@testcase/layout.tsx deleted file mode 100644 index b236e82..0000000 --- a/src/app/(app)/problems/[id]/@terminal/@testcase/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Suspense } from "react"; -import { Loading } from "@/components/loading"; - -interface TerminalTestcaseLayoutProps { - children: React.ReactNode; -} - -export default function TerminalTestcaseLayout({ - children, -}: TerminalTestcaseLayoutProps) { - return ( -
-
- }> - {children} - -
-
- ); -} diff --git a/src/app/(app)/problems/[id]/@terminal/@testcase/page.tsx b/src/app/(app)/problems/[id]/@terminal/@testcase/page.tsx deleted file mode 100644 index 92342ef..0000000 --- a/src/app/(app)/problems/[id]/@terminal/@testcase/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { RadioIcon } from "lucide-react"; - -export default function TerminalTestcasePage() { - return ( -
-
- -
-
-

Launching in v0.0.1

-

- Expected release date: March 16 at 6:00 PM. -

-
-
-
-
- ); -} diff --git a/src/app/(app)/problems/[id]/@terminal/layout.tsx b/src/app/(app)/problems/[id]/@terminal/layout.tsx deleted file mode 100644 index de8dcde..0000000 --- a/src/app/(app)/problems/[id]/@terminal/layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { SquareCheckIcon } from "lucide-react"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; - -interface TerminalLayoutProps { - testcase: React.ReactNode; -} - -export default function TerminalLayout({ testcase }: TerminalLayoutProps) { - return ( - - - - - - - - - - {testcase} - - - ); -} diff --git a/src/app/(app)/problems/[id]/@workspace/@editor/layout.tsx b/src/app/(app)/problems/[id]/@workspace/@editor/layout.tsx deleted file mode 100644 index f159a71..0000000 --- a/src/app/(app)/problems/[id]/@workspace/@editor/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Suspense } from "react"; -import { Loading } from "@/components/loading"; - -interface WorkspaceEditorLayoutProps { - children: React.ReactNode; -} - -export default function WorkspaceEditorLayout({ - children, -}: WorkspaceEditorLayoutProps) { - return ( -
- }> - {children} - -
- ); -} diff --git a/src/app/(app)/problems/[id]/@workspace/@editor/page.tsx b/src/app/(app)/problems/[id]/@workspace/@editor/page.tsx deleted file mode 100644 index 1ce5bed..0000000 --- a/src/app/(app)/problems/[id]/@workspace/@editor/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ProblemEditor } from "@/components/problem-editor"; -import { WorkspaceEditorHeader } from "@/components/features/playground/workspace/editor/components/header"; -import { WorkspaceEditorFooter } from "@/components/features/playground/workspace/editor/components/footer"; - -export default function WorkspaceEditorPage() { - return ( - <> - -
- -
- - - ); -} diff --git a/src/app/(app)/problems/[id]/@workspace/layout.tsx b/src/app/(app)/problems/[id]/@workspace/layout.tsx deleted file mode 100644 index 93850be..0000000 --- a/src/app/(app)/problems/[id]/@workspace/layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { SquarePenIcon } from "lucide-react"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; - -interface WorkspaceLayoutProps { - editor: React.ReactNode; -} - -export default function WorkspaceLayout({ editor }: WorkspaceLayoutProps) { - return ( - - - - - - - - - - {editor} - - - ); -} diff --git a/src/app/(app)/problems/[id]/layout.tsx b/src/app/(app)/problems/[id]/layout.tsx index 83edc23..95f5f1c 100644 --- a/src/app/(app)/problems/[id]/layout.tsx +++ b/src/app/(app)/problems/[id]/layout.tsx @@ -1,28 +1,28 @@ import prisma from "@/lib/prisma"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@/components/ui/resizable"; import { notFound } from "next/navigation"; +import DockView from "@/components/dockview"; import { ProblemStoreProvider } from "@/providers/problem-store-provider"; import { PlaygroundHeader } from "@/components/features/playground/header"; -interface PlaygroundLayoutProps { +interface ProblemProps { params: Promise<{ id: string }>; - problem: React.ReactNode; - workspace: React.ReactNode; - terminal: React.ReactNode; - ai: React.ReactNode; + Description: React.ReactNode; + Solutions: React.ReactNode; + Submissions: React.ReactNode; + Code: React.ReactNode; + Testcase: React.ReactNode; + TestResult: React.ReactNode; } -export default async function PlaygroundLayout({ +export default async function ProblemLayout({ params, - problem, - workspace, - terminal, - ai, -}: PlaygroundLayoutProps) { + Description, + Solutions, + Submissions, + Code, + Testcase, + TestResult, +}: ProblemProps) { const { id } = await params; const [ @@ -45,7 +45,7 @@ export default async function PlaygroundLayout({ const { templates, ...problemWithoutTemplates } = problemData; return ( -
+
- - - {problem} - - - - - - {workspace} - - - - {terminal} - - - - - - {ai} - - +
diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts deleted file mode 100644 index 4db317c..0000000 --- a/src/app/actions/auth.ts +++ /dev/null @@ -1,69 +0,0 @@ -"use server"; - -import bcrypt from "bcrypt"; -import prisma from "@/lib/prisma"; -import { signIn } from "@/lib/auth"; -import { authSchema } from "@/lib/zod"; -import { CredentialsSignInFormValues } from "@/components/credentials-sign-in-form"; -import { CredentialsSignUpFormValues } from "@/components/credentials-sign-up-form"; - -const saltRounds = 10; - -export async function signInWithCredentials(formData: CredentialsSignInFormValues) { - try { - // Parse credentials using authSchema for validation - const { email, password } = await authSchema.parseAsync(formData); - - // Find user by email - const user = await prisma.user.findUnique({ where: { email } }); - - // Check if the user exists - if (!user) { - throw new Error("User not found."); - } - - // Check if the user has a password - if (!user.password) { - throw new Error("Invalid credentials."); - } - - // Check if the password matches - const passwordMatch = await bcrypt.compare(password, user.password); - if (!passwordMatch) { - throw new Error("Incorrect password."); - } - - await signIn("credentials", { ...formData, redirect: false }); - return { success: true }; - } catch (error) { - return { error: error instanceof Error ? error.message : "Failed to sign in. Please try again." }; - } -} - -export async function signUpWithCredentials(formData: CredentialsSignUpFormValues) { - try { - const validatedData = await authSchema.parseAsync(formData); - - // Check if user already exists - const existingUser = await prisma.user.findUnique({ where: { email: validatedData.email } }); - if (existingUser) { - throw new Error("User already exists"); - } - - // Hash password and create user - const pwHash = await bcrypt.hash(validatedData.password, saltRounds); - const user = await prisma.user.create({ - data: { email: validatedData.email, password: pwHash }, - }); - - // Assign admin role if first user - const userCount = await prisma.user.count(); - if (userCount === 1) { - await prisma.user.update({ where: { id: user.id }, data: { role: "ADMIN" } }); - } - - return { success: true }; - } catch (error) { - return { error: error instanceof Error ? error.message : "Registration failed. Please try again." }; - } -} diff --git a/src/app/actions/judge.ts b/src/app/actions/judge.ts deleted file mode 100644 index 0960a92..0000000 --- a/src/app/actions/judge.ts +++ /dev/null @@ -1,346 +0,0 @@ -"use server"; - -import fs from "fs"; -import tar from "tar-stream"; -import Docker from "dockerode"; -import prisma from "@/lib/prisma"; -import { v4 as uuid } from "uuid"; -import { auth } from "@/lib/auth"; -import { redirect } from "next/navigation"; -import { Readable, Writable } from "stream"; -import { ExitCode, EditorLanguage, JudgeResult } from "@/generated/client"; - -const isRemote = process.env.DOCKER_HOST_MODE === "remote"; - -// Docker client initialization -const docker = isRemote - ? new Docker({ - protocol: process.env.DOCKER_REMOTE_PROTOCOL as "https" | "http" | "ssh" | undefined, - host: process.env.DOCKER_REMOTE_HOST, - port: process.env.DOCKER_REMOTE_PORT, - ca: fs.readFileSync(process.env.DOCKER_REMOTE_CA_PATH || "/certs/ca.pem"), - cert: fs.readFileSync(process.env.DOCKER_REMOTE_CERT_PATH || "/certs/cert.pem"), - key: fs.readFileSync(process.env.DOCKER_REMOTE_KEY_PATH || "/certs/key.pem"), - }) - : new Docker({ socketPath: "/var/run/docker.sock" }); - -// Prepare Docker image environment -async function prepareEnvironment(image: string, tag: string) { - const reference = `${image}:${tag}`; - const filters = { reference: [reference] }; - const images = await docker.listImages({ filters }); - if (images.length === 0) await docker.pull(reference); -} - -// Create Docker container with keep-alive -async function createContainer( - image: string, - tag: string, - workingDir: string, - memoryLimit?: number -) { - const container = await docker.createContainer({ - Image: `${image}:${tag}`, - Cmd: ["tail", "-f", "/dev/null"], // Keep container alive - WorkingDir: workingDir, - HostConfig: { - Memory: memoryLimit ? memoryLimit * 1024 * 1024 : undefined, - MemorySwap: memoryLimit ? memoryLimit * 1024 * 1024 : undefined, - }, - NetworkDisabled: true, - }); - - await container.start(); - return container; -} - -// Create tar stream for code submission -function createTarStream(file: string, value: string) { - const pack = tar.pack(); - pack.entry({ name: file }, value); - pack.finalize(); - return Readable.from(pack); -} - -export async function judge( - language: EditorLanguage, - value: string, -): Promise { - const session = await auth(); - if (!session) redirect("/sign-in"); - - let container: Docker.Container | null = null; - - try { - const config = await prisma.editorLanguageConfig.findUnique({ - where: { language }, - include: { - dockerConfig: true, - }, - }); - - if (!config || !config.dockerConfig) { - return { - id: uuid(), - output: "Configuration Error: Missing editor or docker configuration", - exitCode: ExitCode.SE, - executionTime: null, - memoryUsage: null, - }; - } - - const { - image, - tag, - workingDir, - memoryLimit, - timeLimit, - compileOutputLimit, - runOutputLimit, - } = config.dockerConfig; - const { fileName, fileExtension } = config; - const file = `${fileName}.${fileExtension}`; - - // Prepare the environment and create a container - await prepareEnvironment(image, tag); - container = await createContainer(image, tag, workingDir, memoryLimit); - - // Upload code to the container - const tarStream = createTarStream(file, value); - await container.putArchive(tarStream, { path: workingDir }); - - // Compile the code - const compileResult = await compile(container, file, fileName, compileOutputLimit); - if (compileResult.exitCode === ExitCode.CE) { - return compileResult; - } - - // Run the code - const runResult = await run(container, fileName, timeLimit, runOutputLimit); - return runResult; - } catch (error) { - console.error(error); - return { - id: uuid(), - output: "System Error", - exitCode: ExitCode.SE, - executionTime: null, - memoryUsage: null, - }; - } finally { - if (container) { - await container.kill(); - await container.remove(); - } - } -} - -async function compile( - container: Docker.Container, - file: string, - fileName: string, - maxOutput: number = 1 * 1024 * 1024 -): Promise { - const compileExec = await container.exec({ - Cmd: ["gcc", "-O2", file, "-o", fileName], - AttachStdout: true, - AttachStderr: true, - }); - - return new Promise((resolve, reject) => { - compileExec.start({}, (error, stream) => { - if (error || !stream) { - return reject({ output: "System Error", exitCode: ExitCode.SE }); - } - - const stdoutChunks: string[] = []; - let stdoutLength = 0; - const stdoutStream = new Writable({ - write(chunk, _encoding, callback) { - let text = chunk.toString(); - if (stdoutLength + text.length > maxOutput) { - text = text.substring(0, maxOutput - stdoutLength); - stdoutChunks.push(text); - stdoutLength = maxOutput; - callback(); - return; - } - stdoutChunks.push(text); - stdoutLength += text.length; - callback(); - }, - }); - - const stderrChunks: string[] = []; - let stderrLength = 0; - const stderrStream = new Writable({ - write(chunk, _encoding, callback) { - let text = chunk.toString(); - if (stderrLength + text.length > maxOutput) { - text = text.substring(0, maxOutput - stderrLength); - stderrChunks.push(text); - stderrLength = maxOutput; - callback(); - return; - } - stderrChunks.push(text); - stderrLength += text.length; - callback(); - }, - }); - - docker.modem.demuxStream(stream, stdoutStream, stderrStream); - - stream.on("end", async () => { - const stdout = stdoutChunks.join(""); - const stderr = stderrChunks.join(""); - const exitCode = (await compileExec.inspect()).ExitCode; - - let result: JudgeResult; - - if (exitCode !== 0 || stderr) { - result = { - id: uuid(), - output: stderr || "Compilation Error", - exitCode: ExitCode.CE, - executionTime: null, - memoryUsage: null, - }; - } else { - result = { - id: uuid(), - output: stdout, - exitCode: ExitCode.CS, - executionTime: null, - memoryUsage: null, - }; - } - - resolve(result); - }); - - stream.on("error", () => { - reject({ output: "System Error", exitCode: ExitCode.SE }); - }); - }); - }); -} - -// Run code and implement timeout -async function run( - container: Docker.Container, - fileName: string, - timeLimit: number = 1000, - maxOutput: number = 1 * 1024 * 1024, -): Promise { - const runExec = await container.exec({ - Cmd: [`./${fileName}`], - AttachStdout: true, - AttachStderr: true, - AttachStdin: true, - }); - - return new Promise((resolve, reject) => { - const stdoutChunks: string[] = []; - let stdoutLength = 0; - const stdoutStream = new Writable({ - write(chunk, _encoding, callback) { - let text = chunk.toString(); - if (stdoutLength + text.length > maxOutput) { - text = text.substring(0, maxOutput - stdoutLength); - stdoutChunks.push(text); - stdoutLength = maxOutput; - callback(); - return; - } - stdoutChunks.push(text); - stdoutLength += text.length; - callback(); - }, - }); - - const stderrChunks: string[] = []; - let stderrLength = 0; - const stderrStream = new Writable({ - write(chunk, _encoding, callback) { - let text = chunk.toString(); - if (stderrLength + text.length > maxOutput) { - text = text.substring(0, maxOutput - stderrLength); - stderrChunks.push(text); - stderrLength = maxOutput; - callback(); - return; - } - stderrChunks.push(text); - stderrLength += text.length; - callback(); - }, - }); - - // Start the exec stream - runExec.start({ hijack: true }, (error, stream) => { - if (error || !stream) { - return reject({ output: "System Error", exitCode: ExitCode.SE }); - } - - stream.write("[2,7,11,15]\n9\n[3,2,4]\n6\n[3,3]\n6"); - stream.end(); - - docker.modem.demuxStream(stream, stdoutStream, stderrStream); - - // Timeout mechanism - const timeoutId = setTimeout(async () => { - resolve({ - id: uuid(), - output: "Time Limit Exceeded", - exitCode: ExitCode.TLE, - executionTime: null, - memoryUsage: null, - }); - }, timeLimit); - - stream.on("end", async () => { - clearTimeout(timeoutId); // Clear the timeout if the program finishes before the time limit - const stdout = stdoutChunks.join(""); - const stderr = stderrChunks.join(""); - const exitCode = (await runExec.inspect()).ExitCode; - - let result: JudgeResult; - - // Exit code 0 means successful execution - if (exitCode === 0) { - result = { - id: uuid(), - output: stdout, - exitCode: ExitCode.AC, - executionTime: null, - memoryUsage: null, - }; - } else if (exitCode === 137) { - result = { - id: uuid(), - output: stderr || "Memory Limit Exceeded", - exitCode: ExitCode.MLE, - executionTime: null, - memoryUsage: null, - }; - } else { - result = { - id: uuid(), - output: stderr || "Runtime Error", - exitCode: ExitCode.RE, - executionTime: null, - memoryUsage: null, - }; - } - - resolve(result); - }); - - stream.on("error", () => { - clearTimeout(timeoutId); // Clear timeout in case of error - reject({ output: "System Error", exitCode: ExitCode.SE }); - }); - }); - }); -} diff --git a/src/app/actions/language-server.ts b/src/app/actions/language-server.ts deleted file mode 100644 index cd823e2..0000000 --- a/src/app/actions/language-server.ts +++ /dev/null @@ -1,29 +0,0 @@ -"use server"; - -import prisma from "@/lib/prisma"; -import { EditorLanguage } from "@/generated/client"; -import { SettingsLanguageServerFormValues } from "@/app/(app)/dashboard/@admin/settings/language-server/form"; - -export const getLanguageServerConfig = async (language: EditorLanguage) => { - return await prisma.languageServerConfig.findUnique({ - where: { language }, - }); -}; - -export const handleLanguageServerConfigSubmit = async ( - language: EditorLanguage, - data: SettingsLanguageServerFormValues -) => { - const existing = await getLanguageServerConfig(language); - - if (existing) { - await prisma.languageServerConfig.update({ - where: { language }, - data, - }); - } else { - await prisma.languageServerConfig.create({ - data: { ...data, language }, - }); - } -}; diff --git a/src/components/dockview.tsx b/src/components/dockview.tsx index 87f75df..dd1512f 100644 --- a/src/components/dockview.tsx +++ b/src/components/dockview.tsx @@ -43,7 +43,7 @@ const DefaultTab = ({ params }: IDockviewPanelHeaderProps<{ title: string }>) => const Icon = PanelIcons[title]; return ( -
+
{Icon && (
diff --git a/src/components/loading.tsx b/src/components/loading.tsx index 95fff1f..f26da5e 100644 --- a/src/components/loading.tsx +++ b/src/components/loading.tsx @@ -12,7 +12,7 @@ export function Loading({ ...props }: LoadingProps) { return ( -
+
); diff --git a/src/components/problem-editor.tsx b/src/components/problem-editor.tsx index 118f123..9d68736 100644 --- a/src/components/problem-editor.tsx +++ b/src/components/problem-editor.tsx @@ -137,7 +137,7 @@ export function ProblemEditor() { onValidate={handleEditorValidation} options={DefaultEditorOptionConfig} loading={} - className="h-full w-full py-2" + className="h-full w-full" /> ); } diff --git a/src/config/editor-option.ts b/src/config/editor-option.ts index cc64f87..c8f9e6a 100644 --- a/src/config/editor-option.ts +++ b/src/config/editor-option.ts @@ -19,6 +19,9 @@ export const DefaultEditorOptionConfig: editor.IEditorConstructionOptions = { hover: { above: false, }, + padding: { + top: 8, + }, scrollbar: { horizontalSliderSize: 10, verticalSliderSize: 10, diff --git a/src/style/mdx.css b/src/style/mdx.css deleted file mode 100644 index e811089..0000000 --- a/src/style/mdx.css +++ /dev/null @@ -1,49 +0,0 @@ -[data-rehype-pretty-code-figure] pre { - @apply px-0; -} - -[data-rehype-pretty-code-figure] code { - @apply text-sm !leading-loose md:text-base border-0 p-0; -} - -[data-rehype-pretty-code-figure] code[data-line-numbers] { - counter-reset: line; -} - -[data-rehype-pretty-code-figure] code[data-line-numbers] > [data-line]::before { - counter-increment: line; - content: counter(line); - @apply mr-4 inline-block w-4 text-right text-gray-500; -} - -[data-rehype-pretty-code-figure] [data-line] { - @apply border-l-2 border-l-transparent px-3; -} - -[data-rehype-pretty-code-figure] [data-highlighted-line] { - background: rgba(219, 234, 254, 0.5); - @apply border-l-blue-400; -} - -.dark [data-rehype-pretty-code-figure] [data-highlighted-line] { - background: rgba(200, 200, 255, 0.1); - @apply border-l-blue-400; -} - -[data-rehype-pretty-code-figure] [data-highlighted-chars] { - @apply rounded bg-zinc-400/50; - box-shadow: 0 0 0 4px rgb(161 161 170 / 0.5); -} - -.dark [data-rehype-pretty-code-figure] [data-highlighted-chars] { - @apply rounded bg-zinc-500/50; - box-shadow: 0 0 0 4px rgb(113 113 122 / 0.5); -} - -[data-rehype-pretty-code-figure] [data-chars-id] { - @apply border-b-2 p-1 shadow-none; -} - -.subheading-anchor { - @apply no-underline hover:underline; -}