itmpwk.ac.id/resources/js/components/signature-pad.tsx
Yoga Pangestu 692b84cc1b feat: add open semesters badge to academic terms and remove course registration pages
- Added a new column to display open semesters in the academic terms table with badges.
- Removed course registration related pages and components including index, create, and show.
- Updated course registration types to reflect changes in the data structure.
- Refactored admin routes for course registrations to streamline submission handling.
2026-09-01 21:39:46 +07:00

171 lines
5.7 KiB
TypeScript

import { Eraser } from 'lucide-react';
import { useEffect, useRef } from 'react';
import SignatureCanvasImport from 'react-signature-canvas';
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
// react-signature-canvas ships as a CommonJS/UMD bundle. Vite's dev-server
// pre-bundling wraps its `module.exports` (itself `{ __esModule: true,
// default: SignatureCanvas }`) as the ESM default export, so the default
// import resolves one layer too shallow. Unwrap it before use.
const SignatureCanvas = ((
SignatureCanvasImport as unknown as {
default?: typeof SignatureCanvasImport;
}
).default ?? SignatureCanvasImport) as typeof SignatureCanvasImport;
type SignaturePadProps = {
name: string;
error?: string;
/** Renders a small, chrome-free pad sized to sit inline with other
* compact signature placeholders (e.g. a document's signature row). */
compact?: boolean;
/** Fires with the captured signature file whenever the drawing changes,
* for callers that submit it outside a native <form> (e.g. via router). */
onCapture?: (file: File | null) => void;
};
export function SignaturePad({
name,
error,
compact,
onCapture,
}: SignaturePadProps) {
const padRef = useRef<SignatureCanvasImport>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
// The canvas element's drawing-surface resolution (width/height
// attributes) does not automatically follow its CSS-rendered size, so
// without this, pointer coordinates drift from where ink is drawn —
// worse on HiDPI screens. Size the backing buffer to match on mount and
// whenever the layout might change.
useEffect(() => {
function resizeCanvas() {
const canvas = padRef.current?.getCanvas();
if (!canvas) {
return;
}
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext('2d')?.scale(ratio, ratio);
padRef.current?.clear();
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
return () => window.removeEventListener('resize', resizeCanvas);
}, []);
function syncFileInput() {
const pad = padRef.current;
const input = fileInputRef.current;
if (!pad || !input || pad.isEmpty()) {
return;
}
pad.getTrimmedCanvas().toBlob((blob: Blob | null) => {
if (!blob) {
return;
}
const file = new File([blob], 'signature.png', {
type: 'image/png',
});
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
input.files = dataTransfer.files;
onCapture?.(file);
}, 'image/png');
}
function handleClear() {
padRef.current?.clear();
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
onCapture?.(null);
}
if (compact) {
return (
<div className="grid gap-1">
<div className="relative h-28 w-48 overflow-hidden rounded border bg-white">
<SignatureCanvas
ref={padRef}
penColor="#0f172a"
canvasProps={{
className:
'h-28 w-48 cursor-crosshair touch-none',
}}
onEnd={syncFileInput}
/>
<button
type="button"
onClick={handleClear}
aria-label="Hapus tanda tangan"
className="absolute top-0.5 right-0.5 rounded bg-white/80 p-0.5 text-slate-500 hover:text-slate-900"
>
<Eraser className="h-3 w-3" />
</button>
</div>
<input
ref={fileInputRef}
type="file"
name={name}
className="hidden"
/>
<InputError message={error} />
</div>
);
}
return (
<div className="grid gap-2">
<div className="flex items-center justify-between">
<Label>
Tanda Tangan <span className="text-destructive">*</span>
</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleClear}
>
<Eraser className="h-4 w-4" />
Hapus
</Button>
</div>
<div className="overflow-hidden rounded-md border bg-white">
<SignatureCanvas
ref={padRef}
penColor="#0f172a"
canvasProps={{
className:
'h-[180px] w-full cursor-crosshair touch-none',
}}
onEnd={syncFileInput}
/>
</div>
<input
ref={fileInputRef}
type="file"
name={name}
className="hidden"
/>
<p className="text-xs text-muted-foreground">
Gambar tanda tangan Anda pada kotak di atas menggunakan mouse
atau layar sentuh.
</p>
<InputError message={error} />
</div>
);
}