mirror of
https://github.com/massbug/judge4c.git
synced 2025-05-18 07:16:34 +00:00
feat(workspace): add editor layout with header and editor page
This commit is contained in:
parent
a14087b395
commit
2ffb366c7c
@ -1,3 +1,4 @@
|
|||||||
|
import WorkspaceEditorHeader from "@/features/playground/workspace/editor/header";
|
||||||
import WorkspaceEditorFooter from "@/features/playground/workspace/editor/footer";
|
import WorkspaceEditorFooter from "@/features/playground/workspace/editor/footer";
|
||||||
|
|
||||||
interface WorkspaceEditorLayoutProps {
|
interface WorkspaceEditorLayoutProps {
|
||||||
@ -9,6 +10,7 @@ export default function WorkspaceEditorLayout({
|
|||||||
}: WorkspaceEditorLayoutProps) {
|
}: WorkspaceEditorLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
|
<WorkspaceEditorHeader />
|
||||||
<div className="flex-1">{children}</div>
|
<div className="flex-1">{children}</div>
|
||||||
<WorkspaceEditorFooter />
|
<WorkspaceEditorFooter />
|
||||||
</div>
|
</div>
|
||||||
|
5
src/app/(app)/problems/[id]/@workspace/@editor/page.tsx
Normal file
5
src/app/(app)/problems/[id]/@workspace/@editor/page.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import CodeEditor from "@/components/code-editor";
|
||||||
|
|
||||||
|
export default function WorkspaceEditorPage() {
|
||||||
|
return <CodeEditor />;
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
"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 { Button } from "@/components/ui/button";
|
||||||
|
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
|
||||||
|
|
||||||
|
export default function CopyButton() {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const { editor } = useCodeEditorStore();
|
||||||
|
|
||||||
|
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="h-6 w-6 border-none px-1.5 py-0.5 disabled:opacity-100 hover:bg-muted"
|
||||||
|
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"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
Click to Copy
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Paintbrush } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
|
||||||
|
|
||||||
|
export default function FormatButton() {
|
||||||
|
const { editor } = useCodeEditorStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Format Code"
|
||||||
|
onClick={() => {
|
||||||
|
editor?.trigger("format", "editor.action.formatDocument", null);
|
||||||
|
}}
|
||||||
|
className="h-6 w-6 px-1.5 py-0.5 border-none hover:bg-muted"
|
||||||
|
>
|
||||||
|
<Paintbrush size={16} strokeWidth={2} aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="px-2 py-1 text-xs">
|
||||||
|
Format Code
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { getPath } from "@/lib/utils";
|
||||||
|
import { EditorLanguage } from "@prisma/client";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import LanguageServerConfig from "@/config/language-server";
|
||||||
|
import { EditorLanguageConfig } from "@/config/editor-language";
|
||||||
|
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
|
||||||
|
|
||||||
|
export default function LanguageSelector() {
|
||||||
|
const { hydrated, language, setLanguage, setPath, setLspConfig } = useCodeEditorStore();
|
||||||
|
|
||||||
|
if (!hydrated) {
|
||||||
|
return <Skeleton className="h-6 w-16 rounded-2xl" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValueChange = (lang: EditorLanguage) => {
|
||||||
|
setLanguage(lang);
|
||||||
|
setPath(getPath(lang));
|
||||||
|
setLspConfig(LanguageServerConfig[lang]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select value={language} onValueChange={handleValueChange}>
|
||||||
|
<SelectTrigger className="h-6 px-1.5 py-0.5 border-none focus:ring-0 hover:bg-muted [&>span]:flex [&>span]:items-center [&>span]:gap-2 [&>span_svg]:shrink-0 [&>span_svg]:text-muted-foreground/80">
|
||||||
|
<SelectValue placeholder="Select language" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="[&_*[role=option]>span>svg]:shrink-0 [&_*[role=option]>span>svg]:text-muted-foreground/80 [&_*[role=option]>span]:end-2 [&_*[role=option]>span]:start-auto [&_*[role=option]>span]:flex [&_*[role=option]>span]:items-center [&_*[role=option]>span]:gap-2 [&_*[role=option]]:pe-8 [&_*[role=option]]:ps-2">
|
||||||
|
{Object.values(EditorLanguageConfig).map((langConfig) => (
|
||||||
|
<SelectItem key={langConfig.id} value={langConfig.id}>
|
||||||
|
<langConfig.icon size={16} aria-hidden="true" />
|
||||||
|
<span className="truncate text-sm font-semibold mr-2">
|
||||||
|
{langConfig.label}
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Redo2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
|
||||||
|
|
||||||
|
export default function RedoButton() {
|
||||||
|
const { editor } = useCodeEditorStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Redo Code"
|
||||||
|
onClick={() => {
|
||||||
|
editor?.trigger("redo", "redo", null);
|
||||||
|
}}
|
||||||
|
className="h-6 w-6 px-1.5 py-0.5 border-none hover:bg-muted"
|
||||||
|
>
|
||||||
|
<Redo2 size={16} strokeWidth={2} aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="px-2 py-1 text-xs">
|
||||||
|
Redo Code
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { RotateCcw } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
|
||||||
|
import { TEMP_DEFAULT_EDITOR_VALUE } from "@/config/problem/value";
|
||||||
|
|
||||||
|
export default function ResetButton() {
|
||||||
|
const { editor, language } = useCodeEditorStore();
|
||||||
|
return (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Reset Code"
|
||||||
|
onClick={() => {
|
||||||
|
if (editor) {
|
||||||
|
const value = TEMP_DEFAULT_EDITOR_VALUE[language];
|
||||||
|
const model = editor.getModel();
|
||||||
|
if (model) {
|
||||||
|
const fullRange = model.getFullModelRange();
|
||||||
|
editor.executeEdits("reset-code", [
|
||||||
|
{
|
||||||
|
range: fullRange,
|
||||||
|
text: value,
|
||||||
|
forceMoveMarkers: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-6 w-6 px-1.5 py-0.5 border-none hover:bg-muted"
|
||||||
|
>
|
||||||
|
<RotateCcw size={16} strokeWidth={2} aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="px-2 py-1 text-xs">
|
||||||
|
Reset Code
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Undo2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
|
||||||
|
|
||||||
|
export default function UndoButton() {
|
||||||
|
const { editor } = useCodeEditorStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Undo Code"
|
||||||
|
onClick={() => {
|
||||||
|
editor?.trigger("undo", "undo", null);
|
||||||
|
}}
|
||||||
|
className="h-6 w-6 px-1.5 py-0.5 border-none hover:bg-muted"
|
||||||
|
>
|
||||||
|
<Undo2 size={16} strokeWidth={2} aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="px-2 py-1 text-xs">
|
||||||
|
Undo Code
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
36
src/features/playground/workspace/editor/header.tsx
Normal file
36
src/features/playground/workspace/editor/header.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import CopyButton from "./components/copy-button";
|
||||||
|
import RedoButton from "./components/redo-button";
|
||||||
|
import UndoButton from "./components/undo-button";
|
||||||
|
import ResetButton from "./components/reset-button";
|
||||||
|
import FormatButton from "./components/format-button";
|
||||||
|
import LanguageSelector from "./components/language-selector";
|
||||||
|
|
||||||
|
interface WorkspaceEditorHeaderProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WorkspaceEditorHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: WorkspaceEditorHeaderProps) {
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
{...props}
|
||||||
|
className={cn("h-8 flex flex-none items-center px-2 border-b", className)}
|
||||||
|
>
|
||||||
|
<div className="w-full flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<LanguageSelector />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<ResetButton />
|
||||||
|
<UndoButton />
|
||||||
|
<RedoButton />
|
||||||
|
<FormatButton />
|
||||||
|
<CopyButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user