132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
import { useId, useMemo, type ChangeEvent } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Label } from "@/components/ui/label";
|
|
import { FileUp, FileText, Image as ImageIcon, X } from "lucide-react";
|
|
|
|
type DocumentUploadFieldProps = {
|
|
label: string;
|
|
value?: string | null;
|
|
onChange?: (value: string | null) => void;
|
|
required?: boolean;
|
|
helperText?: string;
|
|
accept?: string;
|
|
className?: string;
|
|
};
|
|
|
|
function readFileAsDataUrl(file: File) {
|
|
return new Promise<string>((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
reader.onerror = () => reject(reader.error ?? new Error("Gagal membaca file"));
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
function getPreviewKind(value?: string | null) {
|
|
if (!value) return "none";
|
|
if (value.startsWith("data:application/pdf")) return "pdf";
|
|
if (value.startsWith("data:image/")) return "image";
|
|
return "file";
|
|
}
|
|
|
|
export function DocumentUploadField({
|
|
label,
|
|
value,
|
|
onChange,
|
|
required = false,
|
|
helperText,
|
|
accept = "image/*,application/pdf",
|
|
className = "",
|
|
}: DocumentUploadFieldProps) {
|
|
const inputId = useId();
|
|
const previewKind = useMemo(() => getPreviewKind(value), [value]);
|
|
|
|
const handleSelectFile = async (event: ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0];
|
|
event.target.value = "";
|
|
if (!file || !onChange) return;
|
|
const dataUrl = await readFileAsDataUrl(file);
|
|
onChange(dataUrl);
|
|
};
|
|
|
|
const canEdit = Boolean(onChange);
|
|
|
|
return (
|
|
<div className={`space-y-2 ${className}`}>
|
|
<div className="flex items-center justify-between gap-2">
|
|
<Label htmlFor={inputId} className="text-xs font-medium">
|
|
{label} {required ? <span className="text-red-500">*</span> : null}
|
|
</Label>
|
|
{canEdit && value ? (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 px-2 text-[11px]"
|
|
onClick={() => onChange?.(null)}
|
|
>
|
|
<X className="h-3 w-3 mr-1" />
|
|
Hapus
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
{helperText ? <p className="text-[11px] text-muted-foreground">{helperText}</p> : null}
|
|
<input
|
|
id={inputId}
|
|
type="file"
|
|
accept={accept}
|
|
className="hidden"
|
|
onChange={handleSelectFile}
|
|
disabled={!canEdit}
|
|
/>
|
|
<label
|
|
htmlFor={inputId}
|
|
className={`block cursor-pointer rounded-xl border border-dashed p-4 transition-colors ${
|
|
value ? "bg-muted/20 hover:bg-muted/30" : "bg-background hover:bg-muted/20"
|
|
} ${!canEdit ? "cursor-default" : ""}`}
|
|
>
|
|
{value ? (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
{previewKind === "image" ? (
|
|
<ImageIcon className="h-4 w-4" />
|
|
) : (
|
|
<FileText className="h-4 w-4" />
|
|
)}
|
|
<span>Dokumen siap dipreview</span>
|
|
</div>
|
|
{previewKind === "image" ? (
|
|
<img
|
|
src={value}
|
|
alt={label}
|
|
className="h-40 w-full rounded-lg border object-contain bg-white"
|
|
/>
|
|
) : previewKind === "pdf" ? (
|
|
<iframe
|
|
src={value}
|
|
title={label}
|
|
className="h-40 w-full rounded-lg border bg-white"
|
|
/>
|
|
) : (
|
|
<div className="flex h-40 items-center justify-center rounded-lg border bg-white text-xs text-muted-foreground">
|
|
File terunggah
|
|
</div>
|
|
)}
|
|
{canEdit ? (
|
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
<FileUp className="h-3.5 w-3.5" />
|
|
Klik untuk mengganti file
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
) : (
|
|
<div className="flex min-h-32 flex-col items-center justify-center gap-2 text-center text-muted-foreground">
|
|
<FileUp className="h-6 w-6" />
|
|
<p className="text-sm font-medium">Klik untuk upload atau seret file ke sini</p>
|
|
<p className="text-[11px]">{accept.toUpperCase()} {required ? "- Wajib" : "- Opsional"}</p>
|
|
</div>
|
|
)}
|
|
</label>
|
|
</div>
|
|
);
|
|
} |