mirror of
https://github.com/massbug/judge4c.git
synced 2025-05-17 23:12:23 +00:00
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
This commit is contained in:
parent
6c9351ccd2
commit
3417d2ee49
@ -1,75 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Check, Copy } from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useProblem } from "@/hooks/use-problem";
|
|
||||||
|
|
||||||
export function CopyButton() {
|
|
||||||
const { editor } = useProblem();
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const t = useTranslations("WorkspaceEditorHeader.CopyButton");
|
|
||||||
|
|
||||||
const handleCopy = async () => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(editor?.getValue() || "");
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 1500);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to copy text: ", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className={cn(
|
|
||||||
"h-6 w-6 border-none shadow-none px-1.5 py-0.5 hover:bg-muted",
|
|
||||||
copied ? "disabled:opacity-100" : ""
|
|
||||||
)}
|
|
||||||
onClick={handleCopy}
|
|
||||||
aria-label={copied ? "Copied" : "Copy to clipboard"}
|
|
||||||
disabled={!editor || copied}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"transition-all",
|
|
||||||
copied ? "scale-100 opacity-100" : "scale-0 opacity-0"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className="stroke-emerald-500"
|
|
||||||
size={16}
|
|
||||||
strokeWidth={2}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"absolute transition-all",
|
|
||||||
copied ? "scale-0 opacity-0" : "scale-100 opacity-100"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Copy size={16} strokeWidth={2} aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="px-2 py-1 text-xs">
|
|
||||||
{t("TooltipContent")}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { Paintbrush } from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useProblem } from "@/hooks/use-problem";
|
|
||||||
|
|
||||||
export function FormatButton() {
|
|
||||||
const { editor } = useProblem();
|
|
||||||
const t = useTranslations("WorkspaceEditorHeader.FormatButton");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
aria-label={t("TooltipContent")}
|
|
||||||
onClick={() => {
|
|
||||||
editor?.trigger("format", "editor.action.formatDocument", null);
|
|
||||||
}}
|
|
||||||
className="h-6 w-6 px-1.5 py-0.5 border-none shadow-none hover:bg-muted"
|
|
||||||
disabled={!editor}
|
|
||||||
>
|
|
||||||
<Paintbrush size={16} strokeWidth={2} aria-hidden="true" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="px-2 py-1 text-xs">
|
|
||||||
{t("TooltipContent")}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
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) {
|
|
||||||
const model = editor.getModel();
|
|
||||||
if (model) {
|
|
||||||
const fullRange = model.getFullModelRange();
|
|
||||||
editor.pushUndoStop();
|
|
||||||
editor.executeEdits("reset-code", [
|
|
||||||
{
|
|
||||||
range: fullRange,
|
|
||||||
text: currentTemplate,
|
|
||||||
forceMoveMarkers: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
editor.pushUndoStop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<RotateCcw size={16} strokeWidth={2} aria-hidden="true" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="px-2 py-1 text-xs">
|
|
||||||
{t("TooltipContent")}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { 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}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
aria-label={t("TooltipContent")}
|
|
||||||
onClick={() => {
|
|
||||||
editor?.trigger("undo", "undo", null);
|
|
||||||
}}
|
|
||||||
disabled={!editor}
|
|
||||||
className="h-6 w-6 px-1.5 py-0.5 border-none shadow-none hover:bg-muted"
|
|
||||||
>
|
|
||||||
<Undo2 size={16} strokeWidth={2} aria-hidden="true" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="px-2 py-1 text-xs">
|
|
||||||
{t("TooltipContent")}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Check, Copy } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { TooltipButton } from "@/components/tooltip-button";
|
||||||
|
import { useProblemEditorActions } from "@/features/problems/code/hooks/use-problem-editor-actions";
|
||||||
|
|
||||||
|
const CopyButton = () => {
|
||||||
|
const t = useTranslations("WorkspaceEditorHeader.CopyButton");
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const { canExecute, handleCopy } = useProblemEditorActions();
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
const success = await handleCopy();
|
||||||
|
if (success) {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipButton
|
||||||
|
tooltipContent={t("TooltipContent")}
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-label={copied ? "Copied" : "Copy to clipboard"}
|
||||||
|
disabled={!canExecute || copied}
|
||||||
|
className={copied ? "disabled:opacity-100" : undefined}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</TooltipButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CopyButton };
|
@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Paintbrush } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { TooltipButton } from "@/components/tooltip-button";
|
||||||
|
import { useProblemEditorActions } from "@/features/problems/code/hooks/use-problem-editor-actions";
|
||||||
|
|
||||||
|
const FormatButton = () => {
|
||||||
|
const t = useTranslations("WorkspaceEditorHeader.FormatButton");
|
||||||
|
const { canExecute, handleFormat } = useProblemEditorActions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipButton
|
||||||
|
onClick={handleFormat}
|
||||||
|
aria-label={t("TooltipContent")}
|
||||||
|
disabled={!canExecute}
|
||||||
|
tooltipContent={t("TooltipContent")}
|
||||||
|
>
|
||||||
|
<Paintbrush size={16} strokeWidth={2} aria-hidden="true" />
|
||||||
|
</TooltipButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { FormatButton };
|
@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Redo2 } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { TooltipButton } from "@/components/tooltip-button";
|
||||||
|
import { useProblemEditorActions } from "@/features/problems/code/hooks/use-problem-editor-actions";
|
||||||
|
|
||||||
|
const RedoButton = () => {
|
||||||
|
const t = useTranslations("WorkspaceEditorHeader.RedoButton");
|
||||||
|
const { canExecute, handleRedo } = useProblemEditorActions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipButton
|
||||||
|
tooltipContent={t("TooltipContent")}
|
||||||
|
onClick={handleRedo}
|
||||||
|
aria-label={t("TooltipContent")}
|
||||||
|
disabled={!canExecute}
|
||||||
|
>
|
||||||
|
<Redo2 size={16} strokeWidth={2} aria-hidden="true" />
|
||||||
|
</TooltipButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { RedoButton };
|
@ -0,0 +1,43 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { RotateCcw } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import type { Template } from "@/generated/client";
|
||||||
|
import { TooltipButton } from "@/components/tooltip-button";
|
||||||
|
import { useProblemEditorStore } from "@/stores/problem-editor-store";
|
||||||
|
import { useProblemEditorActions } from "@/features/problems/code/hooks/use-problem-editor-actions";
|
||||||
|
|
||||||
|
interface ResetButtonProps {
|
||||||
|
templates: Template[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResetButton = ({ templates }: ResetButtonProps) => {
|
||||||
|
const t = useTranslations("WorkspaceEditorHeader.ResetButton");
|
||||||
|
const { language } = useProblemEditorStore();
|
||||||
|
const { canExecute, handleReset } = useProblemEditorActions();
|
||||||
|
|
||||||
|
const currentTemplate = useMemo(() => {
|
||||||
|
return (
|
||||||
|
templates.find((template) => template.language === language)?.template ??
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}, [language, templates]);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
handleReset(currentTemplate);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipButton
|
||||||
|
tooltipContent={t("TooltipContent")}
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-label={t("TooltipContent")}
|
||||||
|
disabled={!canExecute}
|
||||||
|
>
|
||||||
|
<RotateCcw size={16} strokeWidth={2} aria-hidden="true" />
|
||||||
|
</TooltipButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ResetButton };
|
@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Undo2 } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { TooltipButton } from "@/components/tooltip-button";
|
||||||
|
import { useProblemEditorActions } from "@/features/problems/code/hooks/use-problem-editor-actions";
|
||||||
|
|
||||||
|
const UndoButton = () => {
|
||||||
|
const t = useTranslations("WorkspaceEditorHeader.UndoButton");
|
||||||
|
const { canExecute, handleUndo } = useProblemEditorActions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipButton
|
||||||
|
tooltipContent={t("TooltipContent")}
|
||||||
|
onClick={handleUndo}
|
||||||
|
aria-label={t("TooltipContent")}
|
||||||
|
disabled={!canExecute}
|
||||||
|
>
|
||||||
|
<Undo2 size={16} strokeWidth={2} aria-hidden="true" />
|
||||||
|
</TooltipButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UndoButton };
|
5
src/features/problems/code/components/toolbar/index.ts
Normal file
5
src/features/problems/code/components/toolbar/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./actions/copy-button";
|
||||||
|
export * from "./actions/format-button";
|
||||||
|
export * from "./actions/redo-button";
|
||||||
|
export * from "./actions/reset-button";
|
||||||
|
export * from "./actions/undo-button";
|
@ -0,0 +1,55 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useProblemEditorStore } from "@/stores/problem-editor-store";
|
||||||
|
|
||||||
|
export const useProblemEditorActions = () => {
|
||||||
|
const { editor } = useProblemEditorStore();
|
||||||
|
|
||||||
|
const handleCopy = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(editor?.getValue() || "");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to copy text: ", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const handleFormat = useCallback(() => {
|
||||||
|
editor?.trigger("format", "editor.action.formatDocument", null);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const handleUndo = useCallback(() => {
|
||||||
|
editor?.trigger("undo", "undo", null);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const handleRedo = useCallback(() => {
|
||||||
|
editor?.trigger("redo", "redo", null);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const handleReset = useCallback((template: string) => {
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
const model = editor.getModel();
|
||||||
|
if (!model) return;
|
||||||
|
|
||||||
|
const fullRange = model.getFullModelRange();
|
||||||
|
editor.pushUndoStop();
|
||||||
|
editor.executeEdits("reset-code", [
|
||||||
|
{
|
||||||
|
range: fullRange,
|
||||||
|
text: template,
|
||||||
|
forceMoveMarkers: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
editor.pushUndoStop();
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleCopy,
|
||||||
|
handleFormat,
|
||||||
|
handleUndo,
|
||||||
|
handleRedo,
|
||||||
|
handleReset,
|
||||||
|
canExecute: !!editor,
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user