78 lines
2.8 KiB
TypeScript
78 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState } from "react";
|
|
|
|
interface Props {
|
|
onChange: (files: File[]) => void;
|
|
files: File[];
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function FileUploader({ onChange, files, disabled }: Props) {
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const [dragOver, setDragOver] = useState(false);
|
|
|
|
function addFiles(incoming: FileList | null) {
|
|
if (!incoming) return;
|
|
const next = [...files];
|
|
for (const file of Array.from(incoming)) {
|
|
if (!next.some((f) => f.name === file.name && f.size === file.size)) {
|
|
next.push(file);
|
|
}
|
|
}
|
|
onChange(next);
|
|
}
|
|
|
|
function remove(index: number) {
|
|
onChange(files.filter((_, i) => i !== index));
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div
|
|
onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
|
|
onDragLeave={() => setDragOver(false)}
|
|
onDrop={(e) => { e.preventDefault(); setDragOver(false); addFiles(e.dataTransfer.files); }}
|
|
onClick={() => !disabled && inputRef.current?.click()}
|
|
className={`flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-8 text-sm transition-colors ${
|
|
dragOver ? "border-primary-400 bg-primary-50" : "border-neutral-300 hover:border-neutral-400"
|
|
} ${disabled ? "cursor-not-allowed opacity-60" : ""}`}
|
|
>
|
|
<span className="font-medium text-neutral-700">Drop files here or click to browse</span>
|
|
<span className="mt-1 text-xs text-neutral-400">PDF, images up to 10 MB each</span>
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
multiple
|
|
className="hidden"
|
|
disabled={disabled}
|
|
onChange={(e) => addFiles(e.target.files)}
|
|
accept=".pdf,.png,.jpg,.jpeg,.gif,.webp"
|
|
/>
|
|
</div>
|
|
|
|
{files.length > 0 && (
|
|
<ul className="space-y-1.5">
|
|
{files.map((file, i) => (
|
|
<li key={i} className="flex items-center justify-between rounded-lg border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm">
|
|
<span className="font-medium text-neutral-800 truncate max-w-xs">{file.name}</span>
|
|
<div className="flex items-center gap-3 ml-3 shrink-0">
|
|
<span className="text-xs text-neutral-400">{(file.size / 1024).toFixed(0)} KB</span>
|
|
{!disabled && (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => { e.stopPropagation(); remove(i); }}
|
|
className="text-neutral-400 hover:text-danger-700 transition-colors"
|
|
aria-label="Remove file"
|
|
>
|
|
✕
|
|
</button>
|
|
)}
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|