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

* 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:
cfngc4594 2025-06-21 14:09:57 +08:00 committed by GitHub
parent 443adf055b
commit 5af9d88db7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
205 changed files with 7145 additions and 6517 deletions

View File

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

693
bun.lock

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,55 +49,133 @@ 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
language Language
content String
status Status
message String?
executionTime Int?
timeUsage Int?
memoryUsage Int?
testcaseResults TestcaseResult[]
userId String
problemId String
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[]
expectedOutput String
inputs TestcaseInput[]
testcaseResults TestcaseResult[]
problemId String
problem Problem @relation(fields: [problemId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model TestcaseData {
model TestcaseInput {
id String @id @default(cuid())
index Int
label String
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
executionTime Int?
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)
@ -182,6 +183,23 @@ model TestcaseResult {
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,6 +225,7 @@ model Session {
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())

File diff suppressed because it is too large Load Diff

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

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

After

Width:  |  Height:  |  Size: 531 B

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

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

After

Width:  |  Height:  |  Size: 741 B

BIN
public/sign-in.mp4 Normal file

Binary file not shown.

View File

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

View File

@ -1,29 +0,0 @@
"use server";
import prisma from "@/lib/prisma";
import { EditorLanguage } from "@/generated/client";
import { SettingsLanguageServerFormValues } from "@/app/(app)/dashboard/@admin/settings/language-server/form";
export const getLanguageServerConfig = async (language: EditorLanguage) => {
return await prisma.languageServerConfig.findUnique({
where: { language },
});
};
export const handleLanguageServerConfigSubmit = async (
language: EditorLanguage,
data: SettingsLanguageServerFormValues
) => {
const existing = await getLanguageServerConfig(language);
if (existing) {
await prisma.languageServerConfig.update({
where: { language },
data,
});
} else {
await prisma.languageServerConfig.create({
data: { ...data, language },
});
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +0,0 @@
import { Suspense } from "react";
import { Loading } from "@/components/loading";
import { WorkspaceEditorHeader } from "@/components/features/playground/workspace/editor/components/header";
import { WorkspaceEditorFooter } from "@/components/features/playground/workspace/editor/components/footer";
interface CodeLayoutProps {
children: React.ReactNode;
}
export default function CodeLayout({ children }: CodeLayoutProps) {
return (
<div className="flex flex-col h-full border border-t-0 border-muted rounded-b-3xl bg-background">
<WorkspaceEditorHeader />
<Suspense fallback={<Loading />}>
{children}
</Suspense>
<WorkspaceEditorFooter />
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

@ -0,0 +1,107 @@
import "server-only";
import fs from "fs";
import tar from "tar-stream";
import Docker from "dockerode";
import { Readable } from "stream";
import { Writable } from "stream";
import type { DockerConfig } from "@/generated/client";
const isRemote = process.env.DOCKER_HOST_MODE === "remote";
// Docker client initialization
export const docker: Docker = isRemote
? new Docker({
protocol: process.env.DOCKER_REMOTE_PROTOCOL as
| "https"
| "http"
| "ssh"
| undefined,
host: process.env.DOCKER_REMOTE_HOST,
port: process.env.DOCKER_REMOTE_PORT,
ca: fs.readFileSync(process.env.DOCKER_REMOTE_CA_PATH || "/certs/ca.pem"),
cert: fs.readFileSync(
process.env.DOCKER_REMOTE_CERT_PATH || "/certs/cert.pem"
),
key: fs.readFileSync(
process.env.DOCKER_REMOTE_KEY_PATH || "/certs/key.pem"
),
})
: new Docker({ socketPath: "/var/run/docker.sock" });
// Prepare Docker image environment
export const prepareEnvironment = async (
image: string,
tag: string
): Promise<boolean> => {
try {
const reference = `${image}:${tag}`;
const filters = { reference: [reference] };
const images = await docker.listImages({ filters });
return images.length !== 0;
} catch (error) {
console.error("Error checking Docker images:", error);
return false;
}
};
// Create Docker container with keep-alive
export const createContainer = async (
config: DockerConfig,
memoryLimit: number
): Promise<Docker.Container> => {
const { image, tag, workingDir } = config;
const container = await docker.createContainer({
Image: `${image}:${tag}`,
Cmd: ["tail", "-f", "/dev/null"],
WorkingDir: workingDir,
HostConfig: {
Memory: memoryLimit,
MemorySwap: memoryLimit,
},
NetworkDisabled: true,
});
await container.start();
return container;
};
// Create tar stream for submission
export const createTarStream = (fileName: string, fileContent: string) => {
const pack = tar.pack();
pack.entry({ name: fileName }, fileContent);
pack.finalize();
return Readable.from(pack);
};
export const createLimitedStream = (maxSize: number) => {
const buffers: string[] = [];
let totalLength = 0;
const stream = new Writable({
write(chunk, _encoding, callback) {
const text = chunk.toString();
const remaining = maxSize - totalLength;
if (remaining <= 0) {
callback();
return;
}
if (text.length > remaining) {
buffers.push(text.slice(0, remaining));
totalLength = maxSize;
} else {
buffers.push(text);
totalLength += text.length;
}
callback();
},
});
return {
stream,
buffers,
};
};

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

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

253
src/app/actions/run.ts Normal file
View 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
);
};

View File

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

View File

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

View File

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

View File

@ -2,40 +2,25 @@ import Link from "next/link";
import { cn } from "@/lib/utils";
import { useTranslations } from "next-intl";
import { ArrowLeftIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { TooltipButton } from "@/components/tooltip-button";
interface BackButtonProps {
href: string;
className?: string;
}
export default function BackButton({
href,
className,
...props
}: BackButtonProps) {
export const BackButton = ({ href, className }: BackButtonProps) => {
const t = useTranslations();
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
<TooltipButton
tooltipContent={t("BackButton")}
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>
);
}
};

View File

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

View File

@ -1,30 +1,26 @@
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}
@ -36,8 +32,8 @@ export function MdxRenderer({ source, className }: MdxRendererProps) {
rehypePrettyCode,
{
theme: {
light: DefaultLightThemeConfig.id,
dark: DefaultDarkThemeConfig.id,
light: "github-light-default",
dark: "github-dark-default",
},
keepBackground: false,
},
@ -46,9 +42,8 @@ export function MdxRenderer({ source, className }: MdxRendererProps) {
remarkPlugins: [remarkGfm, remarkMath],
},
}}
components={MdxComponents}
components={components}
/>
</article>
</Suspense>
);
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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