- 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.
171 lines
5.7 KiB
TypeScript
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>
|
|
);
|
|
}
|