mirror of
https://github.com/massbug/judge4c.git
synced 2025-05-17 14:56:36 +00:00
refactor(i18n): replace hardcoded texts with i18n message keys
This commit is contained in:
parent
8b276ae91e
commit
4428a29306
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": [
|
||||
"messages",
|
||||
"src/i18n"
|
||||
]
|
||||
}
|
130
messages/en.json
Normal file
130
messages/en.json
Normal file
@ -0,0 +1,130 @@
|
||||
{
|
||||
"AppearanceSettings": {
|
||||
"title": "Choose a theme",
|
||||
"items": {
|
||||
"System": "System",
|
||||
"Light": "Light",
|
||||
"Dark": "Dark"
|
||||
}
|
||||
},
|
||||
"AvatarButton": {
|
||||
"Settings": "Settings",
|
||||
"LogIn": "LogIn",
|
||||
"LogOut": "LogOut"
|
||||
},
|
||||
"Banner": {
|
||||
"Text": "Star this project if you like it."
|
||||
},
|
||||
"BackButton": "Back",
|
||||
"Bot": {
|
||||
"title": "Ask Bot",
|
||||
"description": "Powered by Vercel Ai SDK",
|
||||
"placeholder": "Bot will automatically get your current code"
|
||||
},
|
||||
"BotVisibilityToggle": {
|
||||
"open": "Open Bot",
|
||||
"close": "Close Bot"
|
||||
},
|
||||
"DetailsPage": {
|
||||
"BackButton": "All Submissions",
|
||||
"Time": "Submitted on",
|
||||
"Input": "Input",
|
||||
"ExpectedOutput": "Expected Output",
|
||||
"ActualOutput": "Acutal Output",
|
||||
"Code": "Code"
|
||||
},
|
||||
"Difficulty": {
|
||||
"EASY": "EASY",
|
||||
"MEDIUM": "MEDIUM",
|
||||
"HARD": "HARD"
|
||||
},
|
||||
"LanguageSettings": {
|
||||
"en": {
|
||||
"flag": "🇺🇸",
|
||||
"name": "English"
|
||||
},
|
||||
"zh": {
|
||||
"flag": "🇨🇳",
|
||||
"name": "Chinese"
|
||||
}
|
||||
},
|
||||
"PlaygroundHeader": {
|
||||
"RunCodeButton": {
|
||||
"TooltipTrigger": {
|
||||
"loading": "Running...",
|
||||
"ready": "Run"
|
||||
},
|
||||
"TooltipContent": "Run Code"
|
||||
}
|
||||
},
|
||||
"ProblemPage": {
|
||||
"Description": "Description",
|
||||
"Solutions": "Solutions",
|
||||
"Submissions": "Submissions",
|
||||
"Details": "Details",
|
||||
"Code": "Code",
|
||||
"Testcase": "Testcase",
|
||||
"Bot": "Bot"
|
||||
},
|
||||
"ProblemsetPage": {
|
||||
"Status": "Status",
|
||||
"Title": "Title",
|
||||
"Difficulty": "Difficulty"
|
||||
},
|
||||
"SettingsDialog": {
|
||||
"title": "Settings",
|
||||
"description": "Customize your settings here.",
|
||||
"breadcrumb": "Settings",
|
||||
"nav": {
|
||||
"Appearance": "Appearance",
|
||||
"Language": "Language",
|
||||
"CodeEditor": "CodeEditor",
|
||||
"Advanced": "Advanced"
|
||||
}
|
||||
},
|
||||
"StatusMessage": {
|
||||
"PD": "Pending",
|
||||
"QD": "Queued",
|
||||
"CP": "Compiling",
|
||||
"CE": "Compilation Error",
|
||||
"CS": "Compilation Success",
|
||||
"RU": "Running",
|
||||
"TLE": "Time Limit Exceeded",
|
||||
"MLE": "Memory Limit Exceeded",
|
||||
"RE": "Runtime Error",
|
||||
"AC": "Accepted",
|
||||
"WA": "Wrong Answer",
|
||||
"SE": "System Error"
|
||||
},
|
||||
"SubmissionsTable": {
|
||||
"Index": "Index",
|
||||
"Status": "Status",
|
||||
"Language": "Language",
|
||||
"Time": "Time",
|
||||
"Memory": "Memory"
|
||||
},
|
||||
"WorkspaceEditorHeader": {
|
||||
"LspStatusButton": {
|
||||
"TooltipContent": "Language Server"
|
||||
},
|
||||
"ResetButton": {
|
||||
"TooltipContent": "Reset Code"
|
||||
},
|
||||
"UndoButton": {
|
||||
"TooltipContent": "Undo"
|
||||
},
|
||||
"RedoButton": {
|
||||
"TooltipContent": "Redo"
|
||||
},
|
||||
"FormatButton": {
|
||||
"TooltipContent": "Format"
|
||||
},
|
||||
"CopyButton": {
|
||||
"TooltipContent": "Copy"
|
||||
}
|
||||
},
|
||||
"WorkspaceEditorFooter": {
|
||||
"Row": "Row",
|
||||
"Column": "Column"
|
||||
}
|
||||
}
|
130
messages/zh.json
Normal file
130
messages/zh.json
Normal file
@ -0,0 +1,130 @@
|
||||
{
|
||||
"AppearanceSettings": {
|
||||
"title": "选择一个主题",
|
||||
"items": {
|
||||
"System": "系统",
|
||||
"Light": "浅色",
|
||||
"Dark": "深色"
|
||||
}
|
||||
},
|
||||
"AvatarButton": {
|
||||
"Settings": "设置",
|
||||
"LogIn": "登录",
|
||||
"LogOut": "登出"
|
||||
},
|
||||
"Banner": {
|
||||
"Text": "如果喜欢该项目不妨收藏一下"
|
||||
},
|
||||
"BackButton": "返回",
|
||||
"Bot": {
|
||||
"title": "询问AI助手",
|
||||
"description": "由Vercel Ai SDK驱动",
|
||||
"placeholder": "AI助手将自动获取您当前的代码"
|
||||
},
|
||||
"BotVisibilityToggle": {
|
||||
"open": "打开AI助手",
|
||||
"close": "关闭AI助手"
|
||||
},
|
||||
"DetailsPage": {
|
||||
"BackButton": "所有提交记录",
|
||||
"Time": "提交于",
|
||||
"Input": "输入",
|
||||
"ExpectedOutput": "期望输出",
|
||||
"ActualOutput": "实际输出",
|
||||
"Code": "代码"
|
||||
},
|
||||
"Difficulty": {
|
||||
"EASY": "简单",
|
||||
"MEDIUM": "中等",
|
||||
"HARD": "困难"
|
||||
},
|
||||
"LanguageSettings": {
|
||||
"en": {
|
||||
"flag": "🇺🇸",
|
||||
"name": "英语"
|
||||
},
|
||||
"zh": {
|
||||
"flag": "🇨🇳",
|
||||
"name": "中文"
|
||||
}
|
||||
},
|
||||
"PlaygroundHeader": {
|
||||
"RunCodeButton": {
|
||||
"TooltipTrigger": {
|
||||
"loading": "运行中...",
|
||||
"ready": "运行"
|
||||
},
|
||||
"TooltipContent": "运行代码"
|
||||
}
|
||||
},
|
||||
"ProblemPage": {
|
||||
"Description": "题目描述",
|
||||
"Solutions": "题解",
|
||||
"Submissions": "提交记录",
|
||||
"Details": "详情",
|
||||
"Code": "代码",
|
||||
"Testcase": "测试用例",
|
||||
"Bot": "AI助手"
|
||||
},
|
||||
"ProblemsetPage": {
|
||||
"Status": "状态",
|
||||
"Title": "题目",
|
||||
"Difficulty": "难度"
|
||||
},
|
||||
"SettingsDialog": {
|
||||
"title": "设置",
|
||||
"description": "在此处自定义设置。",
|
||||
"breadcrumb": "设置",
|
||||
"nav": {
|
||||
"Appearance": "外观",
|
||||
"Language": "语言",
|
||||
"CodeEditor": "代码编辑器",
|
||||
"Advanced": "高级设置"
|
||||
}
|
||||
},
|
||||
"StatusMessage": {
|
||||
"PD": "待处理",
|
||||
"QD": "排队中",
|
||||
"CP": "编译中",
|
||||
"CE": "编译错误",
|
||||
"CS": "编译成功",
|
||||
"RU": "运行中",
|
||||
"TLE": "超出时间限制",
|
||||
"MLE": "超出内存限制",
|
||||
"RE": "运行时错误",
|
||||
"AC": "通过",
|
||||
"WA": "解答错误",
|
||||
"SE": "系统错误"
|
||||
},
|
||||
"SubmissionsTable": {
|
||||
"Index": "序号",
|
||||
"Status": "状态",
|
||||
"Language": "语言",
|
||||
"Time": "执行用时",
|
||||
"Memory": "消耗内存"
|
||||
},
|
||||
"WorkspaceEditorHeader": {
|
||||
"LspStatusButton": {
|
||||
"TooltipContent": "语言服务"
|
||||
},
|
||||
"ResetButton": {
|
||||
"TooltipContent": "重置代码"
|
||||
},
|
||||
"UndoButton": {
|
||||
"TooltipContent": "撤销"
|
||||
},
|
||||
"RedoButton": {
|
||||
"TooltipContent": "恢复"
|
||||
},
|
||||
"FormatButton": {
|
||||
"TooltipContent": "格式化"
|
||||
},
|
||||
"CopyButton": {
|
||||
"TooltipContent": "复制"
|
||||
}
|
||||
},
|
||||
"WorkspaceEditorFooter": {
|
||||
"Row": "行",
|
||||
"Column": "列"
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import {
|
||||
} 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";
|
||||
@ -19,6 +20,7 @@ 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({
|
||||
@ -58,12 +60,12 @@ export default function Bot() {
|
||||
{!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>Ask Bot</span>
|
||||
<span className="font-thin text-xs">Powered by Vercel Ai SDK</span>
|
||||
</div>
|
||||
)}
|
||||
<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">
|
||||
@ -100,7 +102,7 @@ export default function Bot() {
|
||||
}
|
||||
}}
|
||||
className="h-full bg-muted border-transparent shadow-none rounded-lg"
|
||||
placeholder="Bot will automatically get your current code"
|
||||
placeholder={t("placeholder")}
|
||||
/>
|
||||
|
||||
<TooltipProvider delayDuration={0}>
|
||||
|
8
src/app/(app)/problems/[id]/@Details/layout.tsx
Normal file
8
src/app/(app)/problems/[id]/@Details/layout.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { getUserLocale } from "@/i18n/locale";
|
||||
import DetailsPage from "@/app/(app)/problems/[id]/@Details/page";
|
||||
|
||||
export default async function DetailsLayout() {
|
||||
const locale = await getUserLocale();
|
||||
|
||||
return <DetailsPage locale={locale} />;
|
||||
}
|
@ -7,7 +7,10 @@ import {
|
||||
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";
|
||||
@ -20,7 +23,14 @@ import type { TestcaseResultWithTestcase } from "@/types/prisma";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { formatDistanceToNow, isBefore, subDays, format } from "date-fns";
|
||||
|
||||
export default function DetailsPage() {
|
||||
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] =
|
||||
@ -53,7 +63,7 @@ export default function DetailsPage() {
|
||||
const createdAt = new Date(submission.createdAt);
|
||||
const submittedDisplay = isBefore(createdAt, subDays(new Date(), 1))
|
||||
? format(createdAt, "yyyy-MM-dd")
|
||||
: formatDistanceToNow(createdAt, { addSuffix: true });
|
||||
: formatDistanceToNow(createdAt, { addSuffix: true, locale: localeInstance });
|
||||
|
||||
const source = `\`\`\`${submission?.language}\n${submission?.code}\n\`\`\``;
|
||||
|
||||
@ -76,7 +86,7 @@ export default function DetailsPage() {
|
||||
className="h-8 w-auto p-2 hover:bg-transparent text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeftIcon size={16} aria-hidden="true" />
|
||||
<span>All Submissions</span>
|
||||
<span>{t("BackButton")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex-1">
|
||||
@ -92,10 +102,10 @@ export default function DetailsPage() {
|
||||
getStatusColorClass(submission.status)
|
||||
)}
|
||||
>
|
||||
<span>{statusMap.get(submission.status)?.message}</span>
|
||||
<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">Submitted on</span>
|
||||
<span className="whitespace-nowrap mr-1">{t("Time")}</span>
|
||||
<span className="max-w-full truncate">
|
||||
{submittedDisplay}
|
||||
</span>
|
||||
@ -118,7 +128,7 @@ export default function DetailsPage() {
|
||||
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">Input</h4>
|
||||
<h4 className="text-sm font-medium">{t("Input")}</h4>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground pb-2">
|
||||
<div className="space-y-4">
|
||||
@ -142,7 +152,7 @@ export default function DetailsPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Expected Output</h4>
|
||||
<h4 className="text-sm font-medium">{t("ExpectedOutput")}</h4>
|
||||
<Input
|
||||
type="text"
|
||||
value={lastFailedTestcase.testcase.expectedOutput}
|
||||
@ -153,7 +163,7 @@ export default function DetailsPage() {
|
||||
|
||||
{submission.status === "WA" && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Your Output</h4>
|
||||
<h4 className="text-sm font-medium">{t("ActualOutput")}</h4>
|
||||
<Input
|
||||
type="text"
|
||||
value={lastFailedTestcase.output}
|
||||
@ -167,14 +177,14 @@ export default function DetailsPage() {
|
||||
|
||||
{(submission.status === "CE" ||
|
||||
submission.status === "SE") && (
|
||||
<MdxPreview
|
||||
source={`\`\`\`shell\n${submission.message}\n\`\`\``}
|
||||
/>
|
||||
)}
|
||||
<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>Code</span>
|
||||
<span>{t("Code")}</span>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="h-4 bg-muted-foreground"
|
||||
|
@ -1,5 +1,6 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getUserLocale } from "@/i18n/locale";
|
||||
import SubmissionsTable from "@/components/submissions-table";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
|
||||
@ -37,10 +38,12 @@ export default async function SubmissionsPage({ params }: SubmissionsPageProps)
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const locale = await getUserLocale();
|
||||
|
||||
return (
|
||||
<div className="px-3 flex flex-col h-full border border-t-0 border-muted rounded-b-3xl bg-background">
|
||||
<ScrollArea className="h-full">
|
||||
<SubmissionsTable submissions={problem.submissions} />
|
||||
<SubmissionsTable locale={locale} submissions={problem.submissions} />
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
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";
|
||||
@ -59,6 +60,8 @@ export default async function ProblemLayout({
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const locale = await getUserLocale();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
<ProblemStoreProvider
|
||||
@ -71,6 +74,7 @@ export default async function ProblemLayout({
|
||||
<PlaygroundHeader />
|
||||
<main className="flex flex-grow overflow-y-hidden p-2.5 pt-0">
|
||||
<ProblemPage
|
||||
locale={locale}
|
||||
Description={Description}
|
||||
Solutions={Solutions}
|
||||
Submissions={Submissions}
|
||||
|
@ -8,10 +8,14 @@ import {
|
||||
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;
|
||||
@ -22,6 +26,7 @@ interface ProblemPageProps {
|
||||
}
|
||||
|
||||
export default function ProblemPage({
|
||||
locale,
|
||||
Description,
|
||||
Solutions,
|
||||
Submissions,
|
||||
@ -30,9 +35,17 @@ export default function ProblemPage({
|
||||
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={[
|
||||
@ -40,7 +53,7 @@ export default function ProblemPage({
|
||||
id: "Description",
|
||||
component: "Description",
|
||||
tabComponent: "Description",
|
||||
title: "Description",
|
||||
title: t("Description"),
|
||||
params: {
|
||||
icon: FileTextIcon,
|
||||
content: Description,
|
||||
@ -50,7 +63,7 @@ export default function ProblemPage({
|
||||
id: "Solutions",
|
||||
component: "Solutions",
|
||||
tabComponent: "Solutions",
|
||||
title: "Solutions",
|
||||
title: t("Solutions"),
|
||||
params: {
|
||||
icon: FlaskConicalIcon,
|
||||
content: Solutions,
|
||||
@ -65,7 +78,7 @@ export default function ProblemPage({
|
||||
id: "Submissions",
|
||||
component: "Submissions",
|
||||
tabComponent: "Submissions",
|
||||
title: "Submissions",
|
||||
title: t("Submissions"),
|
||||
params: {
|
||||
icon: CircleCheckBigIcon,
|
||||
content: Submissions,
|
||||
@ -80,7 +93,7 @@ export default function ProblemPage({
|
||||
id: "Details",
|
||||
component: "Details",
|
||||
tabComponent: "Details",
|
||||
title: "Details",
|
||||
title: t("Details"),
|
||||
params: {
|
||||
icon: CircleCheckBigIcon,
|
||||
content: Details,
|
||||
@ -91,7 +104,7 @@ export default function ProblemPage({
|
||||
id: "Code",
|
||||
component: "Code",
|
||||
tabComponent: "Code",
|
||||
title: "Code",
|
||||
title: t("Code"),
|
||||
params: {
|
||||
icon: SquarePenIcon,
|
||||
content: Code,
|
||||
@ -105,7 +118,7 @@ export default function ProblemPage({
|
||||
id: "Testcase",
|
||||
component: "Testcase",
|
||||
tabComponent: "Testcase",
|
||||
title: "Testcase",
|
||||
title: t("Testcase"),
|
||||
params: {
|
||||
icon: SquareCheckIcon,
|
||||
content: Testcase,
|
||||
@ -119,7 +132,7 @@ export default function ProblemPage({
|
||||
id: "Bot",
|
||||
component: "Bot",
|
||||
tabComponent: "Bot",
|
||||
title: "Bot",
|
||||
title: t("Bot"),
|
||||
params: {
|
||||
icon: BotIcon,
|
||||
content: Bot,
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
} 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";
|
||||
|
||||
@ -32,13 +33,15 @@ export default async function ProblemsetPage() {
|
||||
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();
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader className="bg-transparent">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="w-1/3">Status</TableHead>
|
||||
<TableHead className="w-1/3">Title</TableHead>
|
||||
<TableHead className="w-1/3">Difficulty</TableHead>
|
||||
<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">
|
||||
@ -60,7 +63,7 @@ export default async function ProblemsetPage() {
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className={`py-2.5 ${getDifficultyColorClass(problem.difficulty)}`}>
|
||||
{problem.difficulty}
|
||||
{t(`Difficulty.${problem.difficulty}`)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
@ -2,22 +2,25 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { CheckIcon, MinusIcon } from "lucide-react";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
|
||||
const items = [
|
||||
{ value: "system", label: "System", image: "/ui-system.png" },
|
||||
{ value: "light", label: "Light", image: "/ui-light.png" },
|
||||
{ value: "dark", label: "Dark", image: "/ui-dark.png" },
|
||||
];
|
||||
|
||||
export default function AppearanceSettings() {
|
||||
const t = useTranslations("AppearanceSettings");
|
||||
|
||||
const items = [
|
||||
{ value: "system", label: t("items.System"), image: "/ui-system.png" },
|
||||
{ value: "light", label: t("items.Light"), image: "/ui-light.png" },
|
||||
{ value: "dark", label: t("items.Dark"), image: "/ui-dark.png" },
|
||||
];
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<fieldset className="space-y-4">
|
||||
<legend className="text-foreground text-sm leading-none font-medium">
|
||||
Choose a theme
|
||||
{t("title")}
|
||||
</legend>
|
||||
<RadioGroup className="flex gap-3" defaultValue={theme}>
|
||||
{items.map((item) => (
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
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";
|
||||
@ -28,6 +29,7 @@ async function handleLogOut() {
|
||||
|
||||
export async function AvatarButton() {
|
||||
const session = await auth();
|
||||
const t = await getTranslations("AvatarButton");
|
||||
const isLoggedIn = !!session?.user;
|
||||
const image = session?.user?.image ?? "https://github.com/shadcn.png";
|
||||
const name = session?.user?.name ?? "unknown";
|
||||
@ -64,7 +66,7 @@ export async function AvatarButton() {
|
||||
<SettingsButton />
|
||||
<DropdownMenuItem onClick={handleLogOut}>
|
||||
<LogOut />
|
||||
Log out
|
||||
{t("LogOut")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
|
@ -1,5 +1,6 @@
|
||||
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";
|
||||
@ -14,6 +15,8 @@ export default function BackButton({
|
||||
className,
|
||||
...props
|
||||
}: BackButtonProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
@ -29,7 +32,9 @@ export default function BackButton({
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">Back</TooltipContent>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
{t("BackButton")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
|
||||
interface BannerProps {
|
||||
@ -11,9 +12,11 @@ interface BannerProps {
|
||||
export function Banner({
|
||||
className,
|
||||
link = siteConfig.url.repo.github,
|
||||
text = "Star this project if you like it.",
|
||||
text,
|
||||
...props
|
||||
}: BannerProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<header
|
||||
{...props}
|
||||
@ -25,7 +28,7 @@ export function Banner({
|
||||
<p className="flex justify-center text-sm">
|
||||
<a href={link} className="group">
|
||||
<span className="me-1 text-base leading-none">✨</span>
|
||||
{text}
|
||||
{text || t("Banner.Text")}
|
||||
<ArrowRightIcon
|
||||
className="ms-2 -mt-0.5 inline-flex opacity-60 transition-transform group-hover:translate-x-0.5"
|
||||
size={16}
|
||||
|
@ -8,11 +8,13 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
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";
|
||||
|
||||
export default function BotVisibilityToggle() {
|
||||
const { api } = useDockviewStore();
|
||||
const t = useTranslations();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isBotVisible, setBotVisible] = useState<boolean>(false);
|
||||
|
||||
@ -37,7 +39,7 @@ export default function BotVisibilityToggle() {
|
||||
id: "Bot",
|
||||
component: "Bot",
|
||||
tabComponent: "Bot",
|
||||
title: "Bot",
|
||||
title: t("ProblemPage.Bot"),
|
||||
position: {
|
||||
direction: "right",
|
||||
},
|
||||
@ -70,7 +72,7 @@ export default function BotVisibilityToggle() {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
<p>{isBotVisible ? "Close Bot" : "Open Bot"}</p>
|
||||
<p>{isBotVisible ? t("BotVisibilityToggle.close") : t("BotVisibilityToggle.open")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RunCode } from "@/components/run-code";
|
||||
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";
|
||||
|
||||
@ -8,10 +9,12 @@ interface PlaygroundHeaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PlaygroundHeader({
|
||||
export async function PlaygroundHeader({
|
||||
className,
|
||||
...props
|
||||
}: PlaygroundHeaderProps) {
|
||||
const session = await auth();
|
||||
|
||||
return (
|
||||
<header
|
||||
{...props}
|
||||
@ -33,7 +36,7 @@ export function PlaygroundHeader({
|
||||
<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">
|
||||
<RunCode className="bg-muted text-muted-foreground hover:bg-muted/50" />
|
||||
<RunCodeButton session={session} className="bg-muted text-muted-foreground hover:bg-muted/50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,12 +9,14 @@ import {
|
||||
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 {
|
||||
@ -65,7 +67,7 @@ export function CopyButton() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
Click to Copy
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
import { CircleXIcon, TriangleAlertIcon } from "lucide-react";
|
||||
|
||||
@ -20,6 +21,7 @@ export function WorkspaceEditorFooter({
|
||||
className,
|
||||
...props
|
||||
}: WorkspaceEditorFooterProps) {
|
||||
const t = useTranslations("WorkspaceEditorFooter");
|
||||
const { editor, markers } = useProblem();
|
||||
const [position, setPosition] = useState<{ lineNumber: number; column: number } | null>(null);
|
||||
|
||||
@ -62,8 +64,8 @@ export function WorkspaceEditorFooter({
|
||||
</div>
|
||||
<span className="truncate">
|
||||
{position
|
||||
? `Row ${position.lineNumber}, Column ${position.column}`
|
||||
: "Row -, Column -"}
|
||||
? `${t("Row")} ${position.lineNumber}, ${t("Column")} ${position.column}`
|
||||
: `${t("Row")} -, ${t("Column")} -`}
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -7,11 +7,13 @@ import {
|
||||
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}>
|
||||
@ -20,7 +22,7 @@ export function FormatButton() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Format Code"
|
||||
aria-label={t("TooltipContent")}
|
||||
onClick={() => {
|
||||
editor?.trigger("format", "editor.action.formatDocument", null);
|
||||
}}
|
||||
@ -31,7 +33,7 @@ export function FormatButton() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
Format Code
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
|
||||
@ -28,6 +29,7 @@ const getLspStatusColor = (webSocket: WebSocket | null) => {
|
||||
|
||||
export function LspStatusButton() {
|
||||
const { webSocket } = useProblem();
|
||||
const t = useTranslations("WorkspaceEditorHeader.LspStatusButton");
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
@ -48,7 +50,7 @@ export function LspStatusButton() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
Language Server
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
@ -7,11 +7,13 @@ import {
|
||||
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}>
|
||||
@ -20,7 +22,7 @@ export function RedoButton() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Redo Code"
|
||||
aria-label={t("TooltipContent")}
|
||||
onClick={() => {
|
||||
editor?.trigger("redo", "redo", null);
|
||||
}}
|
||||
@ -31,7 +33,7 @@ export function RedoButton() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
Redo Code
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
@ -7,11 +7,13 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { RotateCcw } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
|
||||
export function ResetButton() {
|
||||
const { editor, currentTemplate } = useProblem();
|
||||
const t = useTranslations("WorkspaceEditorHeader.ResetButton");
|
||||
|
||||
const handleReset = () => {
|
||||
if (editor) {
|
||||
@ -38,7 +40,7 @@ export function ResetButton() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Reset Code"
|
||||
aria-label={t("TooltipContent")}
|
||||
onClick={handleReset}
|
||||
disabled={!editor}
|
||||
className="h-6 w-6 px-1.5 py-0.5 border-none shadow-none hover:bg-muted"
|
||||
@ -47,7 +49,7 @@ export function ResetButton() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
Reset Code
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
@ -7,11 +7,13 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Undo2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
|
||||
export function UndoButton() {
|
||||
const { editor } = useProblem();
|
||||
const t = useTranslations("WorkspaceEditorHeader.UndoButton");
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
@ -20,7 +22,7 @@ export function UndoButton() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Undo Code"
|
||||
aria-label={t("TooltipContent")}
|
||||
onClick={() => {
|
||||
editor?.trigger("undo", "undo", null);
|
||||
}}
|
||||
@ -31,7 +33,7 @@ export function UndoButton() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
Undo Code
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
65
src/components/language-settings.tsx
Normal file
65
src/components/language-settings.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import Flag from "react-world-flags";
|
||||
import { Globe } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Locale, locales } from "@/config/i18n";
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { getUserLocale, setUserLocale } from "@/i18n/locale";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
export function LanguageSettings() {
|
||||
const t = useTranslations();
|
||||
const [selectedOption, setSelectedOption] = useState<Locale>();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLocale = async () => {
|
||||
const userLocale = await getUserLocale();
|
||||
if (!userLocale) return;
|
||||
setSelectedOption(userLocale);
|
||||
};
|
||||
fetchLocale();
|
||||
}, []);
|
||||
|
||||
const localeOptions = useMemo(() => {
|
||||
const options = locales.map((locale) => ({
|
||||
value: locale,
|
||||
label: `${t(`LanguageSettings.${locale}.name`)}`,
|
||||
}));
|
||||
return options.sort((a, b) => a.value.localeCompare(b.value));
|
||||
}, [t]);
|
||||
|
||||
const handleValueChange = (value: Locale) => {
|
||||
setSelectedOption(value);
|
||||
setUserLocale(value);
|
||||
};
|
||||
|
||||
const getIconForLocale = (locale: Locale) => {
|
||||
switch (locale) {
|
||||
case "en":
|
||||
return <Flag code="US" className="h-4 w-4 mr-2" />;
|
||||
case "zh":
|
||||
return <Flag code="CN" className="h-4 w-4 mr-2" />;
|
||||
default:
|
||||
return <Globe size={16} className="mr-2" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Select value={selectedOption} onValueChange={handleValueChange}>
|
||||
<SelectTrigger className="w-[200px] shadow-none focus:ring-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-[200px]">
|
||||
{localeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<div className="flex items-center">
|
||||
{getIconForLocale(option.value)}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { LogIn } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
@ -8,6 +9,7 @@ export default function LogInButton() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const t = useTranslations("AvatarButton");
|
||||
|
||||
const handleLogIn = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
@ -17,8 +19,8 @@ export default function LogInButton() {
|
||||
|
||||
return (
|
||||
<DropdownMenuItem onClick={handleLogIn}>
|
||||
<LogIn className="mr-2 h-4 w-4" />
|
||||
Log In
|
||||
<LogIn />
|
||||
{t("LogIn")}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
@ -8,24 +8,32 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { Session } from "next-auth";
|
||||
import { judge } from "@/actions/judge";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
import { useDockviewStore } from "@/stores/dockview";
|
||||
import { LoaderCircleIcon, PlayIcon } from "lucide-react";
|
||||
import { showStatusToast } from "@/hooks/show-status-toast";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
interface RunCodeProps {
|
||||
interface RunCodeButtonProps {
|
||||
className?: string;
|
||||
session: Session | null;
|
||||
}
|
||||
|
||||
export function RunCode({
|
||||
export function RunCodeButton({
|
||||
className,
|
||||
...props
|
||||
}: RunCodeProps) {
|
||||
session,
|
||||
}: RunCodeButtonProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { api } = useDockviewStore();
|
||||
const { currentLang, editor, problemId } = useProblem();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const t = useTranslations("PlaygroundHeader.RunCodeButton");
|
||||
|
||||
const handleJudge = async () => {
|
||||
if (!editor) return;
|
||||
@ -33,18 +41,24 @@ export function RunCode({
|
||||
const code = editor.getValue() || "";
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const result = await judge(currentLang, code, problemId);
|
||||
showStatusToast({ status: result.status });
|
||||
const panel = api?.getPanel("Submissions");
|
||||
if (panel && !panel.api.isActive) {
|
||||
panel.api.setActive();
|
||||
if (!session) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set("redirectTo", pathname);
|
||||
router.push(`/sign-in?${params.toString()}`);
|
||||
} else {
|
||||
try {
|
||||
const result = await judge(currentLang, code, problemId);
|
||||
showStatusToast({ status: result.status });
|
||||
const panel = api?.getPanel("Submissions");
|
||||
if (panel && !panel.api.isActive) {
|
||||
panel.api.setActive();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error occurred while judging the code:");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error occurred while judging the code:");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -53,7 +67,6 @@ export function RunCode({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
{...props}
|
||||
variant="secondary"
|
||||
className={cn("h-8 px-3 py-1.5", className)}
|
||||
onClick={handleJudge}
|
||||
@ -72,10 +85,12 @@ export function RunCode({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{isLoading ? "Running..." : "Run"}
|
||||
{isLoading ? t("TooltipTrigger.loading") : t("TooltipTrigger.ready")}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">Run Code</TooltipContent>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
@ -1,16 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { Settings } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useSettingsStore } from "@/stores/useSettingsStore";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function SettingsButton() {
|
||||
const t = useTranslations("AvatarButton");
|
||||
const { setDialogOpen } = useSettingsStore();
|
||||
|
||||
return (
|
||||
<DropdownMenuItem onClick={() => setDialogOpen(true)}>
|
||||
<Settings />
|
||||
Settings
|
||||
{t("Settings")}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
@ -25,29 +25,31 @@ import {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import AppearanceSettings from "./appearance-settings";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useSettingsStore } from "@/stores/useSettingsStore";
|
||||
import AppearanceSettings from "@/components/appearance-settings";
|
||||
import { LanguageSettings } from "@/components/language-settings";
|
||||
import { CodeXml, Globe, Paintbrush, Settings } from "lucide-react";
|
||||
|
||||
const data = {
|
||||
nav: [
|
||||
{ name: "Appearance", icon: Paintbrush },
|
||||
{ name: "Language & region", icon: Globe },
|
||||
{ name: "Code Editor", icon: CodeXml },
|
||||
{ name: "Advanced", icon: Settings },
|
||||
],
|
||||
};
|
||||
|
||||
export function SettingsDialog() {
|
||||
const t = useTranslations("SettingsDialog");
|
||||
const data = {
|
||||
nav: [
|
||||
{ id: "Appearance", name: t("nav.Appearance"), icon: Paintbrush },
|
||||
{ id: "Language", name: t("nav.Language"), icon: Globe },
|
||||
{ id: "CodeEditor", name: t("nav.CodeEditor"), icon: CodeXml },
|
||||
{ id: "Advanced", name: t("nav.Advanced"), icon: Settings },
|
||||
],
|
||||
};
|
||||
const { isDialogOpen, activeSetting, setDialogOpen, setActiveSetting } = useSettingsStore();
|
||||
|
||||
return (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="overflow-hidden p-0 md:max-h-[500px] md:max-w-[700px] lg:max-w-[800px]">
|
||||
<DialogTitle className="sr-only">Settings</DialogTitle>
|
||||
<DialogTitle className="sr-only">{t("title")}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Customize your settings here.
|
||||
{t("description")}
|
||||
</DialogDescription>
|
||||
<SidebarProvider className="items-start">
|
||||
<Sidebar collapsible="none" className="hidden md:flex">
|
||||
@ -59,10 +61,10 @@ export function SettingsDialog() {
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={item.name === activeSetting}
|
||||
onClick={() => setActiveSetting(item.name)}
|
||||
isActive={item.id === activeSetting}
|
||||
onClick={() => setActiveSetting(item.id)}
|
||||
>
|
||||
<a href="#">
|
||||
<a>
|
||||
<item.icon />
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
@ -80,11 +82,11 @@ export function SettingsDialog() {
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink href="#">Settings</BreadcrumbLink>
|
||||
<BreadcrumbLink>{t("breadcrumb")}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{activeSetting}</BreadcrumbPage>
|
||||
<BreadcrumbPage>{t(`nav.${activeSetting}`)}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
@ -92,16 +94,8 @@ export function SettingsDialog() {
|
||||
</header>
|
||||
<ScrollArea className="flex-1 overflow-y-auto p-4 pt-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
{activeSetting === "Appearance" ? (
|
||||
<AppearanceSettings />
|
||||
) : (
|
||||
Array.from({ length: 10 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="aspect-video max-w-3xl rounded-xl bg-muted/50"
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{activeSetting === "Appearance" && <AppearanceSettings />}
|
||||
{activeSetting === "Language" && <LanguageSettings />}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</main>
|
||||
|
@ -9,6 +9,9 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Locale } from "@/config/i18n";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { enUS, zhCN } from "date-fns/locale";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
import { Clock4Icon, CpuIcon } from "lucide-react";
|
||||
import { useDockviewStore } from "@/stores/dockview";
|
||||
@ -18,10 +21,23 @@ import { EditorLanguageIcons } from "@/config/editor-language-icons";
|
||||
import { formatDistanceToNow, isBefore, subDays, format } from "date-fns";
|
||||
|
||||
interface SubmissionsTableProps {
|
||||
locale: Locale;
|
||||
submissions: SubmissionWithTestcaseResult[];
|
||||
}
|
||||
|
||||
export default function SubmissionsTable({ submissions }: SubmissionsTableProps) {
|
||||
const getLocale = (locale: Locale) => {
|
||||
switch (locale) {
|
||||
case "zh":
|
||||
return zhCN;
|
||||
case "en":
|
||||
default:
|
||||
return enUS;
|
||||
}
|
||||
}
|
||||
|
||||
export default function SubmissionsTable({ locale, submissions }: SubmissionsTableProps) {
|
||||
const s = useTranslations("StatusMessage");
|
||||
const t = useTranslations("SubmissionsTable");
|
||||
const { editorLanguageConfigs } = useProblem();
|
||||
const { api, setSubmission } = useDockviewStore();
|
||||
|
||||
@ -41,7 +57,7 @@ export default function SubmissionsTable({ submissions }: SubmissionsTableProps)
|
||||
id: "Details",
|
||||
component: "Details",
|
||||
tabComponent: "Details",
|
||||
title: submission.status,
|
||||
title: s(`${submission.status}`),
|
||||
position: {
|
||||
referencePanel: "Submissions",
|
||||
direction: "within",
|
||||
@ -54,11 +70,11 @@ export default function SubmissionsTable({ submissions }: SubmissionsTableProps)
|
||||
<Table>
|
||||
<TableHeader className="bg-transparent">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="w-[100px]">Index</TableHead>
|
||||
<TableHead className="w-[170px]">Status</TableHead>
|
||||
<TableHead className="w-[100px]">Language</TableHead>
|
||||
<TableHead className="w-[100px]">Time</TableHead>
|
||||
<TableHead className="w-[100px]">Memory</TableHead>
|
||||
<TableHead className="w-[100px]">{t("Index")}</TableHead>
|
||||
<TableHead className="w-[170px]">{t("Status")}</TableHead>
|
||||
<TableHead className="w-[100px]">{t("Language")}</TableHead>
|
||||
<TableHead className="w-[100px]">{t("Time")}</TableHead>
|
||||
<TableHead className="w-[100px]">{t("Memory")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@ -68,11 +84,13 @@ export default function SubmissionsTable({ submissions }: SubmissionsTableProps)
|
||||
{sortedSubmissions.map((submission, index) => {
|
||||
const Icon = EditorLanguageIcons[submission.language];
|
||||
const createdAt = new Date(submission.createdAt);
|
||||
const localeInstance = getLocale(locale);
|
||||
const submittedDisplay = isBefore(createdAt, subDays(new Date(), 1))
|
||||
? format(createdAt, "yyyy-MM-dd")
|
||||
: formatDistanceToNow(createdAt, { addSuffix: true });
|
||||
: formatDistanceToNow(createdAt, { addSuffix: true, locale: localeInstance });
|
||||
|
||||
const isEven = (submissions.length - index) % 2 === 0;
|
||||
const message = statusMap.get(submission.status)?.message;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
@ -89,7 +107,7 @@ export default function SubmissionsTable({ submissions }: SubmissionsTableProps)
|
||||
<TableCell>
|
||||
<div className="flex flex-col truncate">
|
||||
<span className={getStatusColorClass(submission.status)}>
|
||||
{statusMap.get(submission.status)?.message}
|
||||
{s(`${message}`)}
|
||||
</span>
|
||||
<span className="text-xs">{submittedDisplay}</span>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Status } from "@/generated/client";
|
||||
import { getStatusColorClass, statusMap } from "@/lib/status";
|
||||
@ -17,34 +18,38 @@ const StatusToast = ({
|
||||
Icon: LucideIcon;
|
||||
message: string;
|
||||
colorClass: string;
|
||||
}) => (
|
||||
<div className="bg-background text-foreground w-full rounded-md border px-4 py-1 shadow-lg h-10 flex items-center">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex grow gap-3">
|
||||
<Icon
|
||||
className={`mt-0.5 shrink-0 ${colorClass}`}
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="flex grow justify-between gap-12">
|
||||
<p className="text-sm">{message}</p>
|
||||
}) => {
|
||||
const s = useTranslations("StatusMessage");
|
||||
|
||||
return (
|
||||
<div className="bg-background text-foreground w-full rounded-md border px-4 py-1 shadow-lg h-10 flex items-center">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex grow gap-3">
|
||||
<Icon
|
||||
className={`mt-0.5 shrink-0 ${colorClass}`}
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="flex grow justify-between gap-12">
|
||||
<p className="text-sm">{s(`${message}`)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="group -my-1.5 -me-2 size-8 shrink-0 p-0 hover:bg-transparent"
|
||||
onClick={() => toast.dismiss(t)}
|
||||
aria-label="Close sonner"
|
||||
>
|
||||
<XIcon
|
||||
size={16}
|
||||
className="opacity-60 transition-opacity group-hover:opacity-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="group -my-1.5 -me-2 size-8 shrink-0 p-0 hover:bg-transparent"
|
||||
onClick={() => toast.dismiss(t)}
|
||||
aria-label="Close sonner"
|
||||
>
|
||||
<XIcon
|
||||
size={16}
|
||||
className="opacity-60 transition-opacity group-hover:opacity-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
};
|
||||
|
||||
interface ShowStatusToastProps {
|
||||
status: Status;
|
||||
|
12
src/lib/i18n.ts
Normal file
12
src/lib/i18n.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Locale } from "@/config/i18n";
|
||||
import { enUS, zhCN } from "date-fns/locale";
|
||||
|
||||
export const getLocale = (locale: Locale) => {
|
||||
switch (locale) {
|
||||
case "zh":
|
||||
return zhCN;
|
||||
case "en":
|
||||
default:
|
||||
return enUS;
|
||||
}
|
||||
}
|
@ -25,16 +25,16 @@ export const getStatusColorClass = (status: Status) => {
|
||||
};
|
||||
|
||||
export const statusMap = new Map<Status, { icon: LucideIcon; message: string }>([
|
||||
["PD", { icon: AlertTriangleIcon, message: "Pending" }],
|
||||
["QD", { icon: AlertTriangleIcon, message: "Queued" }],
|
||||
["CP", { icon: AlertTriangleIcon, message: "Compiling" }],
|
||||
["CE", { icon: AlertTriangleIcon, message: "Compilation Error" }],
|
||||
["CS", { icon: CircleCheckIcon, message: "Compilation Success" }],
|
||||
["RU", { icon: AlertTriangleIcon, message: "Running" }],
|
||||
["TLE", { icon: AlertTriangleIcon, message: "Time Limit Exceeded" }],
|
||||
["MLE", { icon: AlertTriangleIcon, message: "Memory Limit Exceeded" }],
|
||||
["RE", { icon: AlertTriangleIcon, message: "Runtime Error" }],
|
||||
["AC", { icon: CircleCheckIcon, message: "Accepted" }],
|
||||
["WA", { icon: AlertTriangleIcon, message: "Wrong Answer" }],
|
||||
["SE", { icon: BanIcon, message: "System Error" }],
|
||||
["PD", { icon: AlertTriangleIcon, message: "PD" }],
|
||||
["QD", { icon: AlertTriangleIcon, message: "QD" }],
|
||||
["CP", { icon: AlertTriangleIcon, message: "CP" }],
|
||||
["CE", { icon: AlertTriangleIcon, message: "CE" }],
|
||||
["CS", { icon: CircleCheckIcon, message: "CS" }],
|
||||
["RU", { icon: AlertTriangleIcon, message: "RU" }],
|
||||
["TLE", { icon: AlertTriangleIcon, message: "TLE" }],
|
||||
["MLE", { icon: AlertTriangleIcon, message: "MLE" }],
|
||||
["RE", { icon: AlertTriangleIcon, message: "RE" }],
|
||||
["AC", { icon: CircleCheckIcon, message: "AC" }],
|
||||
["WA", { icon: AlertTriangleIcon, message: "WA" }],
|
||||
["SE", { icon: BanIcon, message: "SE" }],
|
||||
]);
|
||||
|
Loading…
Reference in New Issue
Block a user