bổ sung chức năng eraser

This commit is contained in:
2026-04-01 09:25:31 +07:00
parent 79eea344e9
commit 89db1c7b5c
+60 -52
View File
@@ -8,7 +8,7 @@ import { api } from "@/lib/api";
// Types
// ─────────────────────────────────────────────────────────────────────────────
type Tool = "select" | "pen" | "highlighter" | "text";
type Tool = "select" | "pen" | "highlighter" | "text" | "eraser";
type ViewMode = "pan" | "draw";
type FitMode = "width" | "height";
type ScrollMode = "single" | "continuous";
@@ -565,6 +565,18 @@ export default function WorkbookViewer({ pdfId }: Props) {
fc.renderAll();
});
break;
case "eraser":
fc.isDrawingMode = false;
fc.selection = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fc.on("mouse:down", (options: any) => {
if (!options.target) return;
fc.remove(options.target);
addedObjects.current = addedObjects.current.filter(o => o !== options.target);
fc.renderAll();
});
break;
}
}, [isReady, mode, tool, penColor, strokeWidth]);
@@ -602,6 +614,12 @@ export default function WorkbookViewer({ pdfId }: Props) {
const t = new fabric.IText("Text", { left: pointer.x, top: pointer.y, fontSize: 20, fill: penColorRef.current, fontFamily: "Arial, sans-serif", padding: 4 });
fc.add(t); fc.setActiveObject(t); t.enterEditing(); t.selectAll(); fc.renderAll();
}); break;
case "eraser":
fc.isDrawingMode = false; fc.selection = false;
fc.on("mouse:down", (options: any) => {
if (!options.target) return;
fc.remove(options.target); fc.renderAll();
}); break;
}
});
}, [isReady, scrollMode, mode, tool, penColor, strokeWidth]);
@@ -700,45 +718,45 @@ export default function WorkbookViewer({ pdfId }: Props) {
{/* ── Sticky Toolbar ──────────────────────────────────────────────────── */}
<header className="bg-white border-b shadow-sm sticky top-0 z-20">
<div className="max-w-5xl mx-auto px-3 py-2 flex flex-wrap items-center gap-2">
<div className="px-3 py-2 flex items-center gap-0">
{/* Scrollable tools strip */}
<div className="flex items-center gap-2 overflow-x-auto flex-1 min-w-0">
{/* Back button + title */}
<button
onClick={() => router.push("/dashboard")}
title="Back"
className="p-1 text-gray-500 hover:text-blue-600 transition rounded"
className="p-1 text-gray-500 hover:text-blue-600 transition rounded flex-shrink-0"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<span className="text-sm font-semibold text-gray-800 truncate max-w-[130px] sm:max-w-xs">
<span className="text-sm font-semibold text-gray-800 truncate max-w-[100px] flex-shrink-0">
{pdfTitle}
</span>
<div className="h-5 w-px bg-gray-200 hidden sm:block" />
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Thumbnail panel toggle */}
{/* Thumbnail toggle */}
<button
onClick={() => setShowThumbnails(v => !v)}
title="Thumbnails"
className={`p-1.5 rounded-lg transition flex items-center gap-1 ${
showThumbnails
? "bg-blue-50 text-blue-600"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-800"
className={`p-1.5 rounded-lg transition flex items-center gap-1 flex-shrink-0 ${
showThumbnails ? "bg-blue-50 text-blue-600" : "text-gray-500 hover:bg-gray-100 hover:text-gray-800"
}`}
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
</svg>
<span className="text-xs font-medium hidden sm:inline">Thumb</span>
</button>
<div className="h-5 w-px bg-gray-200 hidden sm:block" />
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Fit mode buttons */}
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5">
{/* Fit mode */}
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
<button
onClick={() => setFitMode("width")}
title="Fit Width"
@@ -749,7 +767,7 @@ export default function WorkbookViewer({ pdfId }: Props) {
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h8M8 12h8M4 17h16" />
</svg>
<span className="hidden sm:inline">Width</span>
<span className="hidden lg:inline">Width</span>
</button>
<button
onClick={() => setFitMode("height")}
@@ -761,14 +779,14 @@ export default function WorkbookViewer({ pdfId }: Props) {
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8V4m0 0H3m4 0l-4 4M17 8V4m0 0h4m-4 0l4 4M7 16v4m0 0H3m4 0l-4-4M17 16v4m0 0h4m-4 0l4-4" />
</svg>
<span className="hidden sm:inline">Height</span>
<span className="hidden lg:inline">Height</span>
</button>
</div>
<div className="h-5 w-px bg-gray-200 hidden sm:block" />
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Scroll mode toggle */}
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5">
{/* Scroll mode */}
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
<button
onClick={() => setScrollMode("single")}
title="Single page"
@@ -779,7 +797,7 @@ export default function WorkbookViewer({ pdfId }: Props) {
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6M9 8h6M9 16h6M5 4h14a1 1 0 011 1v14a1 1 0 01-1 1H5a1 1 0 01-1-1V5a1 1 0 011-1z" />
</svg>
<span className="hidden sm:inline">Single</span>
<span className="hidden lg:inline">Single</span>
</button>
<button
onClick={() => setScrollMode("continuous")}
@@ -791,14 +809,14 @@ export default function WorkbookViewer({ pdfId }: Props) {
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
<span className="hidden sm:inline">Continuous</span>
<span className="hidden lg:inline">Continuous</span>
</button>
</div>
<div className="h-5 w-px bg-gray-200 hidden sm:block" />
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Page navigation */}
<div className="flex items-center gap-0.5">
<div className="flex items-center gap-0.5 flex-shrink-0">
<button
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage <= 1 || rendering || !isReady}
@@ -840,10 +858,10 @@ export default function WorkbookViewer({ pdfId }: Props) {
</button>
</div>
<div className="h-5 w-px bg-gray-200 hidden sm:block" />
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Pan / Draw mode toggle */}
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5">
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
<button
onClick={() => setViewMode("pan")}
className={`text-xs px-2.5 py-1 rounded-md font-medium transition ${
@@ -862,65 +880,57 @@ export default function WorkbookViewer({ pdfId }: Props) {
</button>
</div>
{/* Drawing tools — only shown in Draw mode */}
{/* Drawing tools — inline, visible only in draw mode */}
{mode === "draw" && (
<>
<div className="h-5 w-px bg-gray-200" />
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Tool buttons */}
<div className="flex items-center gap-0.5">
{/* Select */}
<div className="flex items-center gap-0.5 flex-shrink-0">
<ToolBtn id="select" current={tool} onClick={setTool} title="Select / Move">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
</ToolBtn>
{/* Pen */}
<ToolBtn id="pen" current={tool} onClick={setTool} title="Pen">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</ToolBtn>
{/* Highlighter */}
<ToolBtn id="highlighter" current={tool} onClick={setTool} title="Highlighter">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</ToolBtn>
{/* Text */}
<ToolBtn id="text" current={tool} onClick={setTool} title="Text">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 12h6m-3-3v6M7 20h10a2 2 0 002-2V6a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z" />
</ToolBtn>
<ToolBtn id="eraser" current={tool} onClick={setTool} title="Eraser">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M19 7l-1.5 1.5M4.5 19.5l9-9m0 0L9 6l-4.5 4.5 4.5 4.5m4.5-4.5L18 6" />
</ToolBtn>
</div>
{/* Color picker */}
<input
type="color"
value={penColor}
onChange={(e) => setPenColor(e.target.value)}
title="Color"
className="w-7 h-7 rounded cursor-pointer border border-gray-300 p-0.5 bg-white"
className="w-7 h-7 rounded cursor-pointer border border-gray-300 p-0.5 bg-white flex-shrink-0"
/>
{/* Stroke width slider */}
<input
type="range"
min={1}
max={12}
value={strokeWidth}
onChange={(e) => setStrokeWidth(Number(e.target.value))}
className="w-16 accent-blue-600"
className="w-20 accent-blue-600 flex-shrink-0"
title={`Stroke width: ${strokeWidth}`}
/>
<div className="h-5 w-px bg-gray-200" />
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
{/* Undo */}
<button
onClick={handleUndo}
title="Undo last stroke"
className="p-1.5 text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition"
className="p-1.5 text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition flex-shrink-0"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
@@ -928,11 +938,10 @@ export default function WorkbookViewer({ pdfId }: Props) {
</svg>
</button>
{/* Clear page */}
<button
onClick={handleClear}
title="Clear page annotations"
className="p-1.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition"
className="p-1.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition flex-shrink-0"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
@@ -941,13 +950,12 @@ export default function WorkbookViewer({ pdfId }: Props) {
</button>
</>
)}
</div>
{/* Save button — far right */}
<div className="ml-auto flex items-center gap-2">
{/* Save — fixed right, never scrolls away */}
<div className="flex items-center gap-2 pl-3 flex-shrink-0">
{saveNotice && (
<span className="text-xs text-green-600 font-medium animate-pulse">
Saved
</span>
<span className="text-xs text-green-600 font-medium animate-pulse"> Saved</span>
)}
<button
onClick={handleSave}