mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
fix(work-orders): split SignatureCapture under governance line limit
Extract canvas hook and fields so changed-path maintainability passes on #34.
This commit is contained in:
parent
8df270f713
commit
021fa5b5d4
3 changed files with 193 additions and 145 deletions
91
src/components/common/signature-capture-fields.tsx
Normal file
91
src/components/common/signature-capture-fields.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { Box, Button, Tab, Tabs, TextField, Typography } from "@mui/material";
|
||||
import type { RefObject } from "react";
|
||||
|
||||
interface SignatureCaptureFieldsProps {
|
||||
isPending: boolean;
|
||||
mode: "draw" | "type";
|
||||
onModeChange: (mode: "draw" | "type") => void;
|
||||
signerName: string;
|
||||
onSignerNameChange: (value: string) => void;
|
||||
typedName: string;
|
||||
onTypedNameChange: (value: string) => void;
|
||||
canvasRef: RefObject<HTMLCanvasElement | null>;
|
||||
startDraw: (event: React.MouseEvent | React.TouchEvent) => void;
|
||||
draw: (event: React.MouseEvent | React.TouchEvent) => void;
|
||||
stopDraw: () => void;
|
||||
clearCanvas: () => void;
|
||||
}
|
||||
|
||||
export function SignatureCaptureFields({
|
||||
isPending,
|
||||
mode,
|
||||
onModeChange,
|
||||
signerName,
|
||||
onSignerNameChange,
|
||||
typedName,
|
||||
onTypedNameChange,
|
||||
canvasRef,
|
||||
startDraw,
|
||||
draw,
|
||||
stopDraw,
|
||||
clearCanvas,
|
||||
}: SignatureCaptureFieldsProps) {
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
label="Full Name"
|
||||
value={signerName}
|
||||
onChange={(event) => onSignerNameChange(event.target.value)}
|
||||
placeholder="Enter signer's full name"
|
||||
fullWidth
|
||||
size="small"
|
||||
disabled={isPending}
|
||||
/>
|
||||
|
||||
<Tabs value={mode} onChange={(_event, value: "draw" | "type") => onModeChange(value)}>
|
||||
<Tab label="Draw" value="draw" disabled={isPending} />
|
||||
<Tab label="Type" value="type" disabled={isPending} />
|
||||
</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={isPending ? undefined : startDraw}
|
||||
onMouseMove={isPending ? undefined : draw}
|
||||
onMouseUp={isPending ? undefined : stopDraw}
|
||||
onMouseLeave={isPending ? undefined : stopDraw}
|
||||
onTouchStart={isPending ? undefined : startDraw}
|
||||
onTouchMove={isPending ? undefined : draw}
|
||||
onTouchEnd={isPending ? undefined : stopDraw}
|
||||
/>
|
||||
<Button size="small" onClick={clearCanvas} className="mt-2" disabled={isPending}>
|
||||
Clear
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<Box className="flex flex-col gap-2">
|
||||
<TextField
|
||||
value={typedName}
|
||||
onChange={(event) => onTypedNameChange(event.target.value)}
|
||||
placeholder="Type your name"
|
||||
fullWidth
|
||||
size="small"
|
||||
disabled={isPending}
|
||||
/>
|
||||
{Boolean(typedName) && (
|
||||
<Typography
|
||||
variant="h5"
|
||||
className="rounded border border-dashed border-border px-4 py-6 text-center italic"
|
||||
>
|
||||
{typedName}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,21 +1,17 @@
|
|||
import { CSS_VAR_FALLBACKS, getCssVar } from "@/lib/theme/css-vars";
|
||||
import { guardDialogClose } from "@/lib/dialog-guard";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { 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";
|
||||
import { SignatureCaptureFields } from "@/components/common/signature-capture-fields";
|
||||
import { useSignatureCanvas } from "@/components/common/use-signature-canvas";
|
||||
|
||||
interface SignatureCaptureProps {
|
||||
title?: string;
|
||||
|
|
@ -33,91 +29,7 @@ export function SignatureCapture({
|
|||
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 { canvasRef, startDraw, draw, stopDraw, clearCanvas } = useSignatureCanvas(mode);
|
||||
|
||||
const handleSave = () => {
|
||||
if (isPending || !signerName.trim()) {
|
||||
|
|
@ -150,60 +62,20 @@ export function SignatureCapture({
|
|||
</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"
|
||||
disabled={isPending}
|
||||
<SignatureCaptureFields
|
||||
isPending={isPending}
|
||||
mode={mode}
|
||||
onModeChange={setMode}
|
||||
signerName={signerName}
|
||||
onSignerNameChange={setSignerName}
|
||||
typedName={typedName}
|
||||
onTypedNameChange={setTypedName}
|
||||
canvasRef={canvasRef}
|
||||
startDraw={startDraw}
|
||||
draw={draw}
|
||||
stopDraw={stopDraw}
|
||||
clearCanvas={clearCanvas}
|
||||
/>
|
||||
|
||||
<Tabs value={mode} onChange={(_event, value: "draw" | "type") => setMode(value)}>
|
||||
<Tab label="Draw" value="draw" disabled={isPending} />
|
||||
<Tab label="Type" value="type" disabled={isPending} />
|
||||
</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={isPending ? undefined : startDraw}
|
||||
onMouseMove={isPending ? undefined : draw}
|
||||
onMouseUp={isPending ? undefined : stopDraw}
|
||||
onMouseLeave={isPending ? undefined : stopDraw}
|
||||
onTouchStart={isPending ? undefined : startDraw}
|
||||
onTouchMove={isPending ? undefined : draw}
|
||||
onTouchEnd={isPending ? undefined : stopDraw}
|
||||
/>
|
||||
<Button size="small" onClick={clearCanvas} className="mt-2" disabled={isPending}>
|
||||
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"
|
||||
disabled={isPending}
|
||||
/>
|
||||
{Boolean(typedName) && (
|
||||
<Typography
|
||||
variant="h5"
|
||||
className="rounded border border-dashed border-border px-4 py-6 text-center italic"
|
||||
>
|
||||
{typedName}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
|
|
|
|||
85
src/components/common/use-signature-canvas.ts
Normal file
85
src/components/common/use-signature-canvas.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { CSS_VAR_FALLBACKS, getCssVar } from "@/lib/theme/css-vars";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function useSignatureCanvas(mode: "draw" | "type") {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const isDrawing = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "draw" || !canvasRef.current) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
};
|
||||
|
||||
return { canvasRef, startDraw, draw, stopDraw, clearCanvas };
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue