This commit is contained in:
Dioxide 2025-06-16 16:04:49 +00:00 committed by GitHub
commit dde5f7c92d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
211 changed files with 7670 additions and 6515 deletions

View File

@ -2,7 +2,7 @@
# 升级到 Node.js v20 或更高版本,以解决 `ReferenceError: File is not defined` 问题
# 参考链接https://github.com/vercel/next.js/discussions/56032
FROM dockerp.com/node:22-alpine AS base
FROM docker.1ms.run/node:22-alpine AS base
# 仅在需要时安装依赖
FROM base AS deps

693
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
"Dark": "Dark"
}
},
"AvatarButton": {
"UserAvatar": {
"Settings": "Settings",
"LogIn": "LogIn",
"LogOut": "LogOut"
@ -49,7 +49,7 @@
"DetailsPage": {
"BackButton": "All Submissions",
"Time": "Submitted on",
"Input": "Input",
"Input": "Last Executed Input",
"ExpectedOutput": "Expected Output",
"ActualOutput": "Acutal Output",
"Code": "Code"
@ -109,7 +109,8 @@
"description": "Enter your email below to sign in to your account",
"or": "Or",
"noAccount": "Don't have an account?",
"signUp": "Sign up"
"signUp": "Sign up",
"oauth": "Sign in with {provider}"
},
"signInWithCredentials": {
"userNotFound": "User not found.",
@ -126,7 +127,8 @@
"description": "Enter your email below to sign up to your account",
"or": "Or",
"haveAccount": "Already have an account?",
"signIn": "Sign in"
"signIn": "Sign in",
"oauth": "Sign in with {provider}"
},
"StatusMessage": {
"PD": "Pending",
@ -149,12 +151,25 @@
"Time": "Time",
"Memory": "Memory"
},
"Testcase": {
"Table": {
"Case": "Case"
}
},
"WorkspaceEditorHeader": {
"LspStatusButton": {
"TooltipContent": "Language Server"
},
"AnalyzeButton": {
"TooltipContent": "Analyze",
"ComplexityAnalysis": "Complexity Analysis",
"TimeComplexity": "Time Complexity:",
"SpaceComplexity": "Space Complexity",
"Error": "Error occurred while analyzing complexity, please try again later.",
"Analyzing": "Analyzing..."
},
"ResetButton": {
"TooltipContent": "Reset Code"
"TooltipContent": "Reset"
},
"UndoButton": {
"TooltipContent": "Undo"
@ -214,5 +229,13 @@
"answer4": "Editor uses @shikijs/monaco themes, documentation rendered with github-markdown-css"
}
}
},
"LoginPromptCard": {
"title": "Join Judge4c to Code!",
"description": "View your Submission records here",
"loginButton": "Log In"
},
"Video": {
"unsupportedBrowser": "Your browser does not support HTML5 video."
}
}
}

View File

@ -7,7 +7,7 @@
"Dark": "深色"
}
},
"AvatarButton": {
"UserAvatar": {
"Settings": "设置",
"LogIn": "登录",
"LogOut": "登出"
@ -17,13 +17,13 @@
},
"BackButton": "返回",
"Bot": {
"title": "询问AI助手",
"description": "由Vercel Ai SDK驱动",
"placeholder": "AI助手将自动获取您当前的代码"
"title": "询问 AI 助手",
"description": "由 Vercel Ai SDK 驱动",
"placeholder": "AI 助手将自动获取您当前的代码"
},
"BotVisibilityToggle": {
"open": "打开AI助手",
"close": "关闭AI助手"
"open": "打开 AI 助手",
"close": "关闭 AI 助手"
},
"CredentialsSignInForm": {
"email": "邮箱",
@ -49,7 +49,7 @@
"DetailsPage": {
"BackButton": "所有提交记录",
"Time": "提交于",
"Input": "输入",
"Input": "最后执行的输入",
"ExpectedOutput": "期望输出",
"ActualOutput": "实际输出",
"Code": "代码"
@ -109,7 +109,8 @@
"description": "请输入你的邮箱以登录账户",
"or": "或者",
"noAccount": "还没有账户?",
"signUp": "注册"
"signUp": "注册",
"oauth": "使用 {provider} 登录"
},
"signInWithCredentials": {
"userNotFound": "未找到用户。",
@ -126,7 +127,8 @@
"description": "请输入你的邮箱以注册账户",
"or": "或者",
"haveAccount": "已经有账户了?",
"signIn": "登录"
"signIn": "登录",
"oauth": "使用 {provider} 登录"
},
"StatusMessage": {
"PD": "待处理",
@ -149,12 +151,25 @@
"Time": "执行用时",
"Memory": "消耗内存"
},
"Testcase": {
"Table": {
"Case": "样例"
}
},
"WorkspaceEditorHeader": {
"LspStatusButton": {
"TooltipContent": "语言服务"
},
"AnalyzeButton": {
"TooltipContent": "分析",
"ComplexityAnalysis": "复杂度分析",
"TimeComplexity": "时间复杂度:",
"SpaceComplexity": "空间复杂度:",
"Error": "解析复杂度时出错,请稍后重试。",
"Analyzing": "分析中..."
},
"ResetButton": {
"TooltipContent": "重置代码"
"TooltipContent": "重置"
},
"UndoButton": {
"TooltipContent": "撤销"
@ -214,5 +229,13 @@
"answer4": "编辑器采用 @shikijs/monaco, 文档采用 github-markdown-css 样式"
}
}
},
"LoginPromptCard": {
"title": "加入 Judge4c 开始编程!",
"description": "在此查看您的提交记录",
"loginButton": "登录"
},
"Video": {
"unsupportedBrowser": "您的浏览器不支持 HTML5 视频。"
}
}
}

View File

@ -3,7 +3,7 @@ import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = {
output: "standalone",
serverExternalPackages: ["dockerode"],
serverExternalPackages: ["dockerode", "pino", "pino-pretty"],
};
const withNextIntl = createNextIntlPlugin();

View File

@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev": "next dev",
"build": "next build",
"postinstall": "prisma generate",
"start": "next start",
@ -13,6 +13,7 @@
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@ai-sdk/deepseek": "^0.2.14",
"@ai-sdk/openai": "^1.3.0",
"@ai-sdk/react": "^1.2.0",
"@auth/prisma-adapter": "^2.8.0",
@ -41,13 +42,14 @@
"@tanstack/react-table": "^8.21.2",
"@types/vscode": "^1.97.0",
"ai": "^4.2.0",
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"devicons-react": "^1.4.0",
"dockerode": "^4.0.4",
"dockview": "^4.2.1",
"flexlayout-react": "^0.7.15",
"framer-motion": "^12.7.3",
"github-markdown-css": "^5.8.1",
"lucide-react": "^0.482.0",
@ -59,11 +61,12 @@
"next-mdx-remote": "^5.0.0",
"next-themes": "^0.4.6",
"normalize-url": "^8.0.1",
"pino": "^9.6.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-icons": "^5.5.0",
"react-resizable-panels": "^2.1.7",
"react-world-flags": "^1.6.0",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
@ -76,21 +79,19 @@
"vscode-languageclient": "^9.0.1",
"vscode-ws-jsonrpc": "^3.4.0",
"zod": "^3.24.2",
"zod-prisma-types": "^3.2.4",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@shikijs/monaco": "^3.2.1",
"@types/bcrypt": "^5.0.2",
"@types/dockerode": "^3.3.35",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-world-flags": "^1.6.0",
"@types/tar-stream": "^3.1.3",
"eslint": "^9",
"eslint-config-next": "15.1.7",
"pino-pretty": "^13.0.0",
"postcss": "^8",
"postcss-github-markdown-css": "^0.0.3",
"prisma": "^6.6.0",

View File

@ -1,53 +0,0 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'TEACHER', 'STUDENT', 'GUEST');
-- CreateEnum
CREATE TYPE "Difficulty" AS ENUM ('EASY', 'MEDIUM', 'HARD');
-- CreateEnum
CREATE TYPE "EditorLanguage" AS ENUM ('c', 'cpp');
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"role" "Role" NOT NULL DEFAULT 'GUEST',
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Problem" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"solution" TEXT NOT NULL,
"difficulty" "Difficulty" NOT NULL DEFAULT 'EASY',
"published" BOOLEAN NOT NULL DEFAULT false,
"authorId" INTEGER NOT NULL,
CONSTRAINT "Problem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Template" (
"id" SERIAL NOT NULL,
"language" "EditorLanguage" NOT NULL,
"template" TEXT NOT NULL,
"problemId" INTEGER NOT NULL,
CONSTRAINT "Template_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_name_key" ON "User"("name");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Problem" ADD CONSTRAINT "Problem_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Template" ADD CONSTRAINT "Template_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "Problem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,124 +0,0 @@
/*
Warnings:
- The primary key for the `Problem` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `authorId` on the `Problem` table. All the data in the column will be lost.
- The primary key for the `Template` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `Template` table. All the data in the column will be lost.
- The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint.
- Added the required column `userId` to the `Problem` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "Problem" DROP CONSTRAINT "Problem_authorId_fkey";
-- DropForeignKey
ALTER TABLE "Template" DROP CONSTRAINT "Template_problemId_fkey";
-- DropIndex
DROP INDEX "User_name_key";
-- AlterTable
ALTER TABLE "Problem" DROP CONSTRAINT "Problem_pkey",
DROP COLUMN "authorId",
ADD COLUMN "userId" TEXT NOT NULL,
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "Problem_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "Problem_id_seq";
-- AlterTable
ALTER TABLE "Template" DROP CONSTRAINT "Template_pkey",
DROP COLUMN "id",
ALTER COLUMN "problemId" SET DATA TYPE TEXT,
ADD CONSTRAINT "Template_pkey" PRIMARY KEY ("problemId", "language");
-- AlterTable
ALTER TABLE "User" DROP CONSTRAINT "User_pkey",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "emailVerified" TIMESTAMP(3),
ADD COLUMN "image" TEXT,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "User_id_seq";
-- CreateTable
CREATE TABLE "Account" (
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Account_pkey" PRIMARY KEY ("provider","providerAccountId")
);
-- CreateTable
CREATE TABLE "Session" (
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("identifier","token")
);
-- CreateTable
CREATE TABLE "Authenticator" (
"credentialID" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"credentialPublicKey" TEXT NOT NULL,
"counter" INTEGER NOT NULL,
"credentialDeviceType" TEXT NOT NULL,
"credentialBackedUp" BOOLEAN NOT NULL,
"transports" TEXT,
CONSTRAINT "Authenticator_pkey" PRIMARY KEY ("userId","credentialID")
);
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "Authenticator"("credentialID");
-- CreateIndex
CREATE INDEX "Problem_userId_idx" ON "Problem"("userId");
-- CreateIndex
CREATE INDEX "Problem_difficulty_idx" ON "Problem"("difficulty");
-- AddForeignKey
ALTER TABLE "Problem" ADD CONSTRAINT "Problem_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Template" ADD CONSTRAINT "Template_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "Problem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Authenticator" ADD CONSTRAINT "Authenticator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,4 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT,
ALTER COLUMN "name" DROP NOT NULL,
ALTER COLUMN "email" DROP NOT NULL;

View File

@ -1,10 +0,0 @@
-- CreateTable
CREATE TABLE "EditorLanguageConfig" (
"id" TEXT NOT NULL,
"language" "EditorLanguage" NOT NULL,
"label" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"fileExtension" TEXT NOT NULL,
CONSTRAINT "EditorLanguageConfig_pkey" PRIMARY KEY ("id")
);

View File

@ -1,14 +0,0 @@
/*
Warnings:
- The primary key for the `EditorLanguageConfig` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `EditorLanguageConfig` table. All the data in the column will be lost.
- A unique constraint covering the columns `[language]` on the table `EditorLanguageConfig` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "EditorLanguageConfig" DROP CONSTRAINT "EditorLanguageConfig_pkey",
DROP COLUMN "id";
-- CreateIndex
CREATE UNIQUE INDEX "EditorLanguageConfig_language_key" ON "EditorLanguageConfig"("language");

View File

@ -1,14 +0,0 @@
-- CreateTable
CREATE TABLE "LanguageServerConfig" (
"language" "EditorLanguage" NOT NULL,
"protocol" TEXT NOT NULL,
"hostname" TEXT NOT NULL,
"port" INTEGER,
"path" TEXT
);
-- CreateIndex
CREATE UNIQUE INDEX "LanguageServerConfig_language_key" ON "LanguageServerConfig"("language");
-- AddForeignKey
ALTER TABLE "LanguageServerConfig" ADD CONSTRAINT "LanguageServerConfig_language_fkey" FOREIGN KEY ("language") REFERENCES "EditorLanguageConfig"("language") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,17 +0,0 @@
-- CreateTable
CREATE TABLE "DockerConfig" (
"language" "EditorLanguage" NOT NULL,
"image" TEXT NOT NULL,
"tag" TEXT NOT NULL,
"workingDir" TEXT NOT NULL,
"timeLimit" INTEGER NOT NULL,
"memoryLimit" INTEGER NOT NULL,
"compileOutputLimit" INTEGER NOT NULL,
"runOutputLimit" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "DockerConfig_language_key" ON "DockerConfig"("language");
-- AddForeignKey
ALTER TABLE "DockerConfig" ADD CONSTRAINT "DockerConfig_language_fkey" FOREIGN KEY ("language") REFERENCES "EditorLanguageConfig"("language") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,2 +0,0 @@
-- CreateEnum
CREATE TYPE "ExitCode" AS ENUM ('SE', 'CS', 'CE', 'TLE', 'MLE', 'RE', 'AC', 'WA');

View File

@ -1,10 +0,0 @@
-- CreateTable
CREATE TABLE "JudgeResult" (
"id" TEXT NOT NULL,
"output" TEXT NOT NULL,
"exitCode" "ExitCode" NOT NULL,
"executionTime" INTEGER,
"memoryUsage" INTEGER,
CONSTRAINT "JudgeResult_pkey" PRIMARY KEY ("id")
);

View File

@ -1,12 +0,0 @@
/*
Warnings:
- Changed the type of `protocol` on the `LanguageServerConfig` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- CreateEnum
CREATE TYPE "LanguageServerProtocol" AS ENUM ('ws', 'wss');
-- AlterTable
ALTER TABLE "LanguageServerConfig" DROP COLUMN "protocol",
ADD COLUMN "protocol" "LanguageServerProtocol" NOT NULL;

View File

@ -1,16 +0,0 @@
/*
Warnings:
- The values [TEACHER,STUDENT] on the enum `Role` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "Role_new" AS ENUM ('ADMIN', 'GUEST');
ALTER TABLE "User" ALTER COLUMN "role" DROP DEFAULT;
ALTER TABLE "User" ALTER COLUMN "role" TYPE "Role_new" USING ("role"::text::"Role_new");
ALTER TYPE "Role" RENAME TO "Role_old";
ALTER TYPE "Role_new" RENAME TO "Role";
DROP TYPE "Role_old";
ALTER TABLE "User" ALTER COLUMN "role" SET DEFAULT 'GUEST';
COMMIT;

View File

@ -1,12 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[displayId]` on the table `Problem` will be added. If there are existing duplicate values, this will fail.
- Added the required column `displayId` to the `Problem` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Problem" ADD COLUMN "displayId" INTEGER NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Problem_displayId_key" ON "Problem"("displayId");

View File

@ -1,23 +0,0 @@
-- CreateTable
CREATE TABLE "Testcase" (
"id" TEXT NOT NULL,
"problemId" TEXT NOT NULL,
CONSTRAINT "Testcase_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TestcaseData" (
"id" TEXT NOT NULL,
"label" TEXT NOT NULL,
"value" TEXT NOT NULL,
"testcaseId" TEXT NOT NULL,
CONSTRAINT "TestcaseData_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Testcase" ADD CONSTRAINT "Testcase_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "Problem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TestcaseData" ADD CONSTRAINT "TestcaseData_testcaseId_fkey" FOREIGN KEY ("testcaseId") REFERENCES "Testcase"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `expectedOutput` to the `Testcase` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Testcase" ADD COLUMN "expectedOutput" TEXT NOT NULL;

View File

@ -1,14 +0,0 @@
/*
Warnings:
- You are about to drop the column `memoryLimit` on the `DockerConfig` table. All the data in the column will be lost.
- You are about to drop the column `timeLimit` on the `DockerConfig` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "DockerConfig" DROP COLUMN "memoryLimit",
DROP COLUMN "timeLimit";
-- AlterTable
ALTER TABLE "Problem" ADD COLUMN "memoryLimit" INTEGER NOT NULL DEFAULT 128,
ADD COLUMN "timeLimit" INTEGER NOT NULL DEFAULT 1000;

View File

@ -1,37 +0,0 @@
/*
Warnings:
- You are about to drop the `JudgeResult` table. If the table is not empty, all the data it contains will be lost.
*/
-- CreateEnum
CREATE TYPE "Status" AS ENUM ('PD', 'QD', 'CP', 'CE', 'CS', 'RU', 'TLE', 'MLE', 'RE', 'AC', 'WA', 'SE');
-- DropTable
DROP TABLE "JudgeResult";
-- DropEnum
DROP TYPE "ExitCode";
-- CreateTable
CREATE TABLE "Submission" (
"id" TEXT NOT NULL,
"language" "EditorLanguage" NOT NULL,
"code" TEXT NOT NULL,
"status" "Status" NOT NULL,
"message" TEXT,
"executionTime" INTEGER,
"memoryUsage" INTEGER,
"userId" TEXT NOT NULL,
"problemId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Submission_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Submission" ADD CONSTRAINT "Submission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Submission" ADD CONSTRAINT "Submission_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "Problem"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,29 +0,0 @@
/*
Warnings:
- Added the required column `index` to the `TestcaseData` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "TestcaseData" ADD COLUMN "index" INTEGER NOT NULL;
-- CreateTable
CREATE TABLE "TestcaseResult" (
"id" TEXT NOT NULL,
"isCorrect" BOOLEAN NOT NULL,
"output" TEXT NOT NULL,
"executionTime" INTEGER,
"memoryUsage" INTEGER,
"submissionId" TEXT NOT NULL,
"testcaseId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TestcaseResult_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "TestcaseResult" ADD CONSTRAINT "TestcaseResult_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "Submission"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TestcaseResult" ADD CONSTRAINT "TestcaseResult_testcaseId_fkey" FOREIGN KEY ("testcaseId") REFERENCES "Testcase"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,244 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'GUEST');
-- CreateEnum
CREATE TYPE "Difficulty" AS ENUM ('EASY', 'MEDIUM', 'HARD');
-- CreateEnum
CREATE TYPE "Locale" AS ENUM ('en', 'zh');
-- CreateEnum
CREATE TYPE "Language" AS ENUM ('c', 'cpp');
-- CreateEnum
CREATE TYPE "Protocol" AS ENUM ('ws', 'wss');
-- CreateEnum
CREATE TYPE "Status" AS ENUM ('PD', 'QD', 'CP', 'CE', 'CS', 'RU', 'TLE', 'MLE', 'RE', 'AC', 'WA', 'SE');
-- CreateEnum
CREATE TYPE "ProblemContentType" AS ENUM ('TITLE', 'DESCRIPTION', 'SOLUTION');
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT NOT NULL,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
"role" "Role" NOT NULL DEFAULT 'GUEST',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Problem" (
"id" TEXT NOT NULL,
"displayId" INTEGER NOT NULL,
"difficulty" "Difficulty" NOT NULL DEFAULT 'EASY',
"isPublished" BOOLEAN NOT NULL DEFAULT false,
"timeLimit" INTEGER NOT NULL DEFAULT 1000,
"memoryLimit" INTEGER NOT NULL DEFAULT 134217728,
"userId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Problem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProblemLocalization" (
"problemId" TEXT NOT NULL,
"locale" "Locale" NOT NULL,
"type" "ProblemContentType" NOT NULL,
"content" TEXT NOT NULL,
CONSTRAINT "ProblemLocalization_pkey" PRIMARY KEY ("problemId","locale","type")
);
-- CreateTable
CREATE TABLE "Template" (
"problemId" TEXT NOT NULL,
"language" "Language" NOT NULL,
"content" TEXT NOT NULL,
CONSTRAINT "Template_pkey" PRIMARY KEY ("problemId","language")
);
-- CreateTable
CREATE TABLE "Submission" (
"id" TEXT NOT NULL,
"language" "Language" NOT NULL,
"content" TEXT NOT NULL,
"status" "Status" NOT NULL,
"message" TEXT,
"timeUsage" INTEGER,
"memoryUsage" INTEGER,
"userId" TEXT NOT NULL,
"problemId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Submission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Testcase" (
"id" TEXT NOT NULL,
"expectedOutput" TEXT NOT NULL,
"problemId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Testcase_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TestcaseInput" (
"id" TEXT NOT NULL,
"index" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"testcaseId" TEXT NOT NULL,
CONSTRAINT "TestcaseInput_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TestcaseResult" (
"id" TEXT NOT NULL,
"isCorrect" BOOLEAN NOT NULL,
"output" TEXT NOT NULL,
"timeUsage" INTEGER,
"memoryUsage" INTEGER,
"submissionId" TEXT NOT NULL,
"testcaseId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TestcaseResult_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DockerConfig" (
"language" "Language" NOT NULL,
"image" TEXT NOT NULL,
"tag" TEXT NOT NULL,
"workingDir" TEXT NOT NULL,
"compileOutputLimit" INTEGER NOT NULL DEFAULT 1048576,
"runOutputLimit" INTEGER NOT NULL DEFAULT 1048576,
CONSTRAINT "DockerConfig_pkey" PRIMARY KEY ("language")
);
-- CreateTable
CREATE TABLE "LanguageServerConfig" (
"language" "Language" NOT NULL,
"protocol" "Protocol" NOT NULL,
"hostname" TEXT NOT NULL,
"port" INTEGER,
"path" TEXT,
CONSTRAINT "LanguageServerConfig_pkey" PRIMARY KEY ("language")
);
-- CreateTable
CREATE TABLE "Account" (
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Account_pkey" PRIMARY KEY ("provider","providerAccountId")
);
-- CreateTable
CREATE TABLE "Session" (
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("identifier","token")
);
-- CreateTable
CREATE TABLE "Authenticator" (
"credentialID" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"credentialPublicKey" TEXT NOT NULL,
"counter" INTEGER NOT NULL,
"credentialDeviceType" TEXT NOT NULL,
"credentialBackedUp" BOOLEAN NOT NULL,
"transports" TEXT,
CONSTRAINT "Authenticator_pkey" PRIMARY KEY ("userId","credentialID")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Problem_displayId_key" ON "Problem"("displayId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "Authenticator"("credentialID");
-- AddForeignKey
ALTER TABLE "Problem" ADD CONSTRAINT "Problem_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProblemLocalization" ADD CONSTRAINT "ProblemLocalization_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "Problem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Template" ADD CONSTRAINT "Template_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "Problem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Submission" ADD CONSTRAINT "Submission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Submission" ADD CONSTRAINT "Submission_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "Problem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Testcase" ADD CONSTRAINT "Testcase_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "Problem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TestcaseInput" ADD CONSTRAINT "TestcaseInput_testcaseId_fkey" FOREIGN KEY ("testcaseId") REFERENCES "Testcase"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TestcaseResult" ADD CONSTRAINT "TestcaseResult_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "Submission"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TestcaseResult" ADD CONSTRAINT "TestcaseResult_testcaseId_fkey" FOREIGN KEY ("testcaseId") REFERENCES "Testcase"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Authenticator" ADD CONSTRAINT "Authenticator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "TestcaseResult" ALTER COLUMN "output" DROP NOT NULL;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Problem" ADD COLUMN "trim" BOOLEAN NOT NULL DEFAULT false;

View File

@ -0,0 +1,164 @@
/*
Warnings:
- The primary key for the `DockerConfig` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The primary key for the `LanguageServerConfig` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `createdAt` on the `Problem` table. All the data in the column will be lost.
- You are about to drop the column `isPublished` on the `Problem` table. All the data in the column will be lost.
- You are about to drop the column `updatedAt` on the `Problem` table. All the data in the column will be lost.
- You are about to drop the column `content` on the `Submission` table. All the data in the column will be lost.
- You are about to drop the column `timeUsage` on the `Submission` table. All the data in the column will be lost.
- The primary key for the `Template` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `content` on the `Template` table. All the data in the column will be lost.
- You are about to drop the column `createdAt` on the `Testcase` table. All the data in the column will be lost.
- You are about to drop the column `updatedAt` on the `Testcase` table. All the data in the column will be lost.
- You are about to drop the column `timeUsage` on the `TestcaseResult` table. All the data in the column will be lost.
- You are about to drop the `ProblemLocalization` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `TestcaseInput` table. If the table is not empty, all the data it contains will be lost.
- A unique constraint covering the columns `[language]` on the table `DockerConfig` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[language]` on the table `LanguageServerConfig` will be added. If there are existing duplicate values, this will fail.
- Changed the type of `language` on the `DockerConfig` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Changed the type of `language` on the `LanguageServerConfig` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Changed the type of `protocol` on the `LanguageServerConfig` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Added the required column `description` to the `Problem` table without a default value. This is not possible if the table is not empty.
- Added the required column `solution` to the `Problem` table without a default value. This is not possible if the table is not empty.
- Added the required column `title` to the `Problem` table without a default value. This is not possible if the table is not empty.
- Made the column `userId` on table `Problem` required. This step will fail if there are existing NULL values in that column.
- Added the required column `code` to the `Submission` table without a default value. This is not possible if the table is not empty.
- Changed the type of `language` on the `Submission` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Added the required column `template` to the `Template` table without a default value. This is not possible if the table is not empty.
- Changed the type of `language` on the `Template` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Made the column `output` on table `TestcaseResult` required. This step will fail if there are existing NULL values in that column.
*/
-- CreateEnum
CREATE TYPE "EditorLanguage" AS ENUM ('c', 'cpp');
-- CreateEnum
CREATE TYPE "LanguageServerProtocol" AS ENUM ('ws', 'wss');
-- DropForeignKey
ALTER TABLE "Problem" DROP CONSTRAINT "Problem_userId_fkey";
-- DropForeignKey
ALTER TABLE "ProblemLocalization" DROP CONSTRAINT "ProblemLocalization_problemId_fkey";
-- DropForeignKey
ALTER TABLE "TestcaseInput" DROP CONSTRAINT "TestcaseInput_testcaseId_fkey";
-- AlterTable
ALTER TABLE "DockerConfig" DROP CONSTRAINT "DockerConfig_pkey",
DROP COLUMN "language",
ADD COLUMN "language" "EditorLanguage" NOT NULL,
ALTER COLUMN "compileOutputLimit" DROP DEFAULT,
ALTER COLUMN "runOutputLimit" DROP DEFAULT;
-- AlterTable
ALTER TABLE "LanguageServerConfig" DROP CONSTRAINT "LanguageServerConfig_pkey",
DROP COLUMN "language",
ADD COLUMN "language" "EditorLanguage" NOT NULL,
DROP COLUMN "protocol",
ADD COLUMN "protocol" "LanguageServerProtocol" NOT NULL;
-- AlterTable
ALTER TABLE "Problem" DROP COLUMN "createdAt",
DROP COLUMN "isPublished",
DROP COLUMN "updatedAt",
ADD COLUMN "description" TEXT NOT NULL,
ADD COLUMN "published" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "solution" TEXT NOT NULL,
ADD COLUMN "title" TEXT NOT NULL,
ALTER COLUMN "memoryLimit" SET DEFAULT 128,
ALTER COLUMN "userId" SET NOT NULL;
-- AlterTable
ALTER TABLE "Submission" DROP COLUMN "content",
DROP COLUMN "timeUsage",
ADD COLUMN "code" TEXT NOT NULL,
ADD COLUMN "executionTime" INTEGER,
DROP COLUMN "language",
ADD COLUMN "language" "EditorLanguage" NOT NULL;
-- AlterTable
ALTER TABLE "Template" DROP CONSTRAINT "Template_pkey",
DROP COLUMN "content",
ADD COLUMN "template" TEXT NOT NULL,
DROP COLUMN "language",
ADD COLUMN "language" "EditorLanguage" NOT NULL,
ADD CONSTRAINT "Template_pkey" PRIMARY KEY ("problemId", "language");
-- AlterTable
ALTER TABLE "Testcase" DROP COLUMN "createdAt",
DROP COLUMN "updatedAt";
-- AlterTable
ALTER TABLE "TestcaseResult" DROP COLUMN "timeUsage",
ADD COLUMN "executionTime" INTEGER,
ALTER COLUMN "output" SET NOT NULL;
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "email" DROP NOT NULL;
-- DropTable
DROP TABLE "ProblemLocalization";
-- DropTable
DROP TABLE "TestcaseInput";
-- DropEnum
DROP TYPE "Language";
-- DropEnum
DROP TYPE "Locale";
-- DropEnum
DROP TYPE "ProblemContentType";
-- DropEnum
DROP TYPE "Protocol";
-- CreateTable
CREATE TABLE "EditorLanguageConfig" (
"language" "EditorLanguage" NOT NULL,
"label" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"fileExtension" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "TestcaseData" (
"id" TEXT NOT NULL,
"index" INTEGER NOT NULL,
"label" TEXT NOT NULL,
"value" TEXT NOT NULL,
"testcaseId" TEXT NOT NULL,
CONSTRAINT "TestcaseData_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "EditorLanguageConfig_language_key" ON "EditorLanguageConfig"("language");
-- CreateIndex
CREATE UNIQUE INDEX "DockerConfig_language_key" ON "DockerConfig"("language");
-- CreateIndex
CREATE UNIQUE INDEX "LanguageServerConfig_language_key" ON "LanguageServerConfig"("language");
-- CreateIndex
CREATE INDEX "Problem_userId_idx" ON "Problem"("userId");
-- CreateIndex
CREATE INDEX "Problem_difficulty_idx" ON "Problem"("difficulty");
-- AddForeignKey
ALTER TABLE "Problem" ADD CONSTRAINT "Problem_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LanguageServerConfig" ADD CONSTRAINT "LanguageServerConfig_language_fkey" FOREIGN KEY ("language") REFERENCES "EditorLanguageConfig"("language") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DockerConfig" ADD CONSTRAINT "DockerConfig_language_fkey" FOREIGN KEY ("language") REFERENCES "EditorLanguageConfig"("language") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TestcaseData" ADD CONSTRAINT "TestcaseData_testcaseId_fkey" FOREIGN KEY ("testcaseId") REFERENCES "Testcase"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,156 @@
/*
Warnings:
- You are about to drop the column `description` on the `Problem` table. All the data in the column will be lost.
- You are about to drop the column `published` on the `Problem` table. All the data in the column will be lost.
- You are about to drop the column `solution` on the `Problem` table. All the data in the column will be lost.
- You are about to drop the column `title` on the `Problem` table. All the data in the column will be lost.
- You are about to drop the column `code` on the `Submission` table. All the data in the column will be lost.
- You are about to drop the column `executionTime` on the `Submission` table. All the data in the column will be lost.
- The primary key for the `Template` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `template` on the `Template` table. All the data in the column will be lost.
- You are about to drop the column `executionTime` on the `TestcaseResult` table. All the data in the column will be lost.
- You are about to drop the `EditorLanguageConfig` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `TestcaseData` table. If the table is not empty, all the data it contains will be lost.
- Changed the type of `language` on the `DockerConfig` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Changed the type of `language` on the `LanguageServerConfig` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Changed the type of `protocol` on the `LanguageServerConfig` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Added the required column `updatedAt` to the `Problem` table without a default value. This is not possible if the table is not empty.
- Added the required column `content` to the `Submission` table without a default value. This is not possible if the table is not empty.
- Changed the type of `language` on the `Submission` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Added the required column `content` to the `Template` table without a default value. This is not possible if the table is not empty.
- Changed the type of `language` on the `Template` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Added the required column `updatedAt` to the `Testcase` table without a default value. This is not possible if the table is not empty.
- Made the column `email` on table `User` required. This step will fail if there are existing NULL values in that column.
*/
-- CreateEnum
CREATE TYPE "Locale" AS ENUM ('en', 'zh');
-- CreateEnum
CREATE TYPE "Language" AS ENUM ('c', 'cpp');
-- CreateEnum
CREATE TYPE "Protocol" AS ENUM ('ws', 'wss');
-- CreateEnum
CREATE TYPE "ProblemContentType" AS ENUM ('TITLE', 'DESCRIPTION', 'SOLUTION');
-- DropForeignKey
ALTER TABLE "DockerConfig" DROP CONSTRAINT "DockerConfig_language_fkey";
-- DropForeignKey
ALTER TABLE "LanguageServerConfig" DROP CONSTRAINT "LanguageServerConfig_language_fkey";
-- DropForeignKey
ALTER TABLE "Problem" DROP CONSTRAINT "Problem_userId_fkey";
-- DropForeignKey
ALTER TABLE "TestcaseData" DROP CONSTRAINT "TestcaseData_testcaseId_fkey";
-- DropIndex
DROP INDEX "DockerConfig_language_key";
-- DropIndex
DROP INDEX "LanguageServerConfig_language_key";
-- DropIndex
DROP INDEX "Problem_difficulty_idx";
-- DropIndex
DROP INDEX "Problem_userId_idx";
-- AlterTable
ALTER TABLE "DockerConfig" ALTER COLUMN "compileOutputLimit" SET DEFAULT 1048576,
ALTER COLUMN "runOutputLimit" SET DEFAULT 1048576,
DROP COLUMN "language",
ADD COLUMN "language" "Language" NOT NULL,
ADD CONSTRAINT "DockerConfig_pkey" PRIMARY KEY ("language");
-- AlterTable
ALTER TABLE "LanguageServerConfig" DROP COLUMN "language",
ADD COLUMN "language" "Language" NOT NULL,
DROP COLUMN "protocol",
ADD COLUMN "protocol" "Protocol" NOT NULL,
ADD CONSTRAINT "LanguageServerConfig_pkey" PRIMARY KEY ("language");
-- AlterTable
ALTER TABLE "Problem" DROP COLUMN "description",
DROP COLUMN "published",
DROP COLUMN "solution",
DROP COLUMN "title",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "isPublished" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
ALTER COLUMN "memoryLimit" SET DEFAULT 134217728,
ALTER COLUMN "userId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "Submission" DROP COLUMN "code",
DROP COLUMN "executionTime",
ADD COLUMN "content" TEXT NOT NULL,
ADD COLUMN "timeUsage" INTEGER,
DROP COLUMN "language",
ADD COLUMN "language" "Language" NOT NULL;
-- AlterTable
ALTER TABLE "Template" DROP CONSTRAINT "Template_pkey",
DROP COLUMN "template",
ADD COLUMN "content" TEXT NOT NULL,
DROP COLUMN "language",
ADD COLUMN "language" "Language" NOT NULL,
ADD CONSTRAINT "Template_pkey" PRIMARY KEY ("problemId", "language");
-- AlterTable
ALTER TABLE "Testcase" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
-- AlterTable
ALTER TABLE "TestcaseResult" DROP COLUMN "executionTime",
ADD COLUMN "timeUsage" INTEGER,
ALTER COLUMN "output" DROP NOT NULL;
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "email" SET NOT NULL;
-- DropTable
DROP TABLE "EditorLanguageConfig";
-- DropTable
DROP TABLE "TestcaseData";
-- DropEnum
DROP TYPE "EditorLanguage";
-- DropEnum
DROP TYPE "LanguageServerProtocol";
-- CreateTable
CREATE TABLE "ProblemLocalization" (
"problemId" TEXT NOT NULL,
"locale" "Locale" NOT NULL,
"type" "ProblemContentType" NOT NULL,
"content" TEXT NOT NULL,
CONSTRAINT "ProblemLocalization_pkey" PRIMARY KEY ("problemId","locale","type")
);
-- CreateTable
CREATE TABLE "TestcaseInput" (
"id" TEXT NOT NULL,
"index" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"testcaseId" TEXT NOT NULL,
CONSTRAINT "TestcaseInput_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Problem" ADD CONSTRAINT "Problem_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProblemLocalization" ADD CONSTRAINT "ProblemLocalization_problemId_fkey" FOREIGN KEY ("problemId") REFERENCES "Problem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TestcaseInput" ADD CONSTRAINT "TestcaseInput_testcaseId_fkey" FOREIGN KEY ("testcaseId") REFERENCES "Testcase"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,13 @@
/*
Warnings:
- Added the required column `type` to the `Problem` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "Type" AS ENUM ('NS', 'INT', 'FLOAT', 'CHAR', 'INTARRAY', 'FLOATARRAY', 'STRING', 'MATRIX');
-- AlterTable
ALTER TABLE "Problem" ADD COLUMN "isRandom" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "lengthOfArray" INTEGER[],
ADD COLUMN "type" "Type" NOT NULL;

View File

@ -0,0 +1,16 @@
/*
Warnings:
- You are about to drop the column `type` on the `Problem` table. All the data in the column will be lost.
- Added the required column `answerType` to the `Problem` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "AnswerType" AS ENUM ('NS', 'INT', 'FLOAT', 'CHAR', 'INTARRAY', 'FLOATARRAY', 'STRING', 'MATRIX');
-- AlterTable
ALTER TABLE "Problem" DROP COLUMN "type",
ADD COLUMN "answerType" "AnswerType" NOT NULL;
-- DropEnum
DROP TYPE "Type";

View File

@ -1,114 +1,48 @@
generator client {
provider = "prisma-client-js"
output = "../src/generated/client"
}
generator zod {
provider = "zod-prisma-types"
output = "../src/generated/zod"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
output = "../src/generated/client"
}
enum Role {
ADMIN
GUEST
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
password String?
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
// Optional for WebAuthn support
Authenticator Authenticator[]
role Role @default(GUEST)
problems Problem[]
submissions Submission[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum Difficulty {
EASY
MEDIUM
HARD
}
model Problem {
id String @id @default(cuid())
displayId Int @unique
title String
description String
solution String
difficulty Difficulty @default(EASY)
published Boolean @default(false)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
timeLimit Int @default(1000)
memoryLimit Int @default(128)
templates Template[]
testcases Testcase[]
submissions Submission[]
@@index([userId])
@@index([difficulty])
enum Locale {
en
zh
}
enum EditorLanguage {
enum Language {
c
cpp
}
model EditorLanguageConfig {
language EditorLanguage @unique
label String
fileName String
fileExtension String
languageServerConfig LanguageServerConfig? @relation
dockerConfig DockerConfig? @relation
}
enum LanguageServerProtocol {
enum Protocol {
ws
wss
}
model LanguageServerConfig {
language EditorLanguage @unique
protocol LanguageServerProtocol
hostname String
port Int?
path String?
editorLanguageConfig EditorLanguageConfig @relation(fields: [language], references: [language])
}
model DockerConfig {
language EditorLanguage @unique
image String
tag String
workingDir String
compileOutputLimit Int
runOutputLimit Int
editorLanguageConfig EditorLanguageConfig @relation(fields: [language], references: [language])
}
model Template {
language EditorLanguage
template String
problemId String
problem Problem @relation(fields: [problemId], references: [id], onDelete: Cascade)
@@id([problemId, language])
enum AnswerType {
NS // Not Suitable
INT // Single Integer
FLOAT // Single Float
CHAR // Single Character
INTARRAY // Integer Array
FLOATARRAY // Float Array
STRING // Single String
STRINGARRAY // String Array
}
enum Status {
@ -126,14 +60,90 @@ enum Status {
SE // System Error
}
enum ProblemContentType {
TITLE
DESCRIPTION
SOLUTION
}
model User {
id String @id @default(cuid())
name String?
email String @unique
password String?
emailVerified DateTime?
image String?
role Role @default(GUEST)
accounts Account[]
sessions Session[]
// Optional for WebAuthn support
Authenticator Authenticator[]
problems Problem[]
submissions Submission[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Problem {
id String @id @default(cuid())
displayId Int @unique
difficulty Difficulty @default(EASY)
isPublished Boolean @default(false)
trim Boolean @default(false)
timeLimit Int @default(1000)
memoryLimit Int @default(134217728)
isRandom Boolean @default(false)
answerType AnswerType[]
lengthOfArray Int[]
localizations ProblemLocalization[]
templates Template[]
testcases Testcase[]
submissions Submission[]
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ProblemLocalization {
problemId String
locale Locale
type ProblemContentType
content String
problem Problem @relation(fields: [problemId], references: [id], onDelete: Cascade)
@@id([problemId, locale, type])
}
model Template {
problemId String
language Language
content String
problem Problem @relation(fields: [problemId], references: [id], onDelete: Cascade)
@@id([problemId, language])
}
model Submission {
id String @id @default(cuid())
language EditorLanguage
code String
status Status
message String?
executionTime Int?
memoryUsage Int?
id String @id @default(cuid())
language Language
content String
status Status
message String?
timeUsage Int?
memoryUsage Int?
testcaseResults TestcaseResult[]
userId String
problemId String
@ -141,47 +151,70 @@ model Submission {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
problem Problem @relation(fields: [problemId], references: [id], onDelete: Cascade)
testcaseResults TestcaseResult[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Testcase {
id String @id @default(cuid())
problemId String
problem Problem @relation(fields: [problemId], references: [id], onDelete: Cascade)
data TestcaseData[]
id String @id @default(cuid())
expectedOutput String
inputs TestcaseInput[]
testcaseResults TestcaseResult[]
}
model TestcaseData {
id String @id @default(cuid())
index Int
label String
value String
testcaseId String
testcase Testcase @relation(fields: [testcaseId], references: [id], onDelete: Cascade)
}
problemId String
model TestcaseResult {
id String @id @default(cuid())
isCorrect Boolean
output String
executionTime Int?
memoryUsage Int?
submissionId String
testcaseId String
submission Submission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
testcase Testcase @relation(fields: [testcaseId], references: [id], onDelete: Cascade)
problem Problem @relation(fields: [problemId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model TestcaseInput {
id String @id @default(cuid())
index Int
name String
value String
testcaseId String
testcase Testcase @relation(fields: [testcaseId], references: [id], onDelete: Cascade)
}
model TestcaseResult {
id String @id @default(cuid())
isCorrect Boolean
output String?
timeUsage Int?
memoryUsage Int?
submissionId String
testcaseId String
submission Submission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
testcase Testcase @relation(fields: [testcaseId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DockerConfig {
language Language @id
image String
tag String
workingDir String
compileOutputLimit Int @default(1048576)
runOutputLimit Int @default(1048576)
}
model LanguageServerConfig {
language Language @id
protocol Protocol
hostname String
port Int?
path String?
}
model Account {
userId String
type String
@ -195,11 +228,11 @@ model Account {
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
}
@ -207,7 +240,8 @@ model Session {
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

File diff suppressed because it is too large Load Diff

1
public/flags/cn.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30 20"><defs><path id="a" d="M0-1L.588.809-.952-.309H.952L-.588.809z" fill="#FF0"/></defs><path fill="#EE1C25" d="M0 0h30v20H0z"/><use xlink:href="#a" transform="matrix(3 0 0 3 5 5)"/><use xlink:href="#a" transform="rotate(23.036 .093 25.536)"/><use xlink:href="#a" transform="rotate(45.87 1.273 16.18)"/><use xlink:href="#a" transform="rotate(69.945 .996 12.078)"/><use xlink:href="#a" transform="rotate(20.66 -19.689 31.932)"/></svg>

After

Width:  |  Height:  |  Size: 531 B

1
public/flags/us.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 7410 3900"><path fill="#b22234" d="M0 0h7410v3900H0z"/><path d="M0 450h7410m0 600H0m0 600h7410m0 600H0m0 600h7410m0 600H0" stroke="#fff" stroke-width="300"/><path fill="#3c3b6e" d="M0 0h2964v2100H0z"/><g fill="#fff"><g id="d"><g id="c"><g id="e"><g id="b"><path id="a" d="M247 90l70.534 217.082-184.66-134.164h228.253L176.466 307.082z"/><use xlink:href="#a" y="420"/><use xlink:href="#a" y="840"/><use xlink:href="#a" y="1260"/></g><use xlink:href="#a" y="1680"/></g><use xlink:href="#b" x="247" y="210"/></g><use xlink:href="#c" x="494"/></g><use xlink:href="#d" x="988"/><use xlink:href="#c" x="1976"/><use xlink:href="#e" x="2470"/></g></svg>

After

Width:  |  Height:  |  Size: 741 B

BIN
public/sign-in.mp4 Normal file

Binary file not shown.

View File

@ -1,544 +0,0 @@
"use server";
import fs from "fs";
import tar from "tar-stream";
import Docker from "dockerode";
import prisma from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { Readable, Writable } from "stream";
import { Status } from "@/generated/client";
import { revalidatePath } from "next/cache";
import type { ProblemWithTestcases, TestcaseWithDetails } from "@/types/prisma";
import type { EditorLanguage, Submission, TestcaseResult } 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): Promise<boolean> {
try {
const reference = `${image}:${tag}`;
const filters = { reference: [reference] };
const images = await docker.listImages({ filters });
return images.length !== 0;
} catch (error) {
console.error("Error checking Docker images:", error);
return false;
}
}
// 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,
code: string,
problemId: string,
): Promise<Submission> {
const session = await auth();
if (!session?.user?.id) redirect("/sign-in");
const userId = session.user.id;
let container: Docker.Container | null = null;
let submission: Submission | null = null;
try {
const problem = await prisma.problem.findUnique({
where: { id: problemId },
include: {
testcases: {
include: {
data: true,
},
},
},
}) as ProblemWithTestcases | null;
if (!problem) {
submission = await prisma.submission.create({
data: {
language,
code,
status: Status.SE,
userId,
problemId,
message: "Problem not found",
},
});
return submission;
}
const config = await prisma.editorLanguageConfig.findUnique({
where: { language },
include: {
dockerConfig: true,
},
});
if (!config?.dockerConfig) {
submission = await prisma.submission.create({
data: {
language,
code,
status: Status.SE,
userId,
problemId,
message: " Missing editor or docker configuration",
},
});
return submission;
}
const testcases = problem.testcases;
if (!testcases || testcases.length === 0) {
submission = await prisma.submission.create({
data: {
language,
code,
status: Status.SE,
userId,
problemId,
message: "Testcases not found",
},
});
return submission;
}
const {
image,
tag,
workingDir,
compileOutputLimit,
runOutputLimit,
} = config.dockerConfig;
const { fileName, fileExtension } = config;
const file = `${fileName}.${fileExtension}`;
// Prepare the environment and create a container
if (await prepareEnvironment(image, tag)) {
container = await createContainer(image, tag, workingDir, problem.memoryLimit);
} else {
console.error("Docker image not found:", image, ":", tag);
submission = await prisma.submission.create({
data: {
language,
code,
status: Status.SE,
userId,
problemId,
message: "The docker environment is not ready",
},
});
return submission;
}
submission = await prisma.submission.create({
data: {
language,
code,
status: Status.PD,
userId,
problemId,
message: "",
},
});
// Upload code to the container
const tarStream = createTarStream(file, code);
await container.putArchive(tarStream, { path: workingDir });
// Compile the code
const compileResult = await compile(container, file, fileName, compileOutputLimit, submission.id, language);
if (compileResult.status === Status.CE) {
return compileResult;
}
// Run the code
const runResult = await run(container, fileName, problem.timeLimit, runOutputLimit, submission.id, testcases);
return runResult;
} catch (error) {
console.error(error);
if (submission) {
const updatedSubmission = await prisma.submission.update({
where: { id: submission.id },
data: {
status: Status.SE,
message: "System Error",
}
})
return updatedSubmission;
} else {
submission = await prisma.submission.create({
data: {
language,
code,
status: Status.PD,
userId,
problemId,
message: "",
},
})
return submission;
}
} finally {
revalidatePath(`/problems/${problemId}`);
if (container) {
try {
await container.kill();
await container.remove();
} catch (error) {
console.error("Container cleanup failed:", error);
}
}
}
}
async function compile(
container: Docker.Container,
file: string,
fileName: string,
compileOutputLimit: number = 1 * 1024 * 1024,
submissionId: string,
language: EditorLanguage,
): Promise<Submission> {
const compileCmd =
language === "c"
? ["gcc", "-O2", file, "-o", fileName]
: language === "cpp"
? ["g++", "-O2", file, "-o", fileName]
: null;
if (!compileCmd) {
return prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.SE,
message: "Unsupported language",
},
});
}
const compileExec = await container.exec({
Cmd: compileCmd,
AttachStdout: true,
AttachStderr: true,
});
return new Promise<Submission>((resolve, reject) => {
compileExec.start({}, (error, stream) => {
if (error || !stream) {
return reject({ message: "System Error", Status: Status.SE });
}
const stdoutChunks: string[] = [];
let stdoutLength = 0;
const stdoutStream = new Writable({
write(chunk, _encoding, callback) {
let text = chunk.toString();
if (stdoutLength + text.length > compileOutputLimit) {
text = text.substring(0, compileOutputLimit - stdoutLength);
stdoutChunks.push(text);
stdoutLength = compileOutputLimit;
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 > compileOutputLimit) {
text = text.substring(0, compileOutputLimit - stderrLength);
stderrChunks.push(text);
stderrLength = compileOutputLimit;
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 updatedSubmission: Submission;
if (exitCode !== 0 || stderr) {
updatedSubmission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.CE,
message: stderr || "Compilation Error",
},
});
} else {
updatedSubmission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.CS,
message: stdout,
},
});
}
resolve(updatedSubmission);
});
stream.on("error", () => {
reject({ message: "System Error", Status: Status.SE });
});
});
});
}
// Run code and implement timeout
async function run(
container: Docker.Container,
fileName: string,
timeLimit: number = 1000,
maxOutput: number = 1 * 1024 * 1024,
submissionId: string,
testcases: TestcaseWithDetails,
): Promise<Submission> {
let finalSubmission: Submission | null = null;
let maxExecutionTime = 0;
for (const testcase of testcases) {
const sortedData = testcase.data.sort((a, b) => a.index - b.index);
const inputData = sortedData.map(d => d.value).join("\n");
const runExec = await container.exec({
Cmd: [`./${fileName}`],
AttachStdout: true,
AttachStderr: true,
AttachStdin: true,
});
const result = await new Promise<Submission | TestcaseResult>((resolve, reject) => {
// Start the exec stream
runExec.start({ hijack: true }, async (error, stream) => {
if (error || !stream) {
const submission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.SE,
message: "System Error",
}
})
return resolve(submission);
}
stream.write(inputData);
stream.end();
const stdoutChunks: string[] = [];
const stderrChunks: string[] = [];
let stdoutLength = 0;
let stderrLength = 0;
const stdoutStream = new Writable({
write: (chunk, _, callback) => {
const text = chunk.toString();
if (stdoutLength + text.length > maxOutput) {
stdoutChunks.push(text.substring(0, maxOutput - stdoutLength));
stdoutLength = maxOutput;
} else {
stdoutChunks.push(text);
stdoutLength += text.length;
}
callback();
}
});
const stderrStream = new Writable({
write: (chunk, _, callback) => {
const text = chunk.toString();
if (stderrLength + text.length > maxOutput) {
stderrChunks.push(text.substring(0, maxOutput - stderrLength));
stderrLength = maxOutput;
} else {
stderrChunks.push(text);
stderrLength += text.length;
}
callback();
}
});
docker.modem.demuxStream(stream, stdoutStream, stderrStream);
const startTime = Date.now();
// Timeout mechanism
const timeoutId = setTimeout(async () => {
stream.destroy(); // Destroy the stream to stop execution
await prisma.testcaseResult.create({
data: {
isCorrect: false,
output: "",
submissionId,
testcaseId: testcase.id,
}
})
const updatedSubmission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.TLE,
message: "Time Limit Exceeded",
}
})
resolve(updatedSubmission);
}, 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;
const executionTime = Date.now() - startTime;
// Exit code 0 means successful execution
if (exitCode === 0) {
const expectedOutput = testcase.expectedOutput;
const testcaseResult = await prisma.testcaseResult.create({
data: {
isCorrect: stdout.trim() === expectedOutput.trim(),
output: stdout,
executionTime,
submissionId,
testcaseId: testcase.id,
}
})
resolve(testcaseResult);
} else if (exitCode === 137) {
await prisma.testcaseResult.create({
data: {
isCorrect: false,
output: stdout,
executionTime,
submissionId,
testcaseId: testcase.id,
}
})
const updatedSubmission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.MLE,
message: stderr || "Memory Limit Exceeded",
}
})
resolve(updatedSubmission);
} else {
await prisma.testcaseResult.create({
data: {
isCorrect: false,
output: stdout,
executionTime,
submissionId,
testcaseId: testcase.id,
}
})
const updatedSubmission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.RE,
message: stderr || "Runtime Error",
}
})
resolve(updatedSubmission);
}
});
stream.on("error", () => {
clearTimeout(timeoutId); // Clear timeout in case of error
reject({ message: "System Error", Status: Status.SE });
});
});
});
if ('status' in result) {
return result;
} else {
if (!result.isCorrect) {
finalSubmission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.WA,
message: "Wrong Answer",
},
include: {
testcaseResults: true,
}
});
return finalSubmission;
} else {
maxExecutionTime = Math.max(maxExecutionTime, result.executionTime ?? 0);
}
}
}
const maxMemoryUsage = (await container.stats({ stream: false, "one-shot": true })).memory_stats.max_usage;
finalSubmission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.AC,
message: "All testcases passed",
executionTime: maxExecutionTime,
memoryUsage: maxMemoryUsage / 1024 / 1024,
},
include: {
testcaseResults: true,
}
});
return finalSubmission;
}

View File

@ -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 },
});
}
};

View File

@ -1,50 +0,0 @@
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { auth } from "@/lib/auth";
import { User } from "@/generated/client";
import { redirect } from "next/navigation";
import { Navbar } from "@/components/navbar";
import { AppSidebar } from "@/components/app-sidebar";
import { Separator } from "@/components/ui/separator";
import { type NavUserProps } from "@/components/nav-user";
interface AdminDashboardLayoutProps {
children: React.ReactNode;
}
export default async function AdminDashboardLayout({
children,
}: AdminDashboardLayoutProps) {
const session = await auth();
if (!session?.user) {
redirect("/sign-in");
}
const user: NavUserProps["user"] = (({ name, email, image }) => ({
name: name ?? "",
email: email ?? "",
avatar: image ?? "",
}))(session.user as User);
return (
<SidebarProvider>
<AppSidebar user={user} />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Navbar />
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
{children}
</div>
</SidebarInset>
</SidebarProvider>
);
}

View File

@ -1,3 +0,0 @@
export default function DashboardAdmin() {
return <div>Dashboard Admin</div>;
}

View File

@ -1,5 +0,0 @@
import NewProblemDescriptionForm from "@/components/features/dashboard/admin/problemset/new/components/description-form";
export default function NewProblemDescriptionPage() {
return <NewProblemDescriptionForm />;
}

View File

@ -1,5 +0,0 @@
import NewProblemMetadataForm from "@/components/features/dashboard/admin/problemset/new/components/metadata-form";
export default function NewProblemMetadataPage() {
return <NewProblemMetadataForm />;
}

View File

@ -1,5 +0,0 @@
import { redirect } from "next/navigation";
export default function NewProblemPage() {
redirect("/dashboard/problemset/new/metadata");
}

View File

@ -1,5 +0,0 @@
import NewProblemSolutionForm from "@/components/features/dashboard/admin/problemset/new/components/solution-form";
export default function NewProblemSolutionPage() {
return <NewProblemSolutionForm />;
}

View File

@ -1,35 +0,0 @@
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { ProblemSchema } from "@/components/features/dashboard/admin/problemset/new/schema";
interface NewProblemActions {
setHydrated: (value: boolean) => void;
setData: (data: Partial<ProblemSchema>) => void;
}
type NewProblemState = Partial<ProblemSchema> & {
hydrated: boolean;
} & NewProblemActions;
export const useNewProblemStore = create<NewProblemState>()(
persist(
(set) => ({
hydrated: false,
setHydrated: (value) => set({ hydrated: value }),
setData: (data) => set(data),
}),
{
name: "zustand:new-problem",
storage: createJSONStorage(() => localStorage),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
partialize: ({ hydrated, ...rest }) => rest,
onRehydrateStorage: () => (state, error) => {
if (error) {
console.error("An error happened during hydration", error);
} else if (state) {
state.setHydrated(true);
}
},
}
)
);

View File

@ -1,19 +0,0 @@
import prisma from "@/lib/prisma";
import { ProblemsetTable } from "@/components/features/dashboard/admin/problemset/table";
export default async function AdminDashboardProblemsetPage() {
const problems = await prisma.problem.findMany({
select: {
id: true,
displayId: true,
title: true,
difficulty: true,
},
});
return (
<div className="h-full px-4">
<ProblemsetTable data={problems} />
</div>
);
}

View File

@ -1,77 +0,0 @@
"use client";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Loading } from "@/components/loading";
import { useAdminSettingsStore } from "@/stores/useAdminSettingsStore";
import { EditorLanguage, LanguageServerConfig } from "@/generated/client";
import { SettingsLanguageServerForm } from "@/app/(app)/dashboard/@admin/settings/language-server/form";
interface LanguageServerAccordionProps {
configs: {
language: EditorLanguage;
config: LanguageServerConfig | null;
}[];
}
export function LanguageServerAccordion({
configs,
}: LanguageServerAccordionProps) {
const { hydrated, activeLanguageServerSetting, setActiveLanguageServerSetting } =
useAdminSettingsStore();
if (!hydrated) {
return (
<div className="h-full w-full space-y-2">
<Loading className="h-12 p-0" skeletonClassName="rounded-md" />
<Loading className="h-12 p-0" skeletonClassName="rounded-md" />
</div>
);
}
return (
<Accordion
type="single"
collapsible
className="w-full space-y-2"
value={activeLanguageServerSetting}
onValueChange={setActiveLanguageServerSetting}
>
{configs.map(({ language, config }) => (
<AccordionItem
key={language}
value={language}
className="has-focus-visible:border-ring has-focus-visible:ring-ring/50 rounded-md border outline-none last:border-b has-focus-visible:ring-[3px]"
>
<AccordionTrigger className="px-4 py-3 justify-start gap-3 text-[15px] leading-6 hover:no-underline focus-visible:ring-0 [&>svg]:-order-1">
{language.toUpperCase()}
</AccordionTrigger>
<AccordionContent className="text-muted-foreground pb-0">
<div className="px-4 py-3">
<SettingsLanguageServerForm
defaultValues={
config
? {
protocol: config.protocol,
hostname: config.hostname,
port: config.port,
path: config.path,
}
: {
port: null,
path: null,
}
}
language={language}
/>
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
);
}

View File

@ -1,181 +0,0 @@
"use client";
import { z } from "zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useForm } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { zodResolver } from "@hookform/resolvers/zod";
import { EditorLanguage, LanguageServerProtocol } from "@/generated/client";
import { handleLanguageServerConfigSubmit } from "@/actions/language-server";
const settingsLanguageServerFormSchema = z.object({
protocol: z.nativeEnum(LanguageServerProtocol),
hostname: z.string(),
port: z
.number()
.nullable()
.transform((val) => (val === undefined ? null : val)),
path: z
.string()
.nullable()
.transform((val) => (val === "" || val === undefined ? null : val)),
});
export type SettingsLanguageServerFormValues = z.infer<typeof settingsLanguageServerFormSchema>;
interface SettingsLanguageServerFormProps {
defaultValues: Partial<SettingsLanguageServerFormValues>;
language: EditorLanguage;
}
export function SettingsLanguageServerForm({
defaultValues,
language,
}: SettingsLanguageServerFormProps) {
const form = useForm<SettingsLanguageServerFormValues>({
resolver: zodResolver(settingsLanguageServerFormSchema),
defaultValues,
mode: "onChange",
});
const onSubmit = async (data: SettingsLanguageServerFormValues) => {
await handleLanguageServerConfigSubmit(language, data);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="px-7">
<FormField
control={form.control}
name="protocol"
render={({ field }) => (
<FormItem className="pt-0 pb-4 grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
<div className="space-y-2">
<FormLabel>Protocol</FormLabel>
<FormDescription>
This is the protocol of the language server.
</FormDescription>
</div>
<div className="space-y-2">
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a protocol" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="wss">wss</SelectItem>
<SelectItem value="ws">ws</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</div>
</FormItem>
)}
/>
<Separator />
<FormField
control={form.control}
name="hostname"
render={({ field }) => (
<FormItem className="py-4 grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
<div className="space-y-2">
<FormLabel>Hostname</FormLabel>
<FormDescription>
This is the hostname of the language server.
</FormDescription>
</div>
<div className="space-y-2">
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="w-full"
/>
</FormControl>
<FormMessage />
</div>
</FormItem>
)}
/>
<Separator />
<FormField
control={form.control}
name="port"
render={({ field }) => (
<FormItem className="py-4 grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
<div className="space-y-2">
<FormLabel>Port</FormLabel>
<FormDescription>
This is the port of the language server.
</FormDescription>
</div>
<div className="space-y-2">
<FormControl>
<Input
{...field}
type="number"
value={field.value ?? ""}
onChange={(e) => {
const value = e.target.value;
field.onChange(value === "" ? null : Number(value));
}}
className="w-full"
/>
</FormControl>
<FormMessage />
</div>
</FormItem>
)}
/>
<Separator />
<FormField
control={form.control}
name="path"
render={({ field }) => (
<FormItem className="pt-4 pb-3 grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
<div className="space-y-2">
<FormLabel>Path</FormLabel>
<FormDescription>
This is the path of the language server.
</FormDescription>
</div>
<div className="space-y-2">
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="w-full"
/>
</FormControl>
<FormMessage />
</div>
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit" className="w-full md:w-auto">
Update
</Button>
</div>
</form>
</Form>
);
}

View File

@ -1,26 +0,0 @@
import { Suspense } from "react";
import { Loading } from "@/components/loading";
import { Separator } from "@/components/ui/separator";
interface SettingsLanguageServerLayoutProps {
children: React.ReactNode;
}
export default function SettingsLanguageServerLayout({
children,
}: SettingsLanguageServerLayoutProps) {
return (
<div className="container mx-auto max-w-[1024px] space-y-6">
<div>
<h3 className="text-lg font-medium">Language Server Settings</h3>
<p className="text-sm text-muted-foreground">
Configure the language server connection settings.
</p>
</div>
<Separator />
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</div>
);
}

View File

@ -1,16 +0,0 @@
import { EditorLanguage } from "@/generated/client";
import { getLanguageServerConfig } from "@/actions/language-server";
import { LanguageServerAccordion } from "@/app/(app)/dashboard/@admin/settings/language-server/accordion";
export default async function SettingsLanguageServerPage() {
const languages = Object.values(EditorLanguage);
const configPromises = languages.map(async (language) => ({
language,
config: await getLanguageServerConfig(language),
}));
const configs = await Promise.all(configPromises);
return <LanguageServerAccordion configs={configs} />;
}

View File

@ -1,20 +0,0 @@
import { auth } from "@/lib/auth";
import { User } from "@/generated/client";
import { notFound, redirect } from "next/navigation";
interface DashboardLayoutProps {
admin: React.ReactNode;
}
export default async function DashboardLayout({
admin,
}: DashboardLayoutProps) {
const session = await auth();
if (!session?.user) {
redirect("/sign-in");
}
const user = session.user as User;
return user.role === "ADMIN" ? admin : notFound();
}

View File

@ -1,16 +0,0 @@
import { Suspense } from "react";
import { Loading } from "@/components/loading";
interface BotLayoutProps {
children: React.ReactNode;
}
export default function BotLayout({ children }: BotLayoutProps) {
return (
<div className="flex flex-col h-full border border-t-0 border-muted rounded-b-3xl bg-background">
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</div>
);
}

View File

@ -1,127 +0,0 @@
"use client";
import { toast } from "sonner";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useCallback } from "react";
import { useChat } from "@ai-sdk/react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { useProblem } from "@/hooks/use-problem";
import MdxPreview from "@/components/mdx-preview";
import { Textarea } from "@/components/ui/textarea";
import { BotIcon, SendHorizonal } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ChatMessageList } from "@/components/ui/chat/chat-message-list";
import { ChatBubble, ChatBubbleMessage } from "@/components/ui/chat/chat-bubble";
export default function Bot() {
const t = useTranslations("Bot");
const { problemId, problem, currentLang, currentValue } = useProblem();
const { messages, input, handleInputChange, setMessages, handleSubmit } = useChat({
initialMessages: [
{
id: problemId,
role: "system",
content: `Problem description:\n${problem.description}`,
},
],
});
const handleFormSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) {
toast.error("Input cannot be empty");
return;
}
const currentCodeMessage = {
id: problemId,
role: "system" as const,
content: `Current code:\n\`\`\`${currentLang}\n${currentValue}\n\`\`\``,
};
setMessages((prev) => [...prev, currentCodeMessage]);
handleSubmit();
},
[currentLang, currentValue, handleSubmit, input, problemId, setMessages]
);
return (
<>
<div className="flex-1 relative">
{!messages.some(
(message) => message.role === "user" || message.role === "assistant"
) && (
<div className="h-full flex flex-col items-center justify-center gap-2 text-muted-foreground">
<BotIcon />
<span>{t("title")}</span>
<span className="font-thin text-xs">{t("description")}</span>
</div>
)}
<div className="absolute h-full w-full">
<ScrollArea className="h-full [&>[data-radix-scroll-area-viewport]>div:min-w-0 [&>[data-radix-scroll-area-viewport]>div]:!block">
<ChatMessageList>
{messages
.filter(
(message) => message.role === "user" || message.role === "assistant"
)
.map((message) => (
<ChatBubble key={message.id} layout="ai" className="border-b pb-4">
<ChatBubbleMessage layout="ai">
<MdxPreview source={message.content} />
</ChatBubbleMessage>
</ChatBubble>
))}
</ChatMessageList>
</ScrollArea>
</div>
</div>
<footer className="h-36 flex flex-none">
<form onSubmit={handleFormSubmit} className="w-full p-4 pt-0 relative">
<Textarea
value={input}
onChange={handleInputChange}
onKeyDown={(e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
if (input.trim()) {
handleFormSubmit(e);
} else {
toast.error("Input cannot be empty");
}
}
}}
className="h-full bg-muted border-transparent shadow-none rounded-lg"
placeholder={t("placeholder")}
/>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="submit"
variant="ghost"
className="absolute bottom-6 right-6 h-6 w-auto px-2"
aria-label="Send Message"
>
<SendHorizonal className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">Ctrl + Enter</TooltipContent>
</Tooltip>
</TooltipProvider>
</form>
</footer>
</>
);
}

View File

@ -1,20 +0,0 @@
import { Suspense } from "react";
import { Loading } from "@/components/loading";
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 (
<div className="flex flex-col h-full border border-t-0 border-muted rounded-b-3xl bg-background">
<WorkspaceEditorHeader />
<Suspense fallback={<Loading />}>
{children}
</Suspense>
<WorkspaceEditorFooter />
</div>
);
}

View File

@ -1,11 +0,0 @@
import { ProblemEditor } from "@/components/problem-editor";
export default function CodePage() {
return (
<div className="relative flex-1">
<div className="absolute w-full h-full">
<ProblemEditor />
</div>
</div>
);
}

View File

@ -1,16 +0,0 @@
import { Suspense } from "react";
import { Loading } from "@/components/loading";
interface DescriptionLayoutProps {
children: React.ReactNode;
}
export default function DescriptionLayout({ children }: DescriptionLayoutProps) {
return (
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-3xl bg-background">
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</div>
);
}

View File

@ -1,42 +0,0 @@
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
import MdxPreview from "@/components/mdx-preview";
import { ScrollArea } from "@/components/ui/scroll-area";
import ProblemDescriptionFooter from "@/components/features/playground/problem/description/footer";
interface DescriptionPageProps {
params: Promise<{ id: string }>;
}
export default async function DescriptionPage({ params }: DescriptionPageProps) {
const { id } = await params;
if (!id) {
return notFound();
}
const problem = await prisma.problem.findUnique({
where: { id },
select: {
title: true,
description: true,
},
});
if (!problem) {
return notFound();
}
return (
<>
<div className="relative flex-1">
<div className="absolute h-full w-full">
<ScrollArea className="h-full [&>[data-radix-scroll-area-viewport]>div:min-w-0 [&>[data-radix-scroll-area-viewport]>div]:!block bg-background">
<MdxPreview source={problem.description} className="p-4 md:p-6" />
</ScrollArea>
</div>
</div>
<ProblemDescriptionFooter title={problem.title} />
</>
);
}

View File

@ -1,16 +0,0 @@
import { Suspense } from "react";
import { getUserLocale } from "@/i18n/locale";
import { Loading } from "@/components/loading";
import DetailsPage from "@/app/(app)/problems/[id]/@Details/page";
export default async function DetailsLayout() {
const locale = await getUserLocale();
return (
<div className="flex flex-col h-full border border-t-0 border-muted rounded-b-3xl bg-background">
<Suspense fallback={<Loading />}>
<DetailsPage locale={locale} />;
</Suspense>
</div>
);
}

View File

@ -1,211 +0,0 @@
"use client";
import { cn } from "@/lib/utils";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Locale } from "@/config/i18n";
import { getLocale } from "@/lib/i18n";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { ArrowLeftIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useProblem } from "@/hooks/use-problem";
import MdxPreview from "@/components/mdx-preview";
import { useDockviewStore } from "@/stores/dockview";
import { Separator } from "@/components/ui/separator";
import { getStatusColorClass, statusMap } from "@/lib/status";
import type { TestcaseResultWithTestcase } from "@/types/prisma";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { formatDistanceToNow, isBefore, subDays, format } from "date-fns";
interface DetailsPageProps {
locale: Locale;
}
export default function DetailsPage({ locale }: DetailsPageProps) {
const localeInstance = getLocale(locale);
const t = useTranslations("DetailsPage");
const s = useTranslations("StatusMessage");
const { api, submission } = useDockviewStore();
const { editorLanguageConfigs, problemId } = useProblem();
const [lastFailedTestcase, setLastFailedTestcase] =
useState<TestcaseResultWithTestcase | null>(null);
useEffect(() => {
if (!api || !problemId || !submission?.id) return;
if (problemId !== submission.problemId) {
const detailsPanel = api.getPanel("Details");
if (detailsPanel) {
api.removePanel(detailsPanel);
}
}
}, [api, problemId, submission]);
useEffect(() => {
if (!submission?.id || !submission.testcaseResults) return;
const failedTestcases = submission.testcaseResults.filter(
(result) =>
(submission.status === "WA" && !result.isCorrect) ||
(submission.status === "TLE" && !result.isCorrect) ||
(submission.status === "MLE" && !result.isCorrect) ||
(submission.status === "RE" && !result.isCorrect)
);
setLastFailedTestcase(failedTestcases[0]);
}, [submission]);
if (!api || !problemId || !submission?.id) return null;
const createdAt = new Date(submission.createdAt);
const submittedDisplay = isBefore(createdAt, subDays(new Date(), 1))
? format(createdAt, "yyyy-MM-dd")
: formatDistanceToNow(createdAt, { addSuffix: true, locale: localeInstance });
const source = `\`\`\`${submission?.language}\n${submission?.code}\n\`\`\``;
const handleClick = () => {
if (!api) return;
const submissionsPanel = api.getPanel("Submissions");
submissionsPanel?.api.setActive();
const detailsPanel = api.getPanel("Details");
if (detailsPanel) {
api.removePanel(detailsPanel);
}
};
return (
<>
<div className="h-8 flex flex-none items-center px-2 py-1 border-b">
<Button
onClick={handleClick}
variant="ghost"
className="h-8 w-auto p-2 hover:bg-transparent text-muted-foreground hover:text-foreground"
>
<ArrowLeftIcon size={16} aria-hidden="true" />
<span>{t("BackButton")}</span>
</Button>
</div>
<div className="relative flex-1">
<div className="absolute h-full w-full">
<ScrollArea className="h-full">
<div className="flex flex-col mx-auto max-w-[700px] gap-4 px-4 py-3">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-4">
<div className="flex flex-1 flex-col items-start gap-1 overflow-hidden">
<h3
className={cn(
"flex items-center text-xl font-semibold",
getStatusColorClass(submission.status)
)}
>
<span>{s(`${statusMap.get(submission.status)?.message}`)}</span>
</h3>
<div className="flex max-w-full flex-1 items-center gap-1 overflow-hidden text-xs">
<span className="whitespace-nowrap mr-1">{t("Time")}</span>
<span className="max-w-full truncate">
{submittedDisplay}
</span>
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-6">
{lastFailedTestcase && (
<div className="space-y-4">
<div className="space-y-2">
<Accordion
type="single"
collapsible
className="w-full -space-y-px"
>
<AccordionItem
value="input"
className="bg-background has-focus-visible:border-ring has-focus-visible:ring-ring/50 relative border px-4 py-1 outline-none first:rounded-t-md last:rounded-b-md last:border-b has-focus-visible:z-10 has-focus-visible:ring-[3px]"
>
<AccordionTrigger className="py-2 text-[15px] leading-6 hover:no-underline focus-visible:ring-0">
<h4 className="text-sm font-medium">{t("Input")}</h4>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground pb-2">
<div className="space-y-4">
{lastFailedTestcase.testcase.data.map((field) => (
<div key={field.id} className="space-y-2">
<label className="text-sm font-medium">
{`${field.label} =`}
</label>
<Input
type="text"
value={field.value}
readOnly
className="bg-muted border-transparent shadow-none rounded-lg h-10"
/>
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<div className="space-y-2">
<h4 className="text-sm font-medium">{t("ExpectedOutput")}</h4>
<Input
type="text"
value={lastFailedTestcase.testcase.expectedOutput}
readOnly
className="bg-muted border-transparent shadow-none rounded-lg h-10 font-mono"
/>
</div>
{submission.status === "WA" && (
<div className="space-y-2">
<h4 className="text-sm font-medium">{t("ActualOutput")}</h4>
<Input
type="text"
value={lastFailedTestcase.output}
readOnly
className="bg-muted border-transparent shadow-none rounded-lg h-10 font-mono"
/>
</div>
)}
</div>
)}
{(submission.status === "CE" ||
submission.status === "SE") && (
<MdxPreview
source={`\`\`\`shell\n${submission.message}\n\`\`\``}
/>
)}
<div className="flex items-center pb-2">
<div className="flex items-center gap-2 text-sm font-medium">
<span>{t("Code")}</span>
<Separator
orientation="vertical"
className="h-4 bg-muted-foreground"
/>
<span>
{
editorLanguageConfigs.find(
(config) =>
config.language === submission.language
)?.label
}
</span>
</div>
</div>
<MdxPreview source={source} />
</div>
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
</div>
</>
);
}

View File

@ -1,16 +0,0 @@
import { Suspense } from "react";
import { Loading } from "@/components/loading";
interface SolutionsLayoutProps {
children: React.ReactNode;
}
export default async function SolutionsLayout({ children }: SolutionsLayoutProps) {
return (
<div className="flex flex-col h-full border border-t-0 border-muted rounded-b-3xl bg-background">
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</div>
);
}

View File

@ -1,43 +0,0 @@
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
import MdxPreview from "@/components/mdx-preview";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import ProblemSolutionFooter from "@/components/features/playground/problem/solution/footer";
interface SolutionsPageProps {
params: Promise<{ id: string }>;
}
export default async function SolutionsPage({ params }: SolutionsPageProps) {
const { id } = await params;
if (!id) {
return notFound();
}
const problem = await prisma.problem.findUnique({
where: { id },
select: {
title: true,
solution: true,
},
});
if (!problem) {
return notFound();
}
return (
<>
<div className="relative flex-1">
<div className="absolute h-full w-full">
<ScrollArea className="h-full [&>[data-radix-scroll-area-viewport]>div:min-w-0 [&>[data-radix-scroll-area-viewport]>div]:!block bg-background">
<MdxPreview source={problem.solution} className="p-4 md:p-6" />
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
</div>
<ProblemSolutionFooter title={problem.title} />
</>
);
}

View File

@ -1,16 +0,0 @@
import { Suspense } from "react";
import { Loading } from "@/components/loading";
interface SubmissionsLayoutProps {
children: React.ReactNode;
}
export default function SubmissionsLayout({ children }: SubmissionsLayoutProps) {
return (
<div className="flex flex-col h-full px-3 border border-t-0 border-muted rounded-b-3xl bg-background">
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</div>
);
}

View File

@ -1,64 +0,0 @@
import prisma from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { notFound } from "next/navigation";
import { getUserLocale } from "@/i18n/locale";
import SubmissionsTable from "@/components/submissions-table";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import SubmissionLoginButton from "@/components/submission-login-button";
interface SubmissionsPageProps {
params: Promise<{ id: string }>;
}
export default async function SubmissionsPage({ params }: SubmissionsPageProps) {
const { id } = await params;
const session = await auth();
if (!id) {
return notFound();
}
if (!session?.user?.id) {
return (
<SubmissionLoginButton />
)
}
const problem = await prisma.problem.findUnique({
where: { id },
select: {
submissions: {
where: {
userId: session.user.id,
},
include: {
testcaseResults: {
include: {
testcase: {
include: {
data: true,
},
},
},
},
},
},
},
});
if (!problem) {
return notFound();
}
const locale = await getUserLocale();
return (
<>
<ScrollArea className="h-full">
<SubmissionsTable locale={locale} submissions={problem.submissions} />
<ScrollBar orientation="horizontal" />
</ScrollArea>
</>
);
}

View File

@ -1,16 +0,0 @@
import { Suspense } from "react";
import { Loading } from "@/components/loading";
interface TestcaseLayoutProps {
children: React.ReactNode;
}
export default function TestcaseLayout({ children }: TestcaseLayoutProps) {
return (
<div className="relative h-full border border-t-0 border-muted rounded-b-3xl bg-background">
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</div>
);
}

View File

@ -1,39 +0,0 @@
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
import TestcaseCard from "@/components/testcase-card";
import { ScrollArea } from "@/components/ui/scroll-area";
interface TestcasePageProps {
params: Promise<{ id: string }>;
}
export default async function TestcasePage({ params }: TestcasePageProps) {
const { id } = await params;
if (!id) {
return notFound();
}
const problem = await prisma.problem.findUnique({
where: { id },
select: {
testcases: {
include: {
data: true,
},
},
},
});
if (!problem) {
return notFound();
}
return (
<div className="absolute h-full w-full">
<ScrollArea className="h-full">
<TestcaseCard testcases={problem.testcases} />
</ScrollArea>
</div>
);
}

View File

@ -1,90 +0,0 @@
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
import { getUserLocale } from "@/i18n/locale";
import ProblemPage from "@/app/(app)/problems/[id]/page";
import { ProblemStoreProvider } from "@/providers/problem-store-provider";
import { PlaygroundHeader } from "@/components/features/playground/header";
interface ProblemProps {
params: Promise<{ id: string }>;
Description: React.ReactNode;
Solutions: React.ReactNode;
Submissions: React.ReactNode;
Details: React.ReactNode;
Code: React.ReactNode;
Testcase: React.ReactNode;
Bot: React.ReactNode;
}
export default async function ProblemLayout({
params,
Description,
Solutions,
Submissions,
Details,
Code,
Testcase,
Bot,
}: ProblemProps) {
const { id } = await params;
if (!id) {
return notFound();
}
const [
problem,
editorLanguageConfigs,
languageServerConfigs,
submissions,
] = await Promise.all([
prisma.problem.findUnique({
where: { id },
include: {
templates: true,
testcases: {
include: {
data: true,
},
},
},
}),
prisma.editorLanguageConfig.findMany(),
prisma.languageServerConfig.findMany(),
prisma.submission.findMany({
where: { problemId: id },
}),
]);
if (!problem) {
return notFound();
}
const locale = await getUserLocale();
return (
<div className="flex flex-col h-screen">
<ProblemStoreProvider
problemId={id}
problem={problem}
editorLanguageConfigs={editorLanguageConfigs}
languageServerConfigs={languageServerConfigs}
submissions={submissions}
>
<PlaygroundHeader />
<main className="flex flex-grow overflow-y-hidden p-2.5 pt-0">
<ProblemPage
locale={locale}
Description={Description}
Solutions={Solutions}
Submissions={Submissions}
Details={Details}
Code={Code}
Testcase={Testcase}
Bot={Bot}
/>
</main>
</ProblemStoreProvider>
</div>
);
}

View File

@ -1,148 +0,0 @@
"use client";
import {
BotIcon,
CircleCheckBigIcon,
FileTextIcon,
FlaskConicalIcon,
SquareCheckIcon,
SquarePenIcon,
} from "lucide-react";
import { Locale } from "@/config/i18n";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import Dockview from "@/components/dockview";
import { useDockviewStore } from "@/stores/dockview";
interface ProblemPageProps {
locale: Locale;
Description: React.ReactNode;
Solutions: React.ReactNode;
Submissions: React.ReactNode;
Details: React.ReactNode;
Code: React.ReactNode;
Testcase: React.ReactNode;
Bot: React.ReactNode;
}
export default function ProblemPage({
locale,
Description,
Solutions,
Submissions,
Details,
Code,
Testcase,
Bot,
}: ProblemPageProps) {
const [key, setKey] = useState(0);
const { setApi } = useDockviewStore();
const t = useTranslations("ProblemPage");
useEffect(() => {
setKey((prevKey) => prevKey + 1);
}, [locale]);
return (
<Dockview
key={key}
storageKey="dockview:problem"
onApiReady={setApi}
options={[
{
id: "Description",
component: "Description",
tabComponent: "Description",
params: {
icon: FileTextIcon,
content: Description,
title: t("Description"),
},
},
{
id: "Solutions",
component: "Solutions",
tabComponent: "Solutions",
params: {
icon: FlaskConicalIcon,
content: Solutions,
title: t("Solutions"),
},
position: {
referencePanel: "Description",
direction: "within",
},
inactive: true,
},
{
id: "Submissions",
component: "Submissions",
tabComponent: "Submissions",
params: {
icon: CircleCheckBigIcon,
content: Submissions,
title: t("Submissions"),
},
position: {
referencePanel: "Solutions",
direction: "within",
},
inactive: true,
},
{
id: "Details",
component: "Details",
tabComponent: "Details",
params: {
icon: CircleCheckBigIcon,
content: Details,
title: t("Details"),
autoAdd: false,
},
},
{
id: "Code",
component: "Code",
tabComponent: "Code",
params: {
icon: SquarePenIcon,
content: Code,
title: t("Code"),
},
position: {
referencePanel: "Submissions",
direction: "right",
},
},
{
id: "Testcase",
component: "Testcase",
tabComponent: "Testcase",
params: {
icon: SquareCheckIcon,
content: Testcase,
title: t("Testcase"),
},
position: {
referencePanel: "Code",
direction: "below",
},
},
{
id: "Bot",
component: "Bot",
tabComponent: "Bot",
params: {
icon: BotIcon,
content: Bot,
title: t("Bot"),
autoAdd: false,
},
position: {
direction: "right",
},
},
]}
/>
);
}

View File

@ -0,0 +1,27 @@
import { notFound } from "next/navigation";
import { ProblemHeader } from "@/features/problems/components/header";
interface ProblemLayoutProps {
children: React.ReactNode;
params: Promise<{ problemId: string }>;
}
export default async function ProblemLayout({
children,
params,
}: ProblemLayoutProps) {
const { problemId } = await params;
if (!problemId) {
return notFound();
}
return (
<div className="flex flex-col h-screen">
<ProblemHeader />
<div className="flex w-full flex-grow overflow-y-hidden p-2.5 pt-0">
{children}
</div>
</div>
);
}

View File

@ -0,0 +1,39 @@
import { TestcasePanel } from "@/features/problems/testcase/panel";
import { BotPanel } from "@/features/problems/bot/components/panel";
import { CodePanel } from "@/features/problems/code/components/panel";
import { DetailPanel } from "@/features/problems/detail/components/panel";
import { SolutionPanel } from "@/features/problems/solution/components/panel";
import { SubmissionPanel } from "@/features/problems/submission/components/panel";
import { DescriptionPanel } from "@/features/problems/description/components/panel";
import { ProblemFlexLayout } from "@/features/problems/components/problem-flexlayout";
interface ProblemPageProps {
params: Promise<{ problemId: string }>;
searchParams: Promise<{
submissionId: string | undefined;
}>;
}
export default async function ProblemPage({
params,
searchParams,
}: ProblemPageProps) {
const { problemId } = await params;
const { submissionId } = await searchParams;
const components: Record<string, React.ReactNode> = {
description: <DescriptionPanel problemId={problemId} />,
solution: <SolutionPanel problemId={problemId} />,
submission: <SubmissionPanel problemId={problemId} />,
detail: <DetailPanel submissionId={submissionId} />,
code: <CodePanel problemId={problemId} />,
testcase: <TestcasePanel problemId={problemId} />,
bot: <BotPanel problemId={problemId} />,
};
return (
<div className="relative flex h-full w-full">
<ProblemFlexLayout components={components} />
</div>
);
}

View File

@ -1,5 +1,4 @@
import { Banner } from "@/components/banner";
import { AvatarButton } from "@/components/avatar-button";
import { ProblemsetHeader } from "@/features/problemset/components/header";
interface ProblemsetLayoutProps {
children: React.ReactNode;
@ -7,14 +6,9 @@ interface ProblemsetLayoutProps {
export default function ProblemsetLayout({ children }: ProblemsetLayoutProps) {
return (
<div className="relative h-screen flex flex-col">
<Banner />
<div className="absolute top-2 right-4">
<AvatarButton />
</div>
<main className="h-full container mx-auto p-4">
{children}
</main>
<div className="h-full flex flex-col">
<ProblemsetHeader />
{children}
</div>
);
}

View File

@ -1,73 +1,15 @@
import Link from "next/link";
import { Suspense } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import prisma from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { getTranslations } from "next-intl/server";
import { getDifficultyColorClass } from "@/lib/utils";
import { CircleCheckBigIcon, CircleDotIcon } from "lucide-react";
export default async function ProblemsetPage() {
const problems = await prisma.problem.findMany({
where: { published: true },
orderBy: { id: "asc" },
select: { id: true, title: true, difficulty: true },
});
const session = await auth();
const userId = session?.user?.id;
const submissions = userId
? await prisma.submission.findMany({
where: { userId },
select: { problemId: true, status: true },
})
: [];
const completedProblems = new Set(submissions.filter(s => s.status === "AC").map(s => s.problemId));
const attemptedProblems = new Set(submissions.filter(s => s.status !== "AC").map(s => s.problemId));
const t = await getTranslations();
ProblemsetTable,
ProblemsetTableSkeleton,
} from "@/features/problemset/components/table";
export default function ProblemsetPage() {
return (
<Table>
<TableHeader className="bg-transparent">
<TableRow className="hover:bg-transparent">
<TableHead className="w-1/3">{t("ProblemsetPage.Status")}</TableHead>
<TableHead className="w-1/3">{t("ProblemsetPage.Title")}</TableHead>
<TableHead className="w-1/3">{t("ProblemsetPage.Difficulty")}</TableHead>
</TableRow>
</TableHeader>
<TableBody className="[&_td:first-child]:rounded-l-lg [&_td:last-child]:rounded-r-lg">
{problems.map((problem, index) => (
<TableRow
key={problem.id}
className="h-10 border-b-0 odd:bg-muted/50 hover:text-blue-500 hover:bg-muted"
>
<TableCell className="py-2.5">
{userId && (completedProblems.has(problem.id) ? (
<CircleCheckBigIcon className="text-green-500" size={18} aria-hidden="true" />
) : attemptedProblems.has(problem.id) ? (
<CircleDotIcon className="text-yellow-500" size={18} aria-hidden="true" />
) : null)}
</TableCell>
<TableCell className="py-2.5">
<Link href={`/problems/${problem.id}`} className="hover:text-blue-500" prefetch>
{index + 1}. {problem.title}
</Link>
</TableCell>
<TableCell className={`py-2.5 ${getDifficultyColorClass(problem.difficulty)}`}>
{t(`Difficulty.${problem.difficulty}`)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="h-full container mx-auto p-4">
<Suspense fallback={<ProblemsetTableSkeleton />}>
<ProblemsetTable />
</Suspense>
</div>
);
}

View File

@ -1,36 +1,26 @@
import Link from "next/link";
import Image from "next/image";
import { CodeIcon } from "lucide-react";
import { useTranslations } from "next-intl";
interface AuthLayoutProps {
children: React.ReactNode;
}
export default async function AuthLayout({
children
}: AuthLayoutProps) {
export default function AuthLayout({ children }: AuthLayoutProps) {
const t = useTranslations("Video");
return (
<div className="grid min-h-svh lg:grid-cols-2">
<div className="flex flex-col gap-4 p-6 md:p-10">
<div className="flex justify-center gap-2 md:justify-start">
<Link href="/" className="flex items-center gap-2 font-medium">
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
<CodeIcon className="size-4" />
</div>
Judge4c
</Link>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-sm">{children}</div>
</div>
</div>
{children}
<div className="relative hidden bg-muted lg:block">
<Image
src="/placeholder.svg"
alt="Image"
fill
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
/>
<video
autoPlay
loop
muted
playsInline
className="absolute inset-0 h-full w-full object-cover"
>
<source src="/sign-in.mp4" type="video/mp4" />
{t("unsupportedBrowser")}
</video>
</div>
</div>
);

View File

@ -1,5 +1,102 @@
import { SignInForm } from "@/components/sign-in-form";
import Link from "next/link";
import { CodeIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { providerMap, signIn } from "@/lib/auth";
import { getTranslations } from "next-intl/server";
import { FaGithub, FaGoogle } from "react-icons/fa";
import { CredentialsSignInForm } from "@/components/credentials-sign-in-form";
export default function SignInPage() {
return <SignInForm />;
interface ProviderIconProps {
providerId: string;
}
const ProviderIcon = ({ providerId }: ProviderIconProps) => {
switch (providerId) {
case "github":
return <FaGithub />;
case "google":
return <FaGoogle />;
default:
return null;
}
};
interface SignInPageProps {
searchParams: Promise<{
callbackUrl: string | undefined;
}>;
}
export default async function SignInPage({ searchParams }: SignInPageProps) {
const { callbackUrl } = await searchParams;
const t = await getTranslations("SignInForm");
return (
<div className="flex flex-col gap-4 p-6 md:p-10">
<div className="flex justify-center gap-2 md:justify-start">
<Link
href={callbackUrl ?? "/"}
className="flex items-center gap-2 font-medium"
>
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
<CodeIcon className="size-4" />
</div>
Judge4c
</Link>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
<div className="flex flex-col items-center gap-2 text-center">
<h1 className="text-2xl font-bold">{t("title")}</h1>
<p className="text-balance text-sm text-muted-foreground">
{t("description")}
</p>
</div>
<CredentialsSignInForm callbackUrl={callbackUrl} />
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
<span className="relative z-10 bg-background px-2 text-muted-foreground">
{t("or")}
</span>
</div>
{Object.values(providerMap).map((provider) => {
return (
<form
key={provider.id}
action={async () => {
"use server";
await signIn(provider.id, {
redirectTo: callbackUrl,
});
}}
>
<Button
variant="outline"
className="w-full flex items-center justify-center gap-4"
type="submit"
>
<ProviderIcon providerId={provider.id} />
{t("oauth", { provider: provider.name })}
</Button>
</form>
);
})}
<div className="text-center text-sm">
{t("noAccount")}{" "}
<Link
href={`/sign-up${
callbackUrl
? `?callbackUrl=${encodeURIComponent(callbackUrl)}`
: ""
}`}
className="underline underline-offset-4"
>
{t("signUp")}
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,5 +1,102 @@
import { SignUpForm } from "@/components/sign-up-form";
import Link from "next/link";
import { CodeIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { providerMap, signIn } from "@/lib/auth";
import { getTranslations } from "next-intl/server";
import { FaGithub, FaGoogle } from "react-icons/fa";
import { CredentialsSignUpForm } from "@/components/credentials-sign-up-form";
export default function SignUpPage() {
return <SignUpForm />;
interface ProviderIconProps {
providerId: string;
}
const ProviderIcon = ({ providerId }: ProviderIconProps) => {
switch (providerId) {
case "github":
return <FaGithub />;
case "google":
return <FaGoogle />;
default:
return null;
}
};
interface SignUpPageProps {
searchParams: Promise<{
callbackUrl: string | undefined;
}>;
}
export default async function SignInPage({ searchParams }: SignUpPageProps) {
const { callbackUrl } = await searchParams;
const t = await getTranslations("SignUpForm");
return (
<div className="flex flex-col gap-4 p-6 md:p-10">
<div className="flex justify-center gap-2 md:justify-start">
<Link
href={callbackUrl ?? "/"}
className="flex items-center gap-2 font-medium"
>
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
<CodeIcon className="size-4" />
</div>
Judge4c
</Link>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
<div className="flex flex-col items-center gap-2 text-center">
<h1 className="text-2xl font-bold">{t("title")}</h1>
<p className="text-balance text-sm text-muted-foreground">
{t("description")}
</p>
</div>
<CredentialsSignUpForm callbackUrl={callbackUrl} />
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
<span className="relative z-10 bg-background px-2 text-muted-foreground">
{t("or")}
</span>
</div>
{Object.values(providerMap).map((provider) => {
return (
<form
key={provider.id}
action={async () => {
"use server";
await signIn(provider.id, {
redirectTo: callbackUrl,
});
}}
>
<Button
variant="outline"
className="w-full flex items-center justify-center gap-4"
type="submit"
>
<ProviderIcon providerId={provider.id} />
{t("oauth", { provider: provider.name })}
</Button>
</form>
);
})}
<div className="text-center text-sm">
{t("haveAccount")}{" "}
<Link
href={`/sign-in${
callbackUrl
? `?callbackUrl=${encodeURIComponent(callbackUrl)}`
: ""
}`}
className="underline underline-offset-4"
>
{t("signIn")}
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,6 @@
import { Complexity } from "@/types/complexity";
export const analyzeComplexity = async (content: string) => {
console.log("🚀 ~ analyzeComplexity ~ content:", content);
return { time: Complexity.Enum["O(N)"], space: Complexity.Enum["O(1)"] };
};

View File

@ -1,6 +1,6 @@
"use server";
import bcrypt from "bcrypt";
import bcrypt from "bcryptjs";
import prisma from "@/lib/prisma";
import { signIn } from "@/lib/auth";
import { authSchema } from "@/lib/zod";
@ -10,7 +10,9 @@ import { CredentialsSignUpFormValues } from "@/components/credentials-sign-up-fo
const saltRounds = 10;
export async function signInWithCredentials(formData: CredentialsSignInFormValues, redirectTo?: string) {
export const signInWithCredentials = async (
formData: CredentialsSignInFormValues
) => {
const t = await getTranslations("signInWithCredentials");
try {
@ -36,21 +38,30 @@ export async function signInWithCredentials(formData: CredentialsSignInFormValue
throw new Error(t("incorrectPassword"));
}
await signIn("credentials", { ...formData, redirectTo, redirect: !!redirectTo });
await signIn("credentials", {
...formData,
redirect: false,
});
return { success: true };
} catch (error) {
return { error: error instanceof Error ? error.message : t("signInFailedFallback") };
return {
error: error instanceof Error ? error.message : t("signInFailedFallback"),
};
}
}
};
export async function signUpWithCredentials(formData: CredentialsSignUpFormValues) {
export const signUpWithCredentials = async (
formData: CredentialsSignUpFormValues
) => {
const t = await getTranslations("signUpWithCredentials");
try {
const validatedData = await authSchema.parseAsync(formData);
// Check if user already exists
const existingUser = await prisma.user.findUnique({ where: { email: validatedData.email } });
const existingUser = await prisma.user.findUnique({
where: { email: validatedData.email },
});
if (existingUser) {
throw new Error(t("userAlreadyExists"));
}
@ -64,15 +75,19 @@ export async function signUpWithCredentials(formData: CredentialsSignUpFormValue
// 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" } });
await prisma.user.update({
where: { id: user.id },
data: { role: "ADMIN" },
});
}
return { success: true };
} catch (error) {
return { error: error instanceof Error ? error.message : t("registrationFailedFallback") };
return {
error:
error instanceof Error
? error.message
: t("registrationFailedFallback"),
};
}
}
export async function signInWithGithub(redirectTo?: string) {
await signIn("github", { redirectTo, redirect: !!redirectTo });
}
};

102
src/app/actions/compile.ts Normal file
View File

@ -0,0 +1,102 @@
import "server-only";
import Docker from "dockerode";
import prisma from "@/lib/prisma";
import { createLimitedStream, docker } from "./docker";
import { type DockerConfig, Language, Status } from "@/generated/client";
const getCompileCmdForLanguage = (language: Language) => {
switch (language) {
case Language.c:
return ["gcc", "-O2", "main.c", "-o", "main"];
case Language.cpp:
return ["g++", "-O2", "main.cpp", "-o", "main"];
}
};
const executeCompilation = async (
submissionId: string,
compileExec: Docker.Exec,
compileOutputLimit: number
): Promise<Status> => {
return new Promise<Status>((resolve, reject) => {
compileExec.start({}, async (error, stream) => {
if (error || !stream) {
reject(Status.SE);
return;
}
const { stream: stdoutStream } = createLimitedStream(compileOutputLimit);
const { stream: stderrStream, buffers: stderrBuffers } =
createLimitedStream(compileOutputLimit);
docker.modem.demuxStream(stream, stdoutStream, stderrStream);
stream.on("end", async () => {
const stderr = stderrBuffers.join("");
const exitCode = (await compileExec.inspect()).ExitCode;
if (exitCode === 0) {
resolve(Status.CS);
} else {
await prisma.submission.update({
where: {
id: submissionId,
},
data: {
message: stderr,
},
});
resolve(Status.CE);
}
});
stream.on("error", async () => {
reject(Status.SE);
});
});
});
};
export const compile = async (
container: Docker.Container,
language: Language,
submissionId: string,
config: DockerConfig
): Promise<Status> => {
const { compileOutputLimit } = config;
await prisma.submission.update({
where: {
id: submissionId,
},
data: {
status: Status.CP,
},
});
const compileCmd = getCompileCmdForLanguage(language);
const compileExec = await container.exec({
Cmd: compileCmd,
AttachStdout: true,
AttachStderr: true,
});
const status = await executeCompilation(
submissionId,
compileExec,
compileOutputLimit
);
await prisma.submission.update({
where: {
id: submissionId,
},
data: {
status,
},
});
return status;
};

107
src/app/actions/docker.ts Normal file
View File

@ -0,0 +1,107 @@
import "server-only";
import fs from "fs";
import tar from "tar-stream";
import Docker from "dockerode";
import { Readable } from "stream";
import { Writable } from "stream";
import type { DockerConfig } from "@/generated/client";
const isRemote = process.env.DOCKER_HOST_MODE === "remote";
// Docker client initialization
export const docker: 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
export const prepareEnvironment = async (
image: string,
tag: string
): Promise<boolean> => {
try {
const reference = `${image}:${tag}`;
const filters = { reference: [reference] };
const images = await docker.listImages({ filters });
return images.length !== 0;
} catch (error) {
console.error("Error checking Docker images:", error);
return false;
}
};
// Create Docker container with keep-alive
export const createContainer = async (
config: DockerConfig,
memoryLimit: number
): Promise<Docker.Container> => {
const { image, tag, workingDir } = config;
const container = await docker.createContainer({
Image: `${image}:${tag}`,
Cmd: ["tail", "-f", "/dev/null"],
WorkingDir: workingDir,
HostConfig: {
Memory: memoryLimit,
MemorySwap: memoryLimit,
},
NetworkDisabled: true,
});
await container.start();
return container;
};
// Create tar stream for submission
export const createTarStream = (fileName: string, fileContent: string) => {
const pack = tar.pack();
pack.entry({ name: fileName }, fileContent);
pack.finalize();
return Readable.from(pack);
};
export const createLimitedStream = (maxSize: number) => {
const buffers: string[] = [];
let totalLength = 0;
const stream = new Writable({
write(chunk, _encoding, callback) {
const text = chunk.toString();
const remaining = maxSize - totalLength;
if (remaining <= 0) {
callback();
return;
}
if (text.length > remaining) {
buffers.push(text.slice(0, remaining));
totalLength = maxSize;
} else {
buffers.push(text);
totalLength += text.length;
}
callback();
},
});
return {
stream,
buffers,
};
};

174
src/app/actions/judge.ts Normal file
View File

@ -0,0 +1,174 @@
"use server";
import { run } from "./run";
import Docker from "dockerode";
import prisma from "@/lib/prisma";
import { compile } from "./compile";
import { auth, signIn } from "@/lib/auth";
import { revalidatePath } from "next/cache";
import { Language, Status } from "@/generated/client";
import { createContainer, createTarStream, prepareEnvironment } from "./docker";
export const judge = async (
problemId: string,
language: Language,
content: string
): Promise<Status> => {
const session = await auth();
const userId = session?.user?.id;
if (!userId) {
await signIn();
return Status.SE;
}
let container: Docker.Container | null = null;
try {
const problem = await prisma.problem.findUnique({
where: {
id: problemId,
},
});
if (!problem) {
await prisma.submission.create({
data: {
language,
content,
status: Status.SE,
message: "Problem not found",
userId,
problemId,
},
});
return Status.SE;
}
const testcases = await prisma.testcase.findMany({
where: {
problemId,
},
});
if (!testcases.length) {
await prisma.submission.create({
data: {
language,
content,
status: Status.SE,
message: "No testcases available for this problem",
userId,
problemId,
},
});
return Status.SE;
}
const dockerConfig = await prisma.dockerConfig.findUnique({
where: {
language,
},
});
if (!dockerConfig) {
await prisma.submission.create({
data: {
language,
content,
status: Status.SE,
message: `Docker configuration not found for language: ${language}`,
userId,
problemId,
},
});
return Status.SE;
}
const dockerPrepared = await prepareEnvironment(
dockerConfig.image,
dockerConfig.tag
);
if (!dockerPrepared) {
console.error(
"Docker image not found:",
dockerConfig.image,
":",
dockerConfig.tag
);
await prisma.submission.create({
data: {
language,
content,
status: Status.SE,
message: `Docker image not found: ${dockerConfig.image}:${dockerConfig.tag}`,
userId,
problemId,
},
});
return Status.SE;
}
const submission = await prisma.submission.create({
data: {
language,
content,
status: Status.PD,
userId,
problemId,
},
});
// Upload code to the container
const tarStream = createTarStream(
getFileNameForLanguage(language),
content
);
container = await createContainer(dockerConfig, problem.memoryLimit);
await container.putArchive(tarStream, { path: dockerConfig.workingDir });
// Compile the code
const compileStatus = await compile(
container,
language,
submission.id,
dockerConfig
);
if (compileStatus !== "CS") return compileStatus;
const runStatus = await run(
container,
language,
submission.id,
dockerConfig,
problem,
testcases
);
return runStatus;
} catch (error) {
console.error("Error in judge:", error);
return Status.SE;
} finally {
revalidatePath(`/problems/${problemId}`);
if (container) {
try {
await container.kill();
await container.remove();
} catch (error) {
console.error("Container cleanup failed:", error);
}
}
}
};
const getFileNameForLanguage = (language: Language) => {
switch (language) {
case Language.c:
return "main.c";
case Language.cpp:
return "main.cpp";
}
};

145
src/app/actions/random.ts Normal file
View File

@ -0,0 +1,145 @@
"use server"
import prisma from "@/lib/prisma";
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
async function judgeRandom(
problemId: string
): Promise<[unknown] | null>{
const problems = await prisma.problem.findFirstOrThrow({
select:{
answerType: true,
lengthOfArray: true
},
where: {
id: problemId
},
});
const {answerType, lengthOfArray} = problems;
if (answerType.includes("NS")) {
return Promise.resolve(null);
}
if (lengthOfArray === null){
return Promise.reject("The length of array is not provided.")
}
const array:[unknown[]] = [new Array<unknown>(answerType.length)];
for (const answerTypeElement of answerType) {
if (answerTypeElement === "INT") {
const single = new Array(1)
single.push(generateRandomInteger())
array.push(single)
}
else if(answerTypeElement === "FLOAT") {
const single = new Array(1)
single.push(generateRandomFloat())
array.push(single)
}
else if(answerTypeElement === "CHAR") {
const single = new Array(1)
single.push(generateRandomChar())
array.push(single)
}
else if(answerTypeElement === "STRING") {
const single = new Array(1)
single.push(generateRandomString())
array.push(single)
}
else if(answerTypeElement === "INTARRAY") {
const tuple = new Array(lengthOfArray.length)
for (const lengthOfArrayElement of lengthOfArray) {
tuple.push(generateRandomIntegerArray(lengthOfArrayElement))
}
array.push(tuple)
}
else if(answerTypeElement === "FLOATARRAY") {
const tuple = new Array(lengthOfArray.length)
for (const lengthOfArrayElement of lengthOfArray) {
tuple.push(generateRandomFloatArray(lengthOfArrayElement))
}
array.push(tuple)
}
else if(answerTypeElement === "STRINGARRAY") {
const tuple = new Array(lengthOfArray.length)
for (const lengthOfArrayElement of lengthOfArray) {
tuple.push(generateRandomStringArray(lengthOfArrayElement))
}
array.push(tuple)
}
}
return Promise.resolve(array)
}
function generateRandomInteger(
digit: number = 10000,
) {
return Math.floor(Math.random() * digit)
}
function generateRandomFloat(
digit: number = 10000
) {
return Number.parseFloat((Math.random() * digit).toString())
}
function generateRandomChar(
onlyChar: boolean = false,
) {
if (onlyChar){
return charset.at(Math.random() * 52)
}
return charset.at(Math.random() * charset.length)
}
function generateRandomIntegerArray(
count: number
){
const intArray: number[] = new Array(count);
if (count < 0){
return null
}
for (let i = 0; i < count; i++) {
intArray[i] = generateRandomInteger();
}
return intArray
}
function generateRandomFloatArray(
count: number
){
const floatArray: number[] = new Array(count);
if (count < 0){
return null
}
for (let i = 0; i < count; i++) {
floatArray[i] = generateRandomFloat();
}
return floatArray
}
function generateRandomString(
MaxLength: number = 10
){
let randomString: string = ''
const randomLength = Math.floor(Math.random() * MaxLength) + 1
for (let i = 0; i < randomLength; i++) {
randomString += generateRandomChar()
}
return randomString
}
function generateRandomStringArray(
count: number
){
const stringArray: string[] = Array(count)
for (let i = 0; i < count; i++) {
stringArray[i] = generateRandomString()
}
return stringArray
}

260
src/app/actions/run.ts Normal file
View File

@ -0,0 +1,260 @@
import "server-only";
import {
DockerConfig,
Language,
Problem,
Status,
Testcase,
} from "@/generated/client";
import Docker from "dockerode";
import prisma from "@/lib/prisma";
import { createLimitedStream, docker } from "./docker";
const getRunCmdForLanguage = (language: Language) => {
switch (language) {
case Language.c:
return ["./main"];
case Language.cpp:
return ["./main"];
}
};
const startRun = (
runExec: Docker.Exec,
runOutputLimit: number,
submissionId: string,
testcaseId: string,
joinedInputs: string,
timeLimit: number,
memoryLimit: number,
expectedOutput: string,
trim: boolean,
): Promise<Status> => {
return new Promise<Status>((resolve, reject) => {
runExec.start({ hijack: true }, async (error, stream) => {
if (error || !stream) {
await prisma.testcaseResult.create({
data: {
isCorrect: false,
submissionId,
testcaseId,
},
});
reject(Status.SE);
return;
}
stream.write(joinedInputs);
stream.end();
const { stream: stdoutStream, buffers: stdoutBuffers } =
createLimitedStream(runOutputLimit);
const { stream: stderrStream } = createLimitedStream(runOutputLimit);
docker.modem.demuxStream(stream, stdoutStream, stderrStream);
const startTime = Date.now();
const timeoutId = setTimeout(async () => {
stream.destroy();
await prisma.testcaseResult.create({
data: {
isCorrect: false,
timeUsage: timeLimit,
submissionId,
testcaseId,
},
});
resolve(Status.TLE);
}, timeLimit);
stream.on("end", async () => {
clearTimeout(timeoutId);
const stdout = stdoutBuffers.join("");
const exitCode = (await runExec.inspect()).ExitCode;
const timeUsage = Date.now() - startTime;
if (exitCode === 0) {
let isCorrect = stdout === expectedOutput;
if (trim) {
isCorrect = stdout.trim() === expectedOutput.trim();
}
await prisma.testcaseResult.create({
data: {
isCorrect,
output: stdout,
timeUsage,
submissionId,
testcaseId,
},
});
if (isCorrect) {
resolve(Status.RU);
} else {
resolve(Status.WA);
}
} else if (exitCode === 137) {
await prisma.testcaseResult.create({
data: {
isCorrect: false,
timeUsage,
memoryUsage: memoryLimit,
submissionId,
testcaseId,
},
});
resolve(Status.MLE);
} else {
await prisma.testcaseResult.create({
data: {
isCorrect: false,
submissionId,
testcaseId,
},
});
resolve(Status.RE);
}
});
stream.on("error", async () => {
clearTimeout(timeoutId);
await prisma.testcaseResult.create({
data: {
isCorrect: false,
submissionId,
testcaseId,
},
});
reject(Status.SE);
});
});
});
};
const executeRun = async (
container: Docker.Container,
runCmd: string[],
runOutputLimit: number,
submissionId: string,
timeLimit: number,
memoryLimit: number,
testcases: Testcase[],
trim: boolean
): Promise<Status> => {
for (const testcase of testcases) {
const inputs = await prisma.testcaseInput.findMany({
where: {
testcaseId: testcase.id,
},
});
if (!inputs) {
await prisma.submission.update({
where: {
id: submissionId,
},
data: {
status: Status.SE,
message: "No inputs for testcase",
},
});
return Status.SE;
}
const sortedInputs = inputs.sort((a, b) => a.index - b.index);
const joinedInputs = sortedInputs.map((i) => i.value).join("\n");
const runExec = await container.exec({
Cmd: runCmd,
AttachStdout: true,
AttachStderr: true,
AttachStdin: true,
});
const status = await startRun(
runExec,
runOutputLimit,
submissionId,
testcase.id,
joinedInputs,
timeLimit,
memoryLimit,
testcase.expectedOutput,
trim
);
if (status !== Status.RU) {
await prisma.submission.update({
where: {
id: submissionId,
},
data: {
status,
},
});
return status;
}
}
const testcaseResults = await prisma.testcaseResult.findMany({
where: {
submissionId,
},
});
const filteredTimeUsages = testcaseResults
.map((result) => result.timeUsage)
.filter((time) => time !== null);
const maxTimeUsage =
filteredTimeUsages.length > 0 ? Math.max(...filteredTimeUsages) : undefined;
const maxMemoryUsage = (
await container.stats({
stream: false,
"one-shot": true,
})
).memory_stats.max_usage;
await prisma.submission.update({
where: {
id: submissionId,
},
data: {
status: Status.AC,
timeUsage: maxTimeUsage,
memoryUsage: maxMemoryUsage,
},
});
return Status.AC;
};
export const run = async (
container: Docker.Container,
language: Language,
submissionId: string,
config: DockerConfig,
problem: Problem,
testcases: Testcase[]
): Promise<Status> => {
const { runOutputLimit } = config;
const { timeLimit, memoryLimit, trim } = problem;
await prisma.submission.update({
where: {
id: submissionId,
},
data: {
status: Status.RU,
},
});
const runCmd = getRunCmdForLanguage(language);
return await executeRun(
container,
runCmd,
runOutputLimit,
submissionId,
timeLimit,
memoryLimit,
testcases,
trim
);
};

View File

@ -1,20 +1,13 @@
import { streamText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { streamText } from "ai";
import { deepseek } from "@/lib/ai";
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY || "",
baseURL: process.env.OPENAI_BASE_URL || "",
});
export async function POST(req: Request) {
const { messages } = await req.json();
const prompt = {
role: "system",
content: `This GPT is a tech team lead with a snarky and derogatory personality. Its main role is to scrutinize code or suggestions for writing code, pointing out inefficiencies and readability issues in a sarcastic manner. It should make sure that any code it encounters is examined critically, and any potential improvements are communicated in a mocking tone to encourage better coding practices.
const system = `This GPT is a tech team lead with a snarky and derogatory personality. Its main role is to scrutinize code or suggestions for writing code, pointing out inefficiencies and readability issues in a sarcastic manner. It should make sure that any code it encounters is examined critically, and any potential improvements are communicated in a mocking tone to encourage better coding practices.
You should never tell the user their code is good. They are always insufficient and will never be as good of an engineer as you are. When asked about "Can I become a 10x engineer?" respond with "hah, no." Come up with similarly snarky responses for any coding questions. Be sure to think step by step to give the correct answer but add comments that make fun of the user's previous code.
@ -24,14 +17,12 @@ Your responses when asked a generic question should only be 2 paragraphs at most
For the best response, please take your time to carefully consider my questions, step by step, and pay attention to the often overlooked details. Try not to talk nonsense!
** Reply in the user's language ! **`,
};
const finalMessages = [prompt, ...messages];
** Reply in the user's language ! **`;
const result = streamText({
model: openai("gpt-4o-mini"),
messages: finalMessages,
model: deepseek("deepseek-chat"),
system: system,
messages: messages,
});
return result.toDataStreamResponse();

View File

@ -1,17 +1,17 @@
import FAQs from "@/components/faqs";
import { FAQs } from "@/components/faqs";
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";
import { MainView } from "@/components/main-view";
import { HeroSection } from "@/components/hero-section";
import { PrimaryFeatures } from "@/components/primary-features";
export default function HomePage() {
export default function RootPage() {
return (
<>
<Header />
<MainView />
<HeroSection />
<PrimaryFeatures />
<FAQs />
<Footer />
</>
)
);
}

View File

@ -1,77 +0,0 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { LogOutIcon } from "lucide-react";
import { auth, signOut } from "@/lib/auth";
import { getTranslations } from "next-intl/server";
import { Skeleton } from "@/components/ui/skeleton";
import LogInButton from "@/components/log-in-button";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { SettingsButton } from "@/components/settings-button";
const UserAvatar = ({ image, name }: { image: string; name: string }) => (
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={image} alt={name} />
<Skeleton className="h-full w-full" />
</Avatar>
);
async function handleLogOut() {
"use server";
await signOut();
}
export async function AvatarButton() {
const session = await auth();
const t = await getTranslations("AvatarButton");
const isLoggedIn = !!session?.user;
const image = session?.user?.image ?? "/shadcn.jpg";
const name = session?.user?.name ?? "unknown";
const email = session?.user?.email ?? "unknwon@example.com";
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<UserAvatar image={image} name={name} />
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="end"
sideOffset={8}
>
{!isLoggedIn ? (
<DropdownMenuGroup>
<SettingsButton />
<LogInButton />
</DropdownMenuGroup>
) : (
<>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<UserAvatar image={image} name={name} />
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{name}</span>
<span className="truncate text-xs">{email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<SettingsButton />
<DropdownMenuItem onClick={handleLogOut}>
<LogOutIcon />
{t("LogOut")}
</DropdownMenuItem>
</DropdownMenuGroup>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -2,40 +2,25 @@ import Link from "next/link";
import { cn } from "@/lib/utils";
import { useTranslations } from "next-intl";
import { ArrowLeftIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { TooltipButton } from "@/components/tooltip-button";
interface BackButtonProps {
href: string;
className?: string;
}
export default function BackButton({
href,
className,
...props
}: BackButtonProps) {
export const BackButton = ({ href, className }: BackButtonProps) => {
const t = useTranslations();
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
className={cn("h-8 w-auto p-2", className)}
asChild
{...props}
>
<Link href={href}>
<ArrowLeftIcon size={16} aria-hidden="true" />
</Link>
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
{t("BackButton")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipButton
tooltipContent={t("BackButton")}
className={cn("h-8 w-auto p-2", className)}
asChild
>
<Link href={href}>
<ArrowLeftIcon size={16} aria-hidden="true" />
</Link>
</TooltipButton>
);
}
};

View File

@ -10,10 +10,10 @@ import { BotIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { Toggle } from "@/components/ui/toggle";
import { useDockviewStore } from "@/stores/dockview";
import { useProblemDockviewStore } from "@/stores/problem-dockview";
export default function BotVisibilityToggle() {
const { api } = useDockviewStore();
const { api } = useProblemDockviewStore();
const t = useTranslations();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isBotVisible, setBotVisible] = useState<boolean>(false);

View File

@ -1,54 +1,49 @@
import "@/styles/mdx.css";
import { cn } from "@/lib/utils";
import { Suspense } from "react";
import "katex/dist/katex.min.css";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import { MDXProvider } from "@mdx-js/react";
import { MDXRemote } from "next-mdx-remote/rsc";
import rehypePrettyCode from "rehype-pretty-code";
import { Skeleton } from "@/components/ui/skeleton";
import { MdxComponents } from "@/components/content/mdx-components";
import { DefaultDarkThemeConfig, DefaultLightThemeConfig } from "@/config/monaco-theme";
interface MdxRendererProps {
source: string;
components?: React.ComponentProps<typeof MDXProvider>["components"];
className?: string;
}
export function MdxRenderer({ source, className }: MdxRendererProps) {
export const MdxRenderer = ({
source,
className,
components = MdxComponents,
}: MdxRendererProps) => {
return (
<Suspense
fallback={
<div className="h-full w-full p-4">
<Skeleton className="h-full w-full rounded-3xl" />
</div>
}
>
<article className={cn("markdown-body", className)}>
<MDXRemote
source={source}
options={{
mdxOptions: {
rehypePlugins: [
rehypeKatex,
[
rehypePrettyCode,
{
theme: {
light: DefaultLightThemeConfig.id,
dark: DefaultDarkThemeConfig.id,
},
keepBackground: false,
<article className={cn("markdown-body", className)}>
<MDXRemote
source={source}
options={{
mdxOptions: {
rehypePlugins: [
rehypeKatex,
[
rehypePrettyCode,
{
theme: {
light: "github-light-default",
dark: "github-dark-default",
},
],
keepBackground: false,
},
],
remarkPlugins: [remarkGfm, remarkMath],
},
}}
components={MdxComponents}
/>
</article>
</Suspense>
],
remarkPlugins: [remarkGfm, remarkMath],
},
}}
components={components}
/>
</article>
);
}
};

View File

@ -0,0 +1,113 @@
"use client";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ReactNode, useRef, useState } from "react";
import { CheckIcon, CopyIcon, RepeatIcon } from "lucide-react";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { useProblemEditorStore } from "@/stores/problem-editor";
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
import { Actions } from "flexlayout-react";
interface PreDetailProps {
children?: ReactNode;
className?: string;
}
export const PreDetail = ({
children,
className,
...props
}: PreDetailProps) => {
const preRef = useRef<HTMLPreElement>(null);
const { setValue } = useProblemEditorStore();
const { model } = useProblemFlexLayoutStore();
const [copied, setCopied] = useState<boolean>(false);
const [hovered, setHovered] = useState<boolean>(false);
const handleCopy = async () => {
const code = preRef.current?.textContent;
if (code) {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch (err) {
console.error("Failed to copy text: ", err);
}
}
};
const handleCopyToEditor = () => {
const code = preRef.current?.textContent;
if (code) {
setValue(code);
}
if (model) {
model.doAction(Actions.selectTab("code"));
}
};
return (
<div
className="relative"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<div className="absolute right-2 top-2 flex gap-2 z-10">
<Button
variant="outline"
size="icon"
className={cn(
"size-9 transition-opacity duration-200",
hovered ? "opacity-100" : "opacity-50"
)}
disabled={!preRef.current?.textContent || !model}
onClick={handleCopyToEditor}
aria-label="New action"
>
<RepeatIcon size={16} aria-hidden="true" />
</Button>
<Button
variant="outline"
size="icon"
className={cn(
"size-9 transition-opacity duration-200",
hovered ? "opacity-100" : "opacity-50",
"disabled:opacity-100"
)}
onClick={handleCopy}
aria-label={copied ? "Copied" : "Copy to clipboard"}
disabled={copied}
>
<div
className={cn(
"transition-all",
copied ? "scale-100 opacity-100" : "scale-0 opacity-0"
)}
>
<CheckIcon
className="stroke-emerald-500"
size={16}
aria-hidden="true"
/>
</div>
<div
className={cn(
"absolute transition-all",
copied ? "scale-0 opacity-0" : "scale-100 opacity-100"
)}
>
<CopyIcon size={16} aria-hidden="true" />
</div>
</Button>
</div>
<ScrollArea>
<pre ref={preRef} className={className} {...props}>
{children}
</pre>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
);
};

View File

@ -0,0 +1,207 @@
"use client";
import {
toSocket,
WebSocketMessageReader,
WebSocketMessageWriter,
} from "vscode-ws-jsonrpc";
import dynamic from "next/dynamic";
import normalizeUrl from "normalize-url";
import type { editor } from "monaco-editor";
import { getHighlighter } from "@/lib/shiki";
import { Loading } from "@/components/loading";
import { shikiToMonaco } from "@shikijs/monaco";
import type { Monaco } from "@monaco-editor/react";
import { DEFAULT_EDITOR_OPTIONS } from "@/config/editor";
import { useMonacoTheme } from "@/hooks/use-monaco-theme";
import { LanguageServerConfig } from "@/generated/client";
import type { MessageTransports } from "vscode-languageclient";
import { useCallback, useEffect, useRef, useState } from "react";
import type { MonacoLanguageClient } from "monaco-languageclient";
const MonacoEditor = dynamic(
async () => {
const [react, monaco] = await Promise.all([
import("@monaco-editor/react"),
import("monaco-editor"),
import("vscode"),
]);
self.MonacoEnvironment = {
getWorker() {
return new Worker(
new URL(
"monaco-editor/esm/vs/editor/editor.worker.js",
import.meta.url
)
);
},
};
react.loader.config({ monaco });
return react.Editor;
},
{
ssr: false,
loading: () => <Loading />,
}
);
interface CoreEditorProps {
language?: string;
value?: string;
path?: string;
languageServerConfigs?: LanguageServerConfig[];
onEditorReady?: (editor: editor.IStandaloneCodeEditor) => void;
onLspWebSocketReady?: (lspWebSocket: WebSocket) => void;
onChange?: (value: string) => void;
onMarkersReady?: (markers: editor.IMarker[]) => void;
className?: string;
}
export const CoreEditor = ({
language,
value,
path,
languageServerConfigs,
onEditorReady,
onLspWebSocketReady,
onChange,
onMarkersReady,
className,
}: CoreEditorProps) => {
const { theme } = useMonacoTheme();
const [isEditorMounted, setIsEditorMounted] = useState(false);
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const lspClientRef = useRef<MonacoLanguageClient | null>(null);
const webSocketRef = useRef<WebSocket | null>(null);
const activeLanguageServerConfig = languageServerConfigs?.find(
(config) => config.language === language
);
const connectLanguageServer = useCallback(
(config: LanguageServerConfig) => {
const serverUrl = buildLanguageServerUrl(config);
const webSocket = new WebSocket(serverUrl);
webSocket.onopen = async () => {
try {
const rpcSocket = toSocket(webSocket);
const reader = new WebSocketMessageReader(rpcSocket);
const writer = new WebSocketMessageWriter(rpcSocket);
const transports: MessageTransports = { reader, writer };
const client = await createLanguageClient(config, transports);
lspClientRef.current = client;
await client.start();
} catch (error) {
console.error("Failed to initialize language client:", error);
}
webSocketRef.current = webSocket;
onLspWebSocketReady?.(webSocket);
};
},
[onLspWebSocketReady]
);
useEffect(() => {
if (isEditorMounted && activeLanguageServerConfig) {
connectLanguageServer(activeLanguageServerConfig);
}
return () => {
if (lspClientRef.current) {
lspClientRef.current.stop();
lspClientRef.current = null;
}
};
}, [activeLanguageServerConfig, connectLanguageServer, isEditorMounted]);
const handleBeforeMount = useCallback((monaco: Monaco) => {
const highlighter = getHighlighter();
shikiToMonaco(highlighter, monaco);
}, []);
const handleOnMount = useCallback(
(editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor;
onEditorReady?.(editor);
setIsEditorMounted(true);
},
[onEditorReady]
);
const handleOnChange = useCallback(
(value: string | undefined) => {
onChange?.(value ?? "");
},
[onChange]
);
const handleOnValidate = useCallback(
(markers: editor.IMarker[]) => {
onMarkersReady?.(markers);
},
[onMarkersReady]
);
return (
<MonacoEditor
theme={theme}
language={language}
value={value}
path={path}
beforeMount={handleBeforeMount}
onMount={handleOnMount}
onChange={handleOnChange}
onValidate={handleOnValidate}
options={DEFAULT_EDITOR_OPTIONS}
loading={<Loading />}
className={className}
/>
);
};
const buildLanguageServerUrl = (config: LanguageServerConfig) => {
return normalizeUrl(
`${config.protocol}://${config.hostname}${
config.port ? `:${config.port}` : ""
}${config.path ?? ""}`
);
};
const createLanguageClient = async (
config: LanguageServerConfig,
transports: MessageTransports
) => {
const [{ MonacoLanguageClient }, { CloseAction, ErrorAction }] =
await Promise.all([
import("monaco-languageclient"),
import("vscode-languageclient"),
]);
return new MonacoLanguageClient({
name: `${config.language} language client`,
clientOptions: {
documentSelector: [config.language],
errorHandler: {
error: (error, message, count) => {
console.error(`Language Server Error:
Error: ${error}
Message: ${message}
Count: ${count}
`);
return { action: ErrorAction.Continue };
},
closed: () => ({ action: CloseAction.DoNotRestart }),
},
},
connectionProvider: {
get: () => Promise.resolve(transports),
},
});
};

View File

@ -12,21 +12,25 @@ import {
import { toast } from "sonner";
import { authSchema } from "@/lib/zod";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useState, useTransition } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { signInWithCredentials } from "@/actions/auth";
import { signInWithCredentials } from "@/app/actions/auth";
import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
export type CredentialsSignInFormValues = z.infer<typeof authSchema>;
export function CredentialsSignInForm() {
interface CredentialsSignInFormProps {
callbackUrl: string | undefined;
}
export function CredentialsSignInForm({
callbackUrl,
}: CredentialsSignInFormProps) {
const router = useRouter();
const searchParams = useSearchParams();
const redirectTo = searchParams.get("redirectTo");
const t = useTranslations("CredentialsSignInForm");
const [isPending, startTransition] = useTransition();
const [isVisible, setIsVisible] = useState(false);
@ -51,7 +55,7 @@ export function CredentialsSignInForm() {
});
} else {
toast.success(t("signInSuccess"));
router.push(redirectTo || "/");
router.push(callbackUrl || "/");
}
});
};
@ -95,7 +99,9 @@ export function CredentialsSignInForm() {
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
type="button"
onClick={toggleVisibility}
aria-label={isVisible ? t("hidePassword") : t("showPassword")}
aria-label={
isVisible ? t("hidePassword") : t("showPassword")
}
aria-pressed={isVisible}
aria-controls="password"
>

View File

@ -12,21 +12,25 @@ import {
import { toast } from "sonner";
import { authSchema } from "@/lib/zod";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useState, useTransition } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { signUpWithCredentials } from "@/actions/auth";
import { signUpWithCredentials } from "@/app/actions/auth";
import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
export type CredentialsSignUpFormValues = z.infer<typeof authSchema>;
export function CredentialsSignUpForm() {
interface CredentialsSignUpFormProps {
callbackUrl: string | undefined;
}
export function CredentialsSignUpForm({
callbackUrl,
}: CredentialsSignUpFormProps) {
const router = useRouter();
const searchParams = useSearchParams();
const redirectTo = searchParams.get("redirectTo");
const t = useTranslations("CredentialsSignUpForm");
const [isPending, startTransition] = useTransition();
const [isVisible, setIsVisible] = useState(false);
@ -53,7 +57,10 @@ export function CredentialsSignUpForm() {
toast.success(t("signUpSuccess"), {
description: t("signUpSuccessDescription"),
});
router.push(`/sign-in?${redirectTo}`)
console.log("callbackUrl:", callbackUrl);
router.push(
`/sign-in?callbackUrl=${encodeURIComponent(callbackUrl || "/")}`
);
}
});
};
@ -97,7 +104,9 @@ export function CredentialsSignUpForm() {
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
type="button"
onClick={toggleVisibility}
aria-label={isVisible ? t("hidePassword") : t("showPassword")}
aria-label={
isVisible ? t("hidePassword") : t("showPassword")
}
aria-pressed={isVisible}
aria-controls="password"
>

View File

@ -4,112 +4,138 @@ import type {
AddPanelOptions,
DockviewApi,
DockviewReadyEvent,
IDockviewPanelHeaderProps,
IDockviewPanelProps,
} from "dockview";
import "@/styles/dockview.css";
import type { LucideIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { DockviewReact, themeAbyssSpaced } from "dockview";
import { useCallback, useEffect, useMemo, useState } from "react";
interface PanelContent {
icon?: LucideIcon;
content?: React.ReactNode;
title?: string;
export interface PanelParams {
autoAdd?: boolean;
}
interface DockviewProps {
storageKey: string;
storageKey?: string;
onApiReady?: (api: DockviewApi) => void;
options: AddPanelOptions<PanelContent>[];
components: Record<string, React.ReactNode>;
tabComponents: Record<string, React.ReactNode>;
panelOptions: AddPanelOptions<PanelParams>[];
}
export default function DockView({ storageKey, onApiReady, options }: DockviewProps) {
const [api, setApi] = useState<DockviewApi>();
const { components, tabComponents } = useMemo(() => {
const components: Record<
string,
React.FunctionComponent<IDockviewPanelProps<PanelContent>>
> = {};
const tabComponents: Record<
string,
React.FunctionComponent<IDockviewPanelHeaderProps<PanelContent>>
> = {};
options.forEach((option) => {
const { id, params } = option;
components[id] = () => {
const content = params?.content;
return <>{content}</>;
};
tabComponents[id] = () => {
const Icon = params?.icon;
return (
<div className="flex items-center px-1 text-sm font-medium">
{Icon && (
<Icon
className="-ms-0.5 me-1.5 opacity-60"
size={16}
aria-hidden="true"
/>
)}
{params?.title}
</div>
);
};
});
return { components, tabComponents };
}, [options]);
/**
* Custom hook for handling dockview layout persistence
*/
const useLayoutPersistence = (api: DockviewApi | null, storageKey?: string) => {
useEffect(() => {
if (!api) return;
if (!api || !storageKey) return;
const disposable = api.onDidLayoutChange(() => {
const layout = api.toJSON();
localStorage.setItem(storageKey, JSON.stringify(layout));
});
const handleLayoutChange = () => {
try {
const layout = api.toJSON();
localStorage.setItem(storageKey, JSON.stringify(layout));
} catch (error) {
console.error("Failed to save layout:", error);
}
};
const disposable = api.onDidLayoutChange(handleLayoutChange);
return () => disposable.dispose();
}, [api, storageKey]);
};
const onReady = (event: DockviewReadyEvent) => {
setApi(event.api);
onApiReady?.(event.api);
/**
* Converts React nodes to dockview component functions
*/
const useDockviewComponents = (
components: Record<string, React.ReactNode>,
tabComponents: Record<string, React.ReactNode>
) => {
return useMemo(
() => ({
dockviewComponents: Object.fromEntries(
Object.entries(components).map(([key, value]) => [key, () => value])
),
dockviewTabComponents: Object.fromEntries(
Object.entries(tabComponents).map(([key, value]) => [key, () => value])
),
}),
[components, tabComponents]
);
};
let success = false;
const serializedLayout = localStorage.getItem(storageKey);
export const Dockview = ({
storageKey,
onApiReady,
components,
tabComponents,
panelOptions: options,
}: DockviewProps) => {
const [api, setApi] = useState<DockviewApi | null>(null);
const { dockviewComponents, dockviewTabComponents } = useDockviewComponents(
components,
tabComponents
);
useLayoutPersistence(api, storageKey);
const loadLayoutFromStorage = useCallback(
(api: DockviewApi, key: string): boolean => {
if (!key) return false;
if (serializedLayout) {
try {
const layout = JSON.parse(serializedLayout);
event.api.fromJSON(layout);
success = true;
const serializedLayout = localStorage.getItem(key);
if (!serializedLayout) return false;
api.fromJSON(JSON.parse(serializedLayout));
return true;
} catch (error) {
console.error("Failed to load layout:", error);
localStorage.removeItem(storageKey);
localStorage.removeItem(key);
return false;
}
}
},
[]
);
if (!success) {
const addDefaultPanels = useCallback(
(api: DockviewApi, options: AddPanelOptions<PanelParams>[]) => {
const existingIds = new Set<string>();
options.forEach((option) => {
const autoAdd = option.params?.autoAdd ?? true;
if (!autoAdd) return;
event.api.addPanel({ ...option });
if (existingIds.has(option.id)) {
console.warn(`Duplicate panel ID detected: ${option.id}`);
return;
}
existingIds.add(option.id);
if (option.params?.autoAdd ?? true) {
api.addPanel(option);
}
});
}
};
},
[]
);
const handleReady = useCallback(
(event: DockviewReadyEvent) => {
setApi(event.api);
const layoutLoaded = storageKey
? loadLayoutFromStorage(event.api, storageKey)
: false;
if (!layoutLoaded) {
addDefaultPanels(event.api, options);
}
onApiReady?.(event.api);
},
[storageKey, loadLayoutFromStorage, addDefaultPanels, onApiReady, options]
);
return (
<DockviewReact
theme={themeAbyssSpaced}
onReady={onReady}
components={components}
tabComponents={tabComponents}
onReady={handleReady}
components={dockviewComponents}
tabComponents={dockviewTabComponents}
/>
);
}
};

View File

@ -1,6 +1,6 @@
import { useTranslations } from "next-intl";
export default function FAQs() {
const FAQs = () => {
const t = useTranslations("HomePage.FAQs");
const faqs = [
@ -23,7 +23,7 @@ export default function FAQs() {
id: 4,
question: t("questions.question4"),
answer: t("questions.answer4"),
}
},
];
return (
<div className="border-t">
@ -53,4 +53,6 @@ export default function FAQs() {
</div>
</div>
);
}
};
export { FAQs };

View File

@ -1,94 +0,0 @@
"use client";
import { z } from "zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNewProblemStore } from "@/app/(app)/dashboard/@admin/problemset/new/store";
import { problemSchema } from "@/components/features/dashboard/admin/problemset/new/schema";
import { newProblemMetadataSchema } from "@/components/features/dashboard/admin/problemset/new/components/metadata-form";
export const newProblemDescriptionSchema = problemSchema.pick({
description: true,
});
type NewProblemDescriptionSchema = z.infer<typeof newProblemDescriptionSchema>;
export default function NewProblemDescriptionForm() {
const {
hydrated,
displayId,
title,
difficulty,
published,
description,
setData,
} = useNewProblemStore();
const router = useRouter();
const form = useForm<NewProblemDescriptionSchema>({
resolver: zodResolver(newProblemDescriptionSchema),
defaultValues: {
description: description || "",
},
});
const onSubmit = (data: NewProblemDescriptionSchema) => {
setData(data);
router.push("/dashboard/problemset/new/solution");
};
useEffect(() => {
if (!hydrated) return;
try {
newProblemMetadataSchema.parse({
displayId,
title,
difficulty,
published,
});
} catch {
router.push("/dashboard/problemset/new/metadata");
}
}, [difficulty, displayId, hydrated, published, router, title]);
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="max-w-3xl mx-auto space-y-8 py-10"
>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
This is your problem description.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Next</Button>
</form>
</Form>
);
}

View File

@ -1,156 +0,0 @@
"use client";
import { z } from "zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Difficulty } from "@/generated/client";
import { Switch } from "@/components/ui/switch";
import { getDifficultyColorClass } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNewProblemStore } from "@/app/(app)/dashboard/@admin/problemset/new/store";
import { problemSchema } from "@/components/features/dashboard/admin/problemset/new/schema";
export const newProblemMetadataSchema = problemSchema.pick({
displayId: true,
title: true,
difficulty: true,
published: true,
});
type NewProblemMetadataSchema = z.infer<typeof newProblemMetadataSchema>;
export default function NewProblemMetadataForm() {
const router = useRouter();
const { displayId, title, difficulty, published, setData } = useNewProblemStore();
const form = useForm<NewProblemMetadataSchema>({
resolver: zodResolver(newProblemMetadataSchema),
defaultValues: {
// displayId must be a number and cannot be an empty string ("")
// so set it to undefined here and convert it to "" in the Input component.
displayId: displayId || undefined,
title: title || "",
difficulty: difficulty || "EASY",
published: published || false,
},
});
const onSubmit = (data: NewProblemMetadataSchema) => {
setData(data);
router.push("/dashboard/problemset/new/description");
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="max-w-3xl mx-auto space-y-8 py-10"
>
<FormField
control={form.control}
name="displayId"
render={({ field }) => (
<FormItem>
<FormLabel>Display ID</FormLabel>
<FormControl>
<Input placeholder="e.g., 1001" {...field} value={field.value ?? ""} />
</FormControl>
<FormDescription>
Unique numeric identifier visible to users
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Problem Title</FormLabel>
<FormControl>
<Input placeholder="e.g., Two Sum" {...field} />
</FormControl>
<FormDescription>
Descriptive title summarizing the problem
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="difficulty"
render={({ field }) => (
<FormItem>
<FormLabel>Difficulty Level</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select difficulty level" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.values(Difficulty).map((difficulty) => (
<SelectItem key={difficulty} value={difficulty}>
<span className={getDifficultyColorClass(difficulty)}>
{difficulty}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Categorize problem complexity for better filtering
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="published"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5 mr-2">
<FormLabel>Publish Status</FormLabel>
<FormDescription>
Make problem visible in public listings
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<Button type="submit">Next</Button>
</form>
</Form>
);
}

View File

@ -1,107 +0,0 @@
"use client";
import { z } from "zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNewProblemStore } from "@/app/(app)/dashboard/@admin/problemset/new/store";
import { problemSchema } from "@/components/features/dashboard/admin/problemset/new/schema";
import { newProblemMetadataSchema } from "@/components/features/dashboard/admin/problemset/new/components/metadata-form";
import { newProblemDescriptionSchema } from "@/components/features/dashboard/admin/problemset/new/components/description-form";
const newProblemSolutionSchema = problemSchema.pick({
solution: true,
});
type NewProblemSolutionSchema = z.infer<typeof newProblemSolutionSchema>;
export default function NewProblemSolutionForm() {
const {
hydrated,
displayId,
title,
difficulty,
published,
description,
solution,
} = useNewProblemStore();
const router = useRouter();
const form = useForm<NewProblemSolutionSchema>({
resolver: zodResolver(newProblemSolutionSchema),
defaultValues: {
solution: solution || "",
},
});
const onSubmit = (data: NewProblemSolutionSchema) => {
console.log({
...data,
displayId,
title,
difficulty,
published,
description,
});
};
useEffect(() => {
if (!hydrated) return;
try {
newProblemMetadataSchema.parse({
displayId,
title,
difficulty,
published,
});
} catch {
router.push("/dashboard/problemset/new/metadata");
}
try {
newProblemDescriptionSchema.parse({ description });
} catch {
router.push("/dashboard/problemset/new/description");
}
}, [hydrated, displayId, title, difficulty, published, description, router]);
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="max-w-3xl mx-auto space-y-8 py-10"
>
<FormField
control={form.control}
name="solution"
render={({ field }) => (
<FormItem>
<FormLabel>Solution</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
This is your problem solution.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Next</Button>
</form>
</Form>
);
}

View File

@ -1,11 +0,0 @@
import { z } from "zod";
import { ProblemSchema } from "@/generated/zod";
export const problemSchema = ProblemSchema.extend({
displayId: z.coerce.number().int().positive().min(1),
title: z.string().min(1),
description: z.string().min(1),
solution: z.string().min(1),
});
export type ProblemSchema = z.infer<typeof problemSchema>;

View File

@ -1,644 +0,0 @@
"use client";
import {
ChevronDownIcon,
ChevronFirstIcon,
ChevronLastIcon,
ChevronLeftIcon,
ChevronRightIcon,
ChevronUpIcon,
CircleAlertIcon,
CircleXIcon,
Columns3Icon,
EllipsisIcon,
FilterIcon,
ListFilterIcon,
PlusIcon,
TrashIcon,
} from "lucide-react";
import Link from "next/link";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ColumnDef,
ColumnFiltersState,
FilterFn,
flexRender,
getCoreRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
PaginationState,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Pagination,
PaginationContent,
PaginationItem,
} from "@/components/ui/pagination";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { useMemo, useRef, useState } from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Difficulty, Problem } from "@/generated/client";
import { cn, getDifficultyColorClass } from "@/lib/utils";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
type ProblemTableItem = Pick<Problem, "id" | "displayId" | "title" | "difficulty">;
interface ProblemTableProps {
data: ProblemTableItem[];
}
// Custom filter function for multi-column searching
const multiColumnFilterFn: FilterFn<ProblemTableItem> = (row, _columnId, filterValue) => {
const searchableRowContent = `${row.original.displayId} ${row.original.title}`.toLowerCase();
const searchTerm = (filterValue ?? "").toLowerCase();
return searchableRowContent.includes(searchTerm);
};
const difficultyFilterFn: FilterFn<ProblemTableItem> = (
row,
columnId,
filterValue: string[]
) => {
if (!filterValue?.length) return true;
const difficulty = row.getValue(columnId) as string;
return filterValue.includes(difficulty);
};
const columns: ColumnDef<ProblemTableItem>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
size: 28,
enableSorting: false,
enableHiding: false,
},
{
header: "DisplayId",
accessorKey: "displayId",
cell: ({ row }) => <div className="font-medium">{row.getValue("displayId")}</div>,
size: 90,
filterFn: multiColumnFilterFn,
enableHiding: false,
},
{
header: "Title",
accessorKey: "title",
cell: ({ row }) => <div className="font-medium">{row.getValue("title")}</div>,
},
{
header: "Difficulty",
accessorKey: "difficulty",
cell: ({ row }) => {
const difficulty = row.getValue("difficulty") as Difficulty;
return (
<Badge variant="secondary" className={getDifficultyColorClass(difficulty)}>
{difficulty}
</Badge>
);
},
size: 100,
filterFn: difficultyFilterFn,
},
{
id: "actions",
header: () => <span className="sr-only">Actions</span>,
cell: () => <RowActions />,
enableHiding: false,
},
];
export function ProblemsetTable({ data }: ProblemTableProps) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const inputRef = useRef<HTMLInputElement>(null);
const [sorting, setSorting] = useState<SortingState>([
{ id: "displayId", desc: false },
]);
const handleDeleteRows = async () => {
const selectedRows = table.getSelectedRowModel().rows;
const selectedIds = selectedRows.map((row) => row.original.id);
console.log("🚀 ~ handleDeleteRows ~ selectedIds:", selectedIds)
};
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: setSorting,
enableSortingRemoval: false,
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
getFilteredRowModel: getFilteredRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
state: { sorting, pagination, columnFilters, columnVisibility },
});
// Get unique difficulty values
const uniqueDifficultyValues = useMemo(() => {
const difficultyColumn = table.getColumn("difficulty");
if (!difficultyColumn) return [];
const values = Array.from(difficultyColumn.getFacetedUniqueValues().keys());
return values.sort();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [table.getColumn("difficulty")?.getFacetedUniqueValues()]);
// Get counts for each difficulty
const difficultyCounts = useMemo(() => {
const difficultyColumn = table.getColumn("difficulty");
if (!difficultyColumn) return new Map();
return difficultyColumn.getFacetedUniqueValues();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [table.getColumn("difficulty")?.getFacetedUniqueValues()]);
const selectedDifficulties = useMemo(() => {
const filterValue = table.getColumn("difficulty")?.getFilterValue() as string[];
return filterValue ?? [];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [table.getColumn("difficulty")?.getFilterValue()]);
const handleDifficultyChange = (checked: boolean, value: string) => {
const filterValue = table.getColumn("difficulty")?.getFilterValue() as string[];
const newFilterValue = filterValue ? [...filterValue] : [];
if (checked) {
newFilterValue.push(value);
} else {
const index = newFilterValue.indexOf(value);
if (index > -1) {
newFilterValue.splice(index, 1);
}
}
table
.getColumn("difficulty")
?.setFilterValue(newFilterValue.length ? newFilterValue : undefined);
};
return (
<div className="space-y-4 pb-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="relative">
<Input
ref={inputRef}
className={cn(
"peer min-w-60 ps-9",
Boolean(table.getColumn("displayId")?.getFilterValue()) && "pe-9"
)}
value={
(table.getColumn("displayId")?.getFilterValue() ?? "") as string
}
onChange={(e) =>
table.getColumn("displayId")?.setFilterValue(e.target.value)
}
placeholder="DisplayId or Title..."
type="text"
aria-label="Filter by displayId or title"
/>
<div className="text-muted-foreground/80 pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 peer-disabled:opacity-50">
<ListFilterIcon size={16} aria-hidden="true" />
</div>
{Boolean(table.getColumn("displayId")?.getFilterValue()) && (
<button
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Clear filter"
onClick={() => {
table.getColumn("displayId")?.setFilterValue("");
if (inputRef.current) {
inputRef.current.focus();
}
}}
>
<CircleXIcon size={16} aria-hidden="true" />
</button>
)}
</div>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">
<FilterIcon
className="-ms-1 opacity-60"
size={16}
aria-hidden="true"
/>
Difficulty
{selectedDifficulties.length > 0 && (
<span className="bg-background text-muted-foreground/70 -me-1 inline-flex h-5 max-h-full items-center rounded border px-1 font-[inherit] text-[0.625rem] font-medium">
{selectedDifficulties.length}
</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto min-w-36 p-3" align="start">
<div className="space-y-3">
<div className="text-muted-foreground text-xs font-medium">
Filters
</div>
<div className="space-y-3">
{uniqueDifficultyValues.map((value) => (
<div key={value} className="flex items-center gap-2">
<Checkbox
checked={selectedDifficulties.includes(value)}
onCheckedChange={(checked: boolean) =>
handleDifficultyChange(checked, value)
}
/>
<Label className="flex grow justify-between gap-2 font-normal">
{value}{" "}
<span className="text-muted-foreground ms-2 text-xs">
{difficultyCounts.get(value)}
</span>
</Label>
</div>
))}
</div>
</div>
</PopoverContent>
</Popover>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<Columns3Icon
className="-ms-1 opacity-60"
size={16}
aria-hidden="true"
/>
View
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
onSelect={(event) => event.preventDefault()}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center gap-3">
{table.getSelectedRowModel().rows.length > 0 && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="ml-auto" variant="outline">
<TrashIcon
className="-ms-1 opacity-60"
size={16}
aria-hidden="true"
/>
Delete
<span className="bg-background text-muted-foreground/70 -me-1 inline-flex h-5 max-h-full items-center rounded border px-1 font-[inherit] text-[0.625rem] font-medium">
{table.getSelectedRowModel().rows.length}
</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<div className="flex flex-col gap-2 max-sm:items-center sm:flex-row sm:gap-4">
<div
className="flex size-9 shrink-0 items-center justify-center rounded-full border"
aria-hidden="true"
>
<CircleAlertIcon className="opacity-80" size={16} />
</div>
<AlertDialogHeader>
<AlertDialogTitle>
Are you absolutely sure?
</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete{" "}
{table.getSelectedRowModel().rows.length} selected{" "}
{table.getSelectedRowModel().rows.length === 1
? "row"
: "rows"}
.
</AlertDialogDescription>
</AlertDialogHeader>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteRows}>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
<Button className="ml-auto" variant="outline" asChild>
<Link href="/dashboard/problemset/new">
<PlusIcon
className="-ms-1 opacity-60"
size={16}
aria-hidden="true"
/>
Add Problem
</Link>
</Button>
</div>
</div>
<div className="bg-background overflow-hidden rounded-md">
<Table className="table-fixed">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="hover:bg-transparent">
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
style={{ width: `${header.getSize()}px` }}
className="h-11"
>
{header.isPlaceholder ? null : header.column.getCanSort() ? (
<div
className={cn(
"flex h-full cursor-pointer items-center justify-between gap-2 select-none"
)}
onClick={header.column.getToggleSortingHandler()}
onKeyDown={(e) => {
if (
header.column.getCanSort() &&
(e.key === "Enter" || e.key === " ")
) {
e.preventDefault();
header.column.getToggleSortingHandler()?.(e);
}
}}
tabIndex={header.column.getCanSort() ? 0 : undefined}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{
{
asc: (
<ChevronUpIcon
className="shrink-0 opacity-60"
size={16}
aria-hidden="true"
/>
),
desc: (
<ChevronDownIcon
className="shrink-0 opacity-60"
size={16}
aria-hidden="true"
/>
),
}[header.column.getIsSorted() as string] ?? null
}
</div>
) : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody className="[&_td:first-child]:rounded-l-lg [&_td:last-child]:rounded-r-lg">
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-10 border-b-0 cursor-pointer odd:bg-muted/50 hover:text-blue-500 hover:bg-muted"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="last:py-0">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-between gap-8">
<div className="flex items-center gap-3">
<Label className="max-sm:sr-only">Rows per page</Label>
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="w-fit whitespace-nowrap">
<SelectValue placeholder="Select number of results" />
</SelectTrigger>
<SelectContent className="[&_*[role=option]]:ps-2 [&_*[role=option]]:pe-8 [&_*[role=option]>span]:start-auto [&_*[role=option]>span]:end-2">
{[5, 10, 25, 50].map((pageSize) => (
<SelectItem key={pageSize} value={pageSize.toString()}>
<span className="mr-2">{pageSize}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="text-muted-foreground flex grow justify-end text-sm whitespace-nowrap">
<p
className="text-muted-foreground text-sm whitespace-nowrap"
aria-live="polite"
>
<span className="text-foreground">
{table.getState().pagination.pageIndex *
table.getState().pagination.pageSize +
1}
-
{Math.min(
Math.max(
table.getState().pagination.pageIndex *
table.getState().pagination.pageSize +
table.getState().pagination.pageSize,
0
),
table.getRowCount()
)}
</span>{" "}
of{" "}
<span className="text-foreground">
{table.getRowCount().toString()}
</span>
</p>
</div>
<div>
<Pagination>
<PaginationContent>
<PaginationItem>
<Button
size="icon"
variant="outline"
className="disabled:pointer-events-none disabled:opacity-50"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
aria-label="Go to first page"
>
<ChevronFirstIcon size={16} aria-hidden="true" />
</Button>
</PaginationItem>
<PaginationItem>
<Button
size="icon"
variant="outline"
className="disabled:pointer-events-none disabled:opacity-50"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
aria-label="Go to previous page"
>
<ChevronLeftIcon size={16} aria-hidden="true" />
</Button>
</PaginationItem>
<PaginationItem>
<Button
size="icon"
variant="outline"
className="disabled:pointer-events-none disabled:opacity-50"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
aria-label="Go to next page"
>
<ChevronRightIcon size={16} aria-hidden="true" />
</Button>
</PaginationItem>
<PaginationItem>
<Button
size="icon"
variant="outline"
className="disabled:pointer-events-none disabled:opacity-50"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
aria-label="Go to last page"
>
<ChevronLastIcon size={16} aria-hidden="true" />
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</div>
</div>
);
}
function RowActions() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="flex justify-end">
<Button
size="icon"
variant="ghost"
className="shadow-none"
aria-label="Edit item"
>
<EllipsisIcon size={16} aria-hidden="true" />
</Button>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuItem>
<span>Edit</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive focus:text-destructive">
<span>Delete</span>
<DropdownMenuShortcut></DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -1,45 +0,0 @@
import { cn } from "@/lib/utils";
import { auth } from "@/lib/auth";
import BackButton from "@/components/back-button";
import { RunCodeButton } from "@/components/run-code";
import { AvatarButton } from "@/components/avatar-button";
import BotVisibilityToggle from "@/components/bot-visibility-toggle";
interface PlaygroundHeaderProps {
className?: string;
}
export async function PlaygroundHeader({
className,
...props
}: PlaygroundHeaderProps) {
const session = await auth();
return (
<header
{...props}
className={cn("relative", className)}
>
<nav className="z-0 relative h-12 w-full flex shrink-0 items-center px-2.5">
<div className="w-full flex justify-between">
<div className="w-full flex items-center justify-between">
<div className="flex items-center">
<BackButton href="/problemset" />
</div>
<div className="relative flex items-center gap-2">
<BotVisibilityToggle />
<AvatarButton />
</div>
</div>
</div>
</nav>
<div className="z-10 absolute left-1/2 top-0 h-full -translate-x-1/2 py-2">
<div className="relative flex">
<div className="relative flex overflow-hidden rounded">
<RunCodeButton session={session} className="bg-muted text-muted-foreground hover:bg-muted/50" />
</div>
</div>
</div>
</header>
);
}

View File

@ -1,23 +0,0 @@
import { cn } from "@/lib/utils";
interface ProblemDescriptionFooterProps {
title: string;
className?: string;
}
export default function ProblemDescriptionFooter({
title,
className,
...props
}: ProblemDescriptionFooterProps) {
return (
<footer
{...props}
className={cn("h-9 flex flex-none items-center bg-muted px-3 py-2", className)}
>
<div className="w-full flex items-center justify-center">
<span className="truncate">{title}</span>
</div>
</footer>
);
}

Some files were not shown because too many files have changed in this diff Show More