Files
diyaa.de/components/admin/file-upload.tsx
T

116 lines
3.2 KiB
TypeScript

"use client";
import { useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
uploadedFileSchema,
UPLOAD_RULES,
type UploadKind,
type UploadedFileMetadata,
} from "@/lib/validations/upload";
type FileUploadProps = {
kind: UploadKind;
onUploaded: (files: UploadedFileMetadata[]) => void;
id?: string;
multiple?: boolean;
label?: string;
helperText?: string;
};
const defaultLabels: Record<UploadKind, string> = {
image: "Image",
audio: "Audio",
};
export default function FileUpload({
kind,
onUploaded,
id,
multiple = false,
label,
helperText,
}: FileUploadProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const rules = UPLOAD_RULES[kind];
const fieldId = id ?? `file-upload-${kind}`;
async function handleFiles(selectedFiles: File[]) {
setError(null);
setIsUploading(true);
try {
const uploaded: UploadedFileMetadata[] = [];
for (const file of selectedFiles) {
if (!rules.mimeTypes.includes(file.type) || file.size > rules.maxBytes) {
throw new Error(`Unsupported or oversized ${kind} file.`);
}
const body = new FormData();
body.set("kind", kind);
body.set("file", file);
const response = await fetch("/api/upload", { method: "POST", body });
const payload: unknown = await response.json();
if (!response.ok) {
const message =
typeof payload === "object" && payload !== null && "error" in payload
? String(payload.error)
: "The file could not be uploaded.";
throw new Error(message);
}
const parsed = uploadedFileSchema.safeParse(payload);
if (!parsed.success) {
throw new Error("The upload response was invalid.");
}
uploaded.push(parsed.data);
}
onUploaded(uploaded);
} catch (uploadError) {
setError(uploadError instanceof Error ? uploadError.message : "The file could not be uploaded.");
} finally {
setIsUploading(false);
if (inputRef.current) {
inputRef.current.value = "";
}
}
}
return (
<div className="space-y-2" dir="auto">
<Label htmlFor={fieldId}>{label ?? defaultLabels[kind]}</Label>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Input
ref={inputRef}
id={fieldId}
type="file"
accept={rules.accept}
multiple={multiple}
disabled={isUploading}
onChange={(event) => {
void handleFiles(Array.from(event.target.files ?? []));
}}
/>
{isUploading ? (
<Button type="button" variant="secondary" disabled>
Uploading
</Button>
) : null}
</div>
<p className="text-xs text-muted-foreground">
{helperText ?? `Maximum ${Math.round(rules.maxBytes / (1024 * 1024))} MB.`}
</p>
{error ? <p className="text-sm text-destructive">{error}</p> : null}
</div>
);
}