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:
cfngc4594 2025-05-07 13:42:08 +08:00
parent 6c9351ccd2
commit 3417d2ee49
12 changed files with 231 additions and 255 deletions

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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";

View File

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