128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState } from "react";
|
|
import { saveSignature, removeSignature } from "./actions";
|
|
import { Upload, X } from "lucide-react";
|
|
|
|
interface Props {
|
|
currentSignatureUrl: string | null;
|
|
}
|
|
|
|
export function SignatureUploader({ currentSignatureUrl }: Props) {
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const [preview, setPreview] = useState<string | null>(null);
|
|
const [pending, setPending] = useState(false);
|
|
const [removing, setRemoving] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [success, setSuccess] = useState("");
|
|
|
|
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
setError("");
|
|
setSuccess("");
|
|
setPreview(URL.createObjectURL(file));
|
|
}
|
|
|
|
async function handleUpload(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault();
|
|
const file = inputRef.current?.files?.[0];
|
|
if (!file) { setError("Please select a file first"); return; }
|
|
|
|
const fd = new FormData();
|
|
fd.append("signature", file);
|
|
|
|
setPending(true);
|
|
setError("");
|
|
setSuccess("");
|
|
const result = await saveSignature(fd);
|
|
setPending(false);
|
|
|
|
if ("error" in result) {
|
|
setError(result.error);
|
|
} else {
|
|
setSuccess("Signature saved successfully.");
|
|
setPreview(null);
|
|
if (inputRef.current) inputRef.current.value = "";
|
|
}
|
|
}
|
|
|
|
async function handleRemove() {
|
|
setRemoving(true);
|
|
setError("");
|
|
setSuccess("");
|
|
const result = await removeSignature();
|
|
setRemoving(false);
|
|
if ("error" in result) setError(result.error);
|
|
else setSuccess("Signature removed.");
|
|
}
|
|
|
|
const displayUrl = preview ?? currentSignatureUrl;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{displayUrl && (
|
|
<div className="rounded-lg border border-neutral-200 bg-neutral-50 p-4 inline-block">
|
|
<p className="text-xs font-medium text-neutral-500 mb-2">
|
|
{preview ? "Preview" : "Current signature"}
|
|
</p>
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={displayUrl}
|
|
alt="Signature"
|
|
className="max-h-24 max-w-xs object-contain border border-neutral-200 rounded bg-white p-2"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleUpload} className="space-y-3">
|
|
<div
|
|
className="relative rounded-lg border-2 border-dashed border-neutral-300 bg-neutral-50 p-6 text-center cursor-pointer hover:border-primary-400 hover:bg-primary-50 transition-colors"
|
|
onClick={() => inputRef.current?.click()}
|
|
>
|
|
<Upload className="mx-auto h-8 w-8 text-neutral-400 mb-2" />
|
|
<p className="text-sm text-neutral-600">
|
|
Click to select signature image
|
|
</p>
|
|
<p className="text-xs text-neutral-400 mt-1">PNG, JPG or WebP — max 2 MB</p>
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
name="signature"
|
|
accept="image/png,image/jpeg,image/jpg,image/webp"
|
|
onChange={handleFileChange}
|
|
className="sr-only"
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>
|
|
)}
|
|
{success && (
|
|
<p className="text-sm text-success-700 bg-success-50 rounded-lg px-3 py-2">{success}</p>
|
|
)}
|
|
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="submit"
|
|
disabled={pending || !preview}
|
|
className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-50 transition-colors"
|
|
>
|
|
{pending ? "Saving…" : "Save Signature"}
|
|
</button>
|
|
{currentSignatureUrl && !preview && (
|
|
<button
|
|
type="button"
|
|
onClick={handleRemove}
|
|
disabled={removing}
|
|
className="inline-flex items-center gap-1.5 rounded-lg border border-danger-200 bg-white px-3 py-2 text-sm font-medium text-danger-700 hover:bg-danger-50 disabled:opacity-50 transition-colors"
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
{removing ? "Removing…" : "Remove"}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|