"use client"; import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { Privacy } from "@prisma/client"; import { filenameWithoutExtension } from "@/lib/constants"; import type { ItemMetadata } from "@/lib/types"; import { CategorySelect } from "./CategorySelect"; import { PrivacyToggle } from "./PrivacyToggle"; import { ResolutionSelect } from "./ResolutionSelect"; type AudioItem = { id: string; file: File; path: string | null; uploading: boolean; metadata: ItemMetadata; }; function defaultMetadata(title = ""): ItemMetadata { return { title, description: "", tags: "", privacy: "PUBLIC" as Privacy, categoryId: "10", resolution: "1280x720", notifySubscribers: true, madeForKids: false, embeddable: true, creativeCommons: false, includeWatermark: true, }; } export function UploadForm() { const router = useRouter(); const [imageFile, setImageFile] = useState(null); const [imagePath, setImagePath] = useState(null); const [imageUploading, setImageUploading] = useState(false); const [audioItems, setAudioItems] = useState([]); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [quota, setQuota] = useState<{ remaining: number; limit: number } | null>(null); const loadQuota = useCallback(async () => { const res = await fetch("/api/quota"); if (res.ok) { const data = await res.json(); setQuota({ remaining: data.remaining, limit: data.limit }); } }, []); useEffect(() => { loadQuota(); }, [loadQuota]); async function uploadFile(file: File, type: "image" | "audio"): Promise { const formData = new FormData(); formData.append("file", file); formData.append("type", type); const res = await fetch("/api/upload", { method: "POST", body: formData }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Upload failed"); } const data = await res.json(); return data.path; } async function handleImageChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setError(null); setImageFile(file); setImageUploading(true); try { const path = await uploadFile(file, "image"); setImagePath(path); } catch (err) { setError(err instanceof Error ? err.message : "Image upload failed"); setImageFile(null); setImagePath(null); } finally { setImageUploading(false); } } async function handleAudioChange(e: React.ChangeEvent) { const files = Array.from(e.target.files || []); if (!files.length) return; setError(null); for (const file of files) { const id = crypto.randomUUID(); const autoTitle = filenameWithoutExtension(file.name); setAudioItems((prev) => [ ...prev, { id, file, path: null, uploading: true, metadata: defaultMetadata(autoTitle), }, ]); try { const path = await uploadFile(file, "audio"); setAudioItems((prev) => prev.map((item) => item.id === id ? { ...item, path, uploading: false } : item, ), ); } catch (err) { setError(err instanceof Error ? err.message : "Audio upload failed"); setAudioItems((prev) => prev.filter((item) => item.id !== id)); } } e.target.value = ""; } function updateItemMetadata(id: string, updates: Partial) { setAudioItems((prev) => prev.map((item) => item.id === id ? { ...item, metadata: { ...item.metadata, ...updates } } : item, ), ); } function removeAudioItem(id: string) { setAudioItems((prev) => prev.filter((item) => item.id !== id)); } const readyAudios = audioItems.filter((a) => a.path && !a.uploading); const canSubmit = imagePath && !imageUploading && readyAudios.length > 0 && readyAudios.every((a) => a.metadata.title.trim()) && !submitting; async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!canSubmit || !imagePath) return; setSubmitting(true); setError(null); try { const res = await fetch("/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ imagePath, items: readyAudios.map((item) => ({ audioPath: item.path!, audioFilename: item.file.name, metadata: item.metadata, })), }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Failed to create job"); router.push(`/jobs/${data.jobId}`); } catch (err) { setError(err instanceof Error ? err.message : "Submission failed"); setSubmitting(false); } } return (
{quota && (
{quota.remaining} of {quota.limit} videos remaining this month
)} {error && (
{error}
)}

Files

{imageUploading ? "Uploading…" : imageFile?.name || "No image selected"}
0} /> {audioItems.length === 0 ? "No audio files selected" : `${readyAudios.length} of ${audioItems.length} ready`}
{audioItems.length > 0 && (

Video details (per audio)

Each audio file gets its own metadata. Title is auto-filled from the filename.

{audioItems.map((item, index) => (

Video {index + 1}: {item.file.name}

{item.uploading && (

Uploading…

)}
updateItemMetadata(item.id, { title: e.target.value })} className="input-field" required /> updateItemMetadata(item.id, { categoryId: v })} />