mirror of
https://github.com/massbug/judge4c.git
synced 2025-07-03 23:30:50 +00:00
Merge 7d5a07c106
into 443adf055b
This commit is contained in:
commit
dde5f7c92d
@ -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
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
@ -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 视频。"
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
13
package.json
13
package.json
@ -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",
|
||||
|
@ -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;
|
@ -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;
|
@ -1,4 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "password" TEXT,
|
||||
ALTER COLUMN "name" DROP NOT NULL,
|
||||
ALTER COLUMN "email" DROP NOT NULL;
|
@ -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")
|
||||
);
|
@ -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");
|
@ -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;
|
@ -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;
|
@ -1,2 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ExitCode" AS ENUM ('SE', 'CS', 'CE', 'TLE', 'MLE', 'RE', 'AC', 'WA');
|
@ -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")
|
||||
);
|
@ -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;
|
@ -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;
|
@ -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");
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "TestcaseResult" ALTER COLUMN "output" DROP NOT NULL;
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "password" TEXT;
|
2
prisma/migrations/20250615052102_add_trim/migration.sql
Normal file
2
prisma/migrations/20250615052102_add_trim/migration.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Problem" ADD COLUMN "trim" BOOLEAN NOT NULL DEFAULT false;
|
164
prisma/migrations/20250615073315_init/migration.sql
Normal file
164
prisma/migrations/20250615073315_init/migration.sql
Normal 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;
|
156
prisma/migrations/20250615082129_add_trim/migration.sql
Normal file
156
prisma/migrations/20250615082129_add_trim/migration.sql
Normal 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;
|
13
prisma/migrations/20250616020439_add_random/migration.sql
Normal file
13
prisma/migrations/20250616020439_add_random/migration.sql
Normal 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;
|
16
prisma/migrations/20250616021548_rename_type/migration.sql
Normal file
16
prisma/migrations/20250616021548_rename_type/migration.sql
Normal 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";
|
@ -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
|
||||
|
995
prisma/seed.ts
995
prisma/seed.ts
File diff suppressed because it is too large
Load Diff
1
public/flags/cn.svg
Normal file
1
public/flags/cn.svg
Normal 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
1
public/flags/us.svg
Normal 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
BIN
public/sign-in.mp4
Normal file
Binary file not shown.
@ -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;
|
||||
}
|
@ -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 },
|
||||
});
|
||||
}
|
||||
};
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export default function DashboardAdmin() {
|
||||
return <div>Dashboard Admin</div>;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import NewProblemDescriptionForm from "@/components/features/dashboard/admin/problemset/new/components/description-form";
|
||||
|
||||
export default function NewProblemDescriptionPage() {
|
||||
return <NewProblemDescriptionForm />;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import NewProblemMetadataForm from "@/components/features/dashboard/admin/problemset/new/components/metadata-form";
|
||||
|
||||
export default function NewProblemMetadataPage() {
|
||||
return <NewProblemMetadataForm />;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function NewProblemPage() {
|
||||
redirect("/dashboard/problemset/new/metadata");
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import NewProblemSolutionForm from "@/components/features/dashboard/admin/problemset/new/components/solution-form";
|
||||
|
||||
export default function NewProblemSolutionPage() {
|
||||
return <NewProblemSolutionForm />;
|
||||
}
|
@ -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);
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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} />;
|
||||
}
|
@ -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();
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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",
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
27
src/app/(app)/problems/[problemId]/layout.tsx
Normal file
27
src/app/(app)/problems/[problemId]/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
39
src/app/(app)/problems/[problemId]/page.tsx
Normal file
39
src/app/(app)/problems/[problemId]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
6
src/app/actions/analyze.ts
Normal file
6
src/app/actions/analyze.ts
Normal 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)"] };
|
||||
};
|
@ -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
102
src/app/actions/compile.ts
Normal 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
107
src/app/actions/docker.ts
Normal 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
174
src/app/actions/judge.ts
Normal 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
145
src/app/actions/random.ts
Normal 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
260
src/app/actions/run.ts
Normal 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
|
||||
);
|
||||
};
|
@ -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();
|
||||
|
@ -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 />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
113
src/components/content/pre-detail.tsx
Normal file
113
src/components/content/pre-detail.tsx
Normal 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>
|
||||
);
|
||||
};
|
207
src/components/core-editor.tsx
Normal file
207
src/components/core-editor.tsx
Normal 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),
|
||||
},
|
||||
});
|
||||
};
|
@ -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"
|
||||
>
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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 };
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user