mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 03:02:03 +00:00
214 lines
5.6 KiB
TypeScript
214 lines
5.6 KiB
TypeScript
import { CSS_VAR_FALLBACKS, getCssVar } from "@/lib/theme/css-vars";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import CloseIcon from "@mui/icons-material/Close";
|
|
import {
|
|
Box,
|
|
Button,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
IconButton,
|
|
Tab,
|
|
Tabs,
|
|
TextField,
|
|
Typography,
|
|
} from "@mui/material";
|
|
|
|
interface SignatureCaptureProps {
|
|
title?: string;
|
|
onClose: () => void;
|
|
onSave: (signature: string, method: "drawn" | "typed", name: string) => void;
|
|
}
|
|
|
|
export function SignatureCapture({ onClose, onSave, title }: SignatureCaptureProps) {
|
|
const [mode, setMode] = useState<"draw" | "type">("draw");
|
|
const [typedName, setTypedName] = useState("");
|
|
const [signerName, setSignerName] = useState("");
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const isDrawing = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (mode === "draw" && canvasRef.current) {
|
|
const canvas = canvasRef.current;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
ctx.fillStyle = getCssVar("--background", CSS_VAR_FALLBACKS.background);
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
ctx.strokeStyle = getCssVar("--foreground", CSS_VAR_FALLBACKS.foreground);
|
|
ctx.lineWidth = 2;
|
|
ctx.lineCap = "round";
|
|
}
|
|
}, [mode]);
|
|
|
|
const getPos = (event: React.MouseEvent | React.TouchEvent) => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) {
|
|
return { x: 0, y: 0 };
|
|
}
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
const touch = "touches" in event ? event.touches[0] : event;
|
|
if (!touch) {
|
|
return { x: 0, y: 0 };
|
|
}
|
|
|
|
return { x: touch.clientX - rect.left, y: touch.clientY - rect.top };
|
|
};
|
|
|
|
const startDraw = (event: React.MouseEvent | React.TouchEvent) => {
|
|
event.preventDefault();
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
isDrawing.current = true;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
const pos = getPos(event);
|
|
ctx.beginPath();
|
|
ctx.moveTo(pos.x, pos.y);
|
|
};
|
|
|
|
const draw = (event: React.MouseEvent | React.TouchEvent) => {
|
|
event.preventDefault();
|
|
if (!isDrawing.current || !canvasRef.current) {
|
|
return;
|
|
}
|
|
|
|
const ctx = canvasRef.current.getContext("2d");
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
const pos = getPos(event);
|
|
ctx.lineTo(pos.x, pos.y);
|
|
ctx.stroke();
|
|
};
|
|
|
|
const stopDraw = () => {
|
|
isDrawing.current = false;
|
|
};
|
|
|
|
const clearCanvas = () => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
ctx.fillStyle = getCssVar("--background", CSS_VAR_FALLBACKS.background);
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (!signerName.trim()) {
|
|
return;
|
|
}
|
|
|
|
if (mode === "draw") {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
onSave(canvas.toDataURL("image/png"), "drawn", signerName.trim());
|
|
return;
|
|
}
|
|
|
|
if (!typedName.trim()) {
|
|
return;
|
|
}
|
|
|
|
onSave(typedName, "typed", signerName.trim());
|
|
};
|
|
|
|
const dialog = (
|
|
<Dialog open onClose={onClose} maxWidth="sm" fullWidth>
|
|
<DialogTitle className="flex items-center justify-between pr-2">
|
|
{title || "Capture Signature"}
|
|
<IconButton aria-label="Close" onClick={onClose} size="small">
|
|
<CloseIcon fontSize="small" />
|
|
</IconButton>
|
|
</DialogTitle>
|
|
|
|
<DialogContent className="flex flex-col gap-4">
|
|
<TextField
|
|
label="Full Name"
|
|
value={signerName}
|
|
onChange={(event) => setSignerName(event.target.value)}
|
|
placeholder="Enter signer's full name"
|
|
fullWidth
|
|
size="small"
|
|
/>
|
|
|
|
<Tabs value={mode} onChange={(_event, value: "draw" | "type") => setMode(value)}>
|
|
<Tab label="Draw" value="draw" />
|
|
<Tab label="Type" value="type" />
|
|
</Tabs>
|
|
|
|
{mode === "draw" ? (
|
|
<Box className="relative">
|
|
<canvas
|
|
ref={canvasRef}
|
|
width={460}
|
|
height={180}
|
|
className="block w-full cursor-crosshair rounded border border-border bg-background"
|
|
onMouseDown={startDraw}
|
|
onMouseMove={draw}
|
|
onMouseUp={stopDraw}
|
|
onMouseLeave={stopDraw}
|
|
onTouchStart={startDraw}
|
|
onTouchMove={draw}
|
|
onTouchEnd={stopDraw}
|
|
/>
|
|
<Button size="small" onClick={clearCanvas} className="mt-2">
|
|
Clear
|
|
</Button>
|
|
</Box>
|
|
) : (
|
|
<Box className="flex flex-col gap-2">
|
|
<TextField
|
|
value={typedName}
|
|
onChange={(event) => setTypedName(event.target.value)}
|
|
placeholder="Type your name"
|
|
fullWidth
|
|
size="small"
|
|
/>
|
|
{Boolean(typedName) && (
|
|
<Typography
|
|
variant="h5"
|
|
className="rounded border border-dashed border-border px-4 py-6 text-center italic"
|
|
>
|
|
{typedName}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</DialogContent>
|
|
|
|
<DialogActions>
|
|
<Button onClick={onClose}>Cancel</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={handleSave}
|
|
disabled={!signerName.trim() || (mode === "type" && !typedName.trim())}
|
|
>
|
|
Save Signature
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
|
|
return createPortal(dialog, document.body);
|
|
}
|