diff --git a/src/components/common/signature-capture-fields.tsx b/src/components/common/signature-capture-fields.tsx new file mode 100644 index 00000000..43e68eb6 --- /dev/null +++ b/src/components/common/signature-capture-fields.tsx @@ -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; + 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 ( + <> + onSignerNameChange(event.target.value)} + placeholder="Enter signer's full name" + fullWidth + size="small" + disabled={isPending} + /> + + onModeChange(value)}> + + + + + {mode === "draw" ? ( + + + + + ) : ( + + onTypedNameChange(event.target.value)} + placeholder="Type your name" + fullWidth + size="small" + disabled={isPending} + /> + {Boolean(typedName) && ( + + {typedName} + + )} + + )} + + ); +} diff --git a/src/components/common/signature-capture.tsx b/src/components/common/signature-capture.tsx index 6c17eb80..c7360b9d 100644 --- a/src/components/common/signature-capture.tsx +++ b/src/components/common/signature-capture.tsx @@ -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(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({ - setSignerName(event.target.value)} - placeholder="Enter signer's full name" - fullWidth - size="small" - disabled={isPending} + - - setMode(value)}> - - - - - {mode === "draw" ? ( - - - - - ) : ( - - setTypedName(event.target.value)} - placeholder="Type your name" - fullWidth - size="small" - disabled={isPending} - /> - {Boolean(typedName) && ( - - {typedName} - - )} - - )} diff --git a/src/components/common/use-signature-canvas.ts b/src/components/common/use-signature-canvas.ts new file mode 100644 index 00000000..8d8904c8 --- /dev/null +++ b/src/components/common/use-signature-canvas.ts @@ -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(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 }; +}