178 lines
5.3 KiB
TypeScript
178 lines
5.3 KiB
TypeScript
import React, { useRef, useState, useEffect } from 'react';
|
|
import { Eraser, Check } from 'lucide-react';
|
|
|
|
interface SignatureCaptureProps {
|
|
onSignatureComplete: (signatureDataUrl: string) => void;
|
|
width?: number;
|
|
height?: number;
|
|
}
|
|
|
|
export const SignatureCapture: React.FC<SignatureCaptureProps> = ({
|
|
onSignatureComplete,
|
|
width = 600,
|
|
height = 200
|
|
}) => {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const [isDrawing, setIsDrawing] = useState(false);
|
|
const [hasSignature, setHasSignature] = useState(false);
|
|
|
|
// Initialize canvas context
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
// Detect theme for stroke color
|
|
const isDark = document.documentElement.classList.contains('dark');
|
|
ctx.strokeStyle = isDark ? '#fafafa' : '#09090b';
|
|
ctx.lineWidth = 3;
|
|
ctx.lineCap = 'round';
|
|
ctx.lineJoin = 'round';
|
|
|
|
// Handle high DPI displays for crisp rendering
|
|
const dpr = window.devicePixelRatio || 1;
|
|
canvas.width = width * dpr;
|
|
canvas.height = height * dpr;
|
|
canvas.style.width = `${width}px`;
|
|
canvas.style.height = `${height}px`;
|
|
ctx.scale(dpr, dpr);
|
|
}, [width, height]);
|
|
|
|
// Adjust stroke color if theme toggles during active view
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
const isDark = document.documentElement.classList.contains('dark');
|
|
ctx.strokeStyle = isDark ? '#fafafa' : '#09090b';
|
|
});
|
|
|
|
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
|
|
e.preventDefault();
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
let clientX, clientY;
|
|
|
|
if ('touches' in e) {
|
|
clientX = e.touches[0].clientX;
|
|
clientY = e.touches[0].clientY;
|
|
} else {
|
|
clientX = (e as React.MouseEvent).clientX;
|
|
clientY = (e as React.MouseEvent).clientY;
|
|
}
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(clientX - rect.left, clientY - rect.top);
|
|
setIsDrawing(true);
|
|
};
|
|
|
|
const draw = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
|
|
e.preventDefault();
|
|
if (!isDrawing) return;
|
|
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
let clientX, clientY;
|
|
|
|
if ('touches' in e) {
|
|
clientX = e.touches[0].clientX;
|
|
clientY = e.touches[0].clientY;
|
|
} else {
|
|
clientX = (e as React.MouseEvent).clientX;
|
|
clientY = (e as React.MouseEvent).clientY;
|
|
}
|
|
|
|
ctx.lineTo(clientX - rect.left, clientY - rect.top);
|
|
ctx.stroke();
|
|
setHasSignature(true);
|
|
};
|
|
|
|
const stopDrawing = () => {
|
|
setIsDrawing(false);
|
|
};
|
|
|
|
const clearSignature = () => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
setHasSignature(false);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas || !hasSignature) return;
|
|
|
|
const dataUrl = canvas.toDataURL('image/png');
|
|
onSignatureComplete(dataUrl);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col w-full max-w-2xl mx-auto">
|
|
<div className="relative rounded-2xl bg-ink-50 border-2 border-dashed border-ink-200 overflow-hidden shadow-inner group">
|
|
|
|
{/* Helper Text */}
|
|
{!hasSignature && (
|
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
<p className="text-ink-400 font-medium text-sm">
|
|
Draw your signature here
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<canvas
|
|
ref={canvasRef}
|
|
onMouseDown={startDrawing}
|
|
onMouseMove={draw}
|
|
onMouseUp={stopDrawing}
|
|
onMouseOut={stopDrawing}
|
|
onTouchStart={startDrawing}
|
|
onTouchMove={draw}
|
|
onTouchEnd={stopDrawing}
|
|
className="touch-none cursor-crosshair relative z-10 w-full"
|
|
style={{ width: '100%', height: `${height}px`, maxWidth: `${width}px` }}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between mt-4">
|
|
<button
|
|
type="button"
|
|
onClick={clearSignature}
|
|
disabled={!hasSignature}
|
|
className="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-ink-500 hover:text-red-600 hover:bg-red-500/10 rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Eraser className="w-4 h-4" />
|
|
Clear
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleSave}
|
|
disabled={!hasSignature}
|
|
className="flex items-center gap-2 px-6 py-2.5 text-sm font-bold text-ink-0 bg-ink-900 hover:bg-ink-800 rounded-xl transition-all shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-md hover:-translate-y-0.5"
|
|
>
|
|
<Check className="w-4 h-4" />
|
|
Accept & Sign
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|