mirror of
https://github.com/massbug/judge4c.git
synced 2025-07-01 05:34:47 +00:00
feat: update main branch (#45)
Some checks failed
Build & Push Judge4c Docker Image / build-and-push-judge4c-docker-image (., Dockerfile, judge4c) (push) Failing after 17s
Some checks failed
Build & Push Judge4c Docker Image / build-and-push-judge4c-docker-image (., Dockerfile, judge4c) (push) Failing after 17s
* chore(deps): add pino and pino-pretty packages * feat(log): add pino logging support * chore(tailwind): update config with features path and animate import * chore(deps): add react-icons package * refactor(auth)!: replace credentials with OAuth providers and add logging BREAKING CHANGE: - Removed credentials-based authentication - Added Google OAuth provider - Implemented detailed logging for auth events - Removed custom JWT/session handling - Added sign-in page configuration - Marked as server-only * refactor(auth)!: remove components and rewrite sign-in page * feat(user-avatar): refactor avatar component into user-avatar with improved structure * style(dockview): reduce tabs container height and center align items - Changed --dv-tabs-and-actions-container-height from 44px to 36px - Added align-items: center to .dv-tabs-container - Improved CSS formatting for better readability * refactor(stores): split dockview store into problem-specific store - Remove generic dockview store (`src/stores/dockview.tsx`) - Add problem-specific dockview store (`src/stores/problem-dockview.tsx`) - Remove submission-related state as it's no longer needed * feat(dockview): refactor dockview component and add problem-specific implementation - Refactor Dockview component into more modular structure: - Extract layout persistence logic to custom hook - Extract component conversion logic to custom hook - Make storageKey optional - Improve type safety with PanelParams interface - Add better error handling and duplicate panel detection - Add new ProblemDockview wrapper component: - Integrates with problem-dockview store - Adds locale awareness - Provides standardized storage key - Update related type definitions and imports * refactor(problems): migrate description and solution to feature-based structure - Remove old parallel route implementations (@Description and @Solutions) - Add new feature-based components for problem description and solution - Create content and panel components for both features - Implement skeleton loading states - Use cached data fetching - Update MDX rendering and scroll area implementations * chore(problems): move problem-dockview to components directory * refactor(layouts): overhaul problem and problemset page structures - Simplify ProblemLayout to use children prop and remove ProblemStoreProvider - Replace PlaygroundHeader with dedicated ProblemHeader component - Streamline ProblemsetLayout with new ProblemsetHeader - Remove deprecated BackButton in favor of NavigateBackButton - Delete unused ProblemStoreProvider and related dependencies * feat(prisma): add server-only caching for problems queries - Add 'server-only' import to enforce server-side usage - Implement cached problem queries with logging: - Add getProblems/getCachedProblems for all problems - Add getProblem/getCachedProblem for single problem by ID - Use React cache and Next.js unstable_cache with tags - Add detailed logging with timing metrics * refactor(mdx-renderer): simplify component implementation - Remove Suspense and Skeleton loading state - Convert to arrow function syntax - Reorganize import statements - Simplify export syntax * feat(components): add TooltipButton component - A reusable button with tooltip functionality - Supports customizable delay, tooltip content, and className - Uses shadcn/ui Tooltip and Button components * refactor(editor): consolidate editor toolbar actions into unified structure - Moved all editor action buttons (copy, format, undo, redo, reset) from `src/components/features/playground/workspace/editor/components/` to new location `src/features/problems/code/components/toolbar/actions/` - Introduced shared `TooltipButton` component to reduce duplication - Created centralized `useProblemEditorActions` hook for common editor operations - Updated imports and exports through new index file - Maintained all existing functionality while improving code organization * refactor(structure): reorganize page and component exports - Move root page from /(app) to / directory - Convert default exports to named exports in components - Rename MainView component to HeroSection for better semantics * refactor(route): rename [id] to [problemId] in problems route * chore(components): remove unused problem-related components - Delete footer.tsx from problem/description - Delete solution.tsx from problem/description - Delete header.tsx from problem/workspace/editor/components * fix(page): add missing return statement in RootPage component * feat(prisma/schema): add multilingual support for problem descriptions and solutions BREAKING CHANGE: - Removed `description` and `solution` fields from Problem model - Added new models `ProblemDescription` and `ProblemSolution` with language support (EN/ZH) - Updated seed data structure to support multilingual content - Requires database migration and data migration from old structure * chore(prisma/migrations): add migration for multilingual problem support * fix(styles): adjust tabs overflow container height in abyss theme Changed height property from 'unset !important' to '100%' for .dv-tabs-overflow-container and .dv-tabs-overflow-dropdown-default in the abyss-spaced theme to fix layout issues. * refactor(loading): change to named export and arrow function - Switch from default export to named export - Convert component to arrow function - Remove unused props parameter * chore(shiki): convert function to arrow function * chore(scripts): remove --turbopack flag from dev script * refactor(prisma): simplify schema and remove zod-prisma-types - Reorganized Prisma schema structure with simplified models and relations - Removed zod-prisma-types generator as it's no longer needed - Consolidated problem content types into a single ProblemLocalization model - Simplified testcase and template structures - Removed unused prisma types file * refactor(prisma): simplify schema and remove zod-prisma-types - Reorganized Prisma schema structure with simplified models and relations - Removed zod-prisma-types generator as it's no longer needed - Consolidated problem content types into a single ProblemLocalization model - Simplified testcase and template structures - Removed unused prisma types file * refactor(i18n): migrate locale configuration to generated types - Replace hardcoded locale strings with generated Locale enum from client - Update cookie key name to be more specific (LOCALE_COOKIE_NAME -> JUDGE4C_LOCALE_KEY) - Rename defaultLocale to DEFAULT_LOCALE for consistency - Add 'server-only' imports to ensure server-side usage - Simplify locale validation using Object.values(Locale) - Clean up accept-language header parsing logic * refactor(hooks): remove theme config and simplify theme hook - Delete src/config/monaco-theme.ts and src/types/monaco-theme.ts - Simplify useMonacoTheme hook to directly return theme strings - Format use-mobile.ts with consistent semicolons and quotes * refactor(i18n): replace language-settings with locale-switcher - Replace react-world-flags with next/image for better optimization - Simplify locale handling logic and remove unused getUserLocale - Rename component to be more descriptive (language-settings -> locale-switcher) - Update all references to use the new component - Add proper SVG flag assets for supported locales - Remove react-world-flags dependency from package.json * refactor(shiki): migrate to shiki/core with lazy initialization - Replace shiki with shiki/core for better tree-shaking - Change highlighter initialization to explicit theme/lang imports - Export getHighlighter function instead of direct highlighter instance - Add error handling for uninitialized highlighter access * refactor(language-selector): migrate to new location and implementation - Remove old language selector from `src/components/features/playground/workspace/editor/components` - Add new implementation in `src/features/problems/code/components/toolbar/controls` - Update toolbar exports to include the new selector * feat(problems): add localization support for problem descriptions and solutions - Replace cached problem data with direct Prisma queries for localized content - Implement locale-based content selection for both descriptions and solutions - Refactor skeleton loading components structure - Change all exports from named to default exports * refactor(code-toolbar): standardize component exports and simplify reset logic - Changed all toolbar action components from named exports with curly braces to default exports - Simplified ResetButton by moving template logic to useProblemEditorActions hook - Updated useProblemEditorActions to handle template selection internally using store data - Renamed problem-editor-store import to problem-editor for consistency * refactor(prisma): simplify prisma client initialization and remove caching Removed all caching and logging functionality from prisma.ts, keeping only the basic Prisma client initialization logic. This includes: - Removal of getProblems, getCachedProblems, getProblem, getCachedProblem functions - Removal of related logger and cache imports - Simplified the global prisma client instantiation * feat(config): add LANGUAGES constant for language enum values * refactor(problemset): move components to separate files and improve structure - Move ProblemsetHeader component from 'problemset-header' to 'header' - Extract problemset table logic into dedicated ProblemsetTable component - Add Suspense and skeleton loading for better UX - Update layout and page structure * feat(dockview): export Dockview component and create ProblemDockview wrapper - Export Dockview component from src/components/dockview.tsx to make it reusable - Create new ProblemDockview component in src/features/problems/components/dockview.tsx that: - Integrates with next-intl for locale handling - Connects to problem-dockview store - Wraps Dockview with problem-specific configuration - Adds locale-based key for proper re-rendering - Maintain existing Dockview functionality including: - Layout persistence - Panel management - API handling * refactor(problems): update ProblemHeader import path to new location * refactor(mdx-render): simplify theme configuration and change export style - Remove custom Monaco theme imports - Use github default themes for rehypePrettyCode - Change component export from default to named export * feat(store): add problem editor state management - Create problem-editor store with Zustand - Manage editor state including problem, language, value and path - Implement localStorage persistence for editor content - Provide utility functions for language and value retrieval * chore(utils): remove unused utility functions - Remove getPath function (moved to problem-editor store) - Remove getDifficultyColorClass function (unused) - Keep only core cn utility function * feat(code-panel): add code editor panel components - Add CodePanel component as container for editor interface - Add CodeContent component with Prisma data fetching - Include CodeContentSkeleton for loading state - Implement ProblemEditor integration with templates * refactor(back-button): relocate and rename back button component - Move from features/problems/components to shared components directory - Rename NavigateBackButton to simpler BackButton - Update component interface naming to BackButtonProps - Keep all existing functionality intact * feat(problemset): enhance header component with back button and className prop - Replace problem-header.tsx with new header.tsx implementation - Add BackButton functionality to navigate to home page - Make className prop configurable using cn utility - Maintain existing user avatar functionality * refactor(config): migrate editor options to standalone editor config - Rename `editor-language.ts` to `editor.ts` - Update interface from `IEditorConstructionOptions` to `IStandaloneEditorConstructionOptions` - Keep all existing editor options unchanged * refactor(components): simplify BackButton by using TooltipButton * refactor(problems): consolidate judge status toast - Migrate status toast from shared hooks to problems feature - Remove deprecated show-status-toast.tsx and status.ts - Implement self-contained JudgeToast component with built-in status mapping * refactor(judge-button): migrate run code button to standalone component - Replace RunCodeButton with new JudgeButton component - Use problem-specific stores instead of playground stores - Implement new judge toast notification system - Simplify authentication check logic - Utilize new TooltipButton component * refactor(api): extract OpenAI client to shared module * fix(prisma): make TestcaseResult.output field optional * chore(prisma): make TestcaseResult.output nullable in migration * feat: replace dockview with flexlayout-react
This commit is contained in:
parent
443adf055b
commit
5af9d88db7
@ -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;
|
@ -1,116 +1,39 @@
|
||||
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 Status {
|
||||
PD // PENDING
|
||||
QD // QUEUED
|
||||
@ -126,14 +49,86 @@ 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)
|
||||
timeLimit Int @default(1000)
|
||||
memoryLimit Int @default(134217728)
|
||||
|
||||
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 +136,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 +213,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 +225,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
|
||||
|
986
prisma/seed.ts
986
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";
|
||||
}
|
||||
};
|
253
src/app/actions/run.ts
Normal file
253
src/app/actions/run.ts
Normal file
@ -0,0 +1,253 @@
|
||||
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
|
||||
): 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) {
|
||||
const isCorrect = stdout.trim() === expectedOutput;
|
||||
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[]
|
||||
): 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
|
||||
);
|
||||
|
||||
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 } = 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
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ProblemSolutionFooterProps {
|
||||
title: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ProblemSolutionFooter({
|
||||
title,
|
||||
className,
|
||||
...props
|
||||
}: ProblemSolutionFooterProps) {
|
||||
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>
|
||||
);
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
|
||||
export function CopyButton() {
|
||||
const { editor } = useProblem();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const t = useTranslations("WorkspaceEditorHeader.CopyButton");
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(editor?.getValue() || "");
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-6 w-6 border-none shadow-none px-1.5 py-0.5 hover:bg-muted",
|
||||
copied ? "disabled:opacity-100" : ""
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
aria-label={copied ? "Copied" : "Copy to clipboard"}
|
||||
disabled={!editor || copied}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all",
|
||||
copied ? "scale-100 opacity-100" : "scale-0 opacity-0"
|
||||
)}
|
||||
>
|
||||
<Check
|
||||
className="stroke-emerald-500"
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute transition-all",
|
||||
copied ? "scale-0 opacity-0" : "scale-100 opacity-100"
|
||||
)}
|
||||
>
|
||||
<Copy size={16} strokeWidth={2} aria-hidden="true" />
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Paintbrush } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
|
||||
export function FormatButton() {
|
||||
const { editor } = useProblem();
|
||||
const t = useTranslations("WorkspaceEditorHeader.FormatButton");
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label={t("TooltipContent")}
|
||||
onClick={() => {
|
||||
editor?.trigger("format", "editor.action.formatDocument", null);
|
||||
}}
|
||||
className="h-6 w-6 px-1.5 py-0.5 border-none shadow-none hover:bg-muted"
|
||||
disabled={!editor}
|
||||
>
|
||||
<Paintbrush size={16} strokeWidth={2} aria-hidden="true" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CopyButton } from "./copy-button";
|
||||
import { RedoButton } from "./redo-button";
|
||||
import { UndoButton } from "./undo-button";
|
||||
import { ResetButton } from "./reset-button";
|
||||
import { FormatButton } from "./format-button";
|
||||
import { LspStatusButton } from "./lsp-status-button";
|
||||
import { LanguageSelector } from "./language-selector";
|
||||
|
||||
interface WorkspaceEditorHeaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function WorkspaceEditorHeader({
|
||||
className,
|
||||
...props
|
||||
}: WorkspaceEditorHeaderProps) {
|
||||
return (
|
||||
<header
|
||||
{...props}
|
||||
className={cn("flex items-center flex-none h-8 relative border-b", className)}
|
||||
>
|
||||
<div className="absolute flex w-full items-center justify-between px-2">
|
||||
<div className="flex items-center">
|
||||
<LanguageSelector />
|
||||
<LspStatusButton />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<ResetButton />
|
||||
<UndoButton />
|
||||
<RedoButton />
|
||||
<FormatButton />
|
||||
<CopyButton />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
|
||||
const getLspStatusColor = (webSocket: WebSocket | null) => {
|
||||
if (!webSocket) return "bg-gray-500";
|
||||
|
||||
switch (webSocket.readyState) {
|
||||
case WebSocket.CONNECTING:
|
||||
return "bg-yellow-500";
|
||||
case WebSocket.OPEN:
|
||||
return "bg-green-500";
|
||||
case WebSocket.CLOSING:
|
||||
return "bg-orange-500";
|
||||
case WebSocket.CLOSED:
|
||||
return "bg-red-500";
|
||||
default:
|
||||
return "bg-gray-500";
|
||||
}
|
||||
};
|
||||
|
||||
export function LspStatusButton() {
|
||||
const { webSocket } = useProblem();
|
||||
const t = useTranslations("WorkspaceEditorHeader.LspStatusButton");
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-6 w-auto px-2 py-0 gap-1 border-none shadow-none hover:bg-muted"
|
||||
>
|
||||
<div className="h-3 w-3 flex items-center justify-center">
|
||||
<div
|
||||
className={`h-1.5 w-1.5 rounded-full transition-all ${getLspStatusColor(
|
||||
webSocket
|
||||
)}`}
|
||||
/>
|
||||
</div>
|
||||
LSP
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Redo2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
|
||||
export function RedoButton() {
|
||||
const { editor } = useProblem();
|
||||
const t = useTranslations("WorkspaceEditorHeader.RedoButton");
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label={t("TooltipContent")}
|
||||
onClick={() => {
|
||||
editor?.trigger("redo", "redo", null);
|
||||
}}
|
||||
disabled={!editor}
|
||||
className="h-6 w-6 px-1.5 py-0.5 border-none shadow-none hover:bg-muted"
|
||||
>
|
||||
<Redo2 size={16} strokeWidth={2} aria-hidden="true" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user