mirror of
https://git.victorphan.net/basketballcantho/ten-project.git
synced 2026-08-05 10:43:11 +07:00
bổ sung chức năng eraser
This commit is contained in:
@@ -8,7 +8,7 @@ import { api } from "@/lib/api";
|
|||||||
// Types
|
// Types
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type Tool = "select" | "pen" | "highlighter" | "text";
|
type Tool = "select" | "pen" | "highlighter" | "text" | "eraser";
|
||||||
type ViewMode = "pan" | "draw";
|
type ViewMode = "pan" | "draw";
|
||||||
type FitMode = "width" | "height";
|
type FitMode = "width" | "height";
|
||||||
type ScrollMode = "single" | "continuous";
|
type ScrollMode = "single" | "continuous";
|
||||||
@@ -565,6 +565,18 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
|||||||
fc.renderAll();
|
fc.renderAll();
|
||||||
});
|
});
|
||||||
break;
|
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]);
|
}, [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 });
|
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();
|
fc.add(t); fc.setActiveObject(t); t.enterEditing(); t.selectAll(); fc.renderAll();
|
||||||
}); break;
|
}); 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]);
|
}, [isReady, scrollMode, mode, tool, penColor, strokeWidth]);
|
||||||
@@ -700,45 +718,45 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
|||||||
|
|
||||||
{/* ── Sticky Toolbar ──────────────────────────────────────────────────── */}
|
{/* ── Sticky Toolbar ──────────────────────────────────────────────────── */}
|
||||||
<header className="bg-white border-b shadow-sm sticky top-0 z-20">
|
<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 */}
|
{/* Back button + title */}
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push("/dashboard")}
|
onClick={() => router.push("/dashboard")}
|
||||||
title="Back"
|
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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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}
|
{pdfTitle}
|
||||||
</span>
|
</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
|
<button
|
||||||
onClick={() => setShowThumbnails(v => !v)}
|
onClick={() => setShowThumbnails(v => !v)}
|
||||||
title="Thumbnails"
|
title="Thumbnails"
|
||||||
className={`p-1.5 rounded-lg transition flex items-center gap-1 ${
|
className={`p-1.5 rounded-lg transition flex items-center gap-1 flex-shrink-0 ${
|
||||||
showThumbnails
|
showThumbnails ? "bg-blue-50 text-blue-600" : "text-gray-500 hover:bg-gray-100 hover:text-gray-800"
|
||||||
? "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">
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
<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" />
|
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>
|
</svg>
|
||||||
<span className="text-xs font-medium hidden sm:inline">Thumb</span>
|
|
||||||
</button>
|
</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 */}
|
{/* Fit mode */}
|
||||||
<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
|
<button
|
||||||
onClick={() => setFitMode("width")}
|
onClick={() => setFitMode("width")}
|
||||||
title="Fit 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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h8M8 12h8M4 17h16" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="hidden sm:inline">Width</span>
|
<span className="hidden lg:inline">Width</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setFitMode("height")}
|
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">
|
<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" />
|
<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>
|
</svg>
|
||||||
<span className="hidden sm:inline">Height</span>
|
<span className="hidden lg:inline">Height</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Scroll mode */}
|
||||||
<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
|
<button
|
||||||
onClick={() => setScrollMode("single")}
|
onClick={() => setScrollMode("single")}
|
||||||
title="Single page"
|
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">
|
<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" />
|
<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>
|
</svg>
|
||||||
<span className="hidden sm:inline">Single</span>
|
<span className="hidden lg:inline">Single</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setScrollMode("continuous")}
|
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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="hidden sm:inline">Continuous</span>
|
<span className="hidden lg:inline">Continuous</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Page navigation */}
|
||||||
<div className="flex items-center gap-0.5">
|
<div className="flex items-center gap-0.5 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => goToPage(currentPage - 1)}
|
onClick={() => goToPage(currentPage - 1)}
|
||||||
disabled={currentPage <= 1 || rendering || !isReady}
|
disabled={currentPage <= 1 || rendering || !isReady}
|
||||||
@@ -840,10 +858,10 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* 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
|
<button
|
||||||
onClick={() => setViewMode("pan")}
|
onClick={() => setViewMode("pan")}
|
||||||
className={`text-xs px-2.5 py-1 rounded-md font-medium transition ${
|
className={`text-xs px-2.5 py-1 rounded-md font-medium transition ${
|
||||||
@@ -862,65 +880,57 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Drawing tools — only shown in Draw mode */}
|
{/* Drawing tools — inline, visible only in draw mode */}
|
||||||
{mode === "draw" && (
|
{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 flex-shrink-0">
|
||||||
<div className="flex items-center gap-0.5">
|
|
||||||
{/* Select */}
|
|
||||||
<ToolBtn id="select" current={tool} onClick={setTool} title="Select / Move">
|
<ToolBtn id="select" current={tool} onClick={setTool} title="Select / Move">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
|
||||||
d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
|
|
||||||
</ToolBtn>
|
</ToolBtn>
|
||||||
|
|
||||||
{/* Pen */}
|
|
||||||
<ToolBtn id="pen" current={tool} onClick={setTool} title="Pen">
|
<ToolBtn id="pen" current={tool} onClick={setTool} title="Pen">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
<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" />
|
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>
|
</ToolBtn>
|
||||||
|
|
||||||
{/* Highlighter */}
|
|
||||||
<ToolBtn id="highlighter" current={tool} onClick={setTool} title="Highlighter">
|
<ToolBtn id="highlighter" current={tool} onClick={setTool} title="Highlighter">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
<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" />
|
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>
|
</ToolBtn>
|
||||||
|
|
||||||
{/* Text */}
|
|
||||||
<ToolBtn id="text" current={tool} onClick={setTool} title="Text">
|
<ToolBtn id="text" current={tool} onClick={setTool} title="Text">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
<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" />
|
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>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Color picker */}
|
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={penColor}
|
value={penColor}
|
||||||
onChange={(e) => setPenColor(e.target.value)}
|
onChange={(e) => setPenColor(e.target.value)}
|
||||||
title="Color"
|
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
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={1}
|
min={1}
|
||||||
max={12}
|
max={12}
|
||||||
value={strokeWidth}
|
value={strokeWidth}
|
||||||
onChange={(e) => setStrokeWidth(Number(e.target.value))}
|
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}`}
|
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
|
<button
|
||||||
onClick={handleUndo}
|
onClick={handleUndo}
|
||||||
title="Undo last stroke"
|
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">
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||||
@@ -928,11 +938,10 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Clear page */}
|
|
||||||
<button
|
<button
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
title="Clear page annotations"
|
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">
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||||
@@ -941,13 +950,12 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Save button — far right */}
|
{/* Save — fixed right, never scrolls away */}
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="flex items-center gap-2 pl-3 flex-shrink-0">
|
||||||
{saveNotice && (
|
{saveNotice && (
|
||||||
<span className="text-xs text-green-600 font-medium animate-pulse">
|
<span className="text-xs text-green-600 font-medium animate-pulse">✓ Saved</span>
|
||||||
✓ Saved
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
|||||||
Reference in New Issue
Block a user