import { useEffect, useRef, useState, type DragEvent } from "react"; import type { MediaType } from "../lib/types"; interface UploadPanelProps { kind: MediaType; file: File | null; error: string | null; errorTitle: string; loading: boolean; loadingMessage: string; onFileChange: (file: File | null) => void; onValidationError: (message: string | null) => void; onAnalyze: () => void; } const FILE_RULES = { image: { extensions: [".jpg", ".jpeg", ".png", ".webp"], accept: ".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp", formats: "JPG, JPEG, PNG, WEBP", maxBytes: 15 * 1024 * 1024, maxLabel: "15 MB" }, video: { extensions: [".mp4", ".mov", ".webm"], accept: ".mp4,.mov,.webm,video/mp4,video/quicktime,video/webm", formats: "MP4, MOV, WEBM", maxBytes: 80 * 1024 * 1024, maxLabel: "80 MB" } } as const; function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; const kilobytes = bytes / 1024; if (kilobytes < 1024) return `${kilobytes.toFixed(1)} KB`; return `${(kilobytes / 1024).toFixed(1)} MB`; } function formatDuration(seconds: number): string { const totalSeconds = Math.max(0, Math.round(seconds)); const minutes = Math.floor(totalSeconds / 60); const remainder = totalSeconds % 60; return `${minutes}:${remainder.toString().padStart(2, "0")}`; } function fileExtension(filename: string): string { const dotIndex = filename.lastIndexOf("."); return dotIndex >= 0 ? filename.slice(dotIndex).toLowerCase() : ""; } function validateFile(file: File, kind: MediaType): string | null { const rules = FILE_RULES[kind]; const extension = fileExtension(file.name); if (file.size === 0) return "This file is empty. Choose a file that contains media."; if (!rules.extensions.some((allowedExtension) => allowedExtension === extension)) { return `Unsupported file type. Choose ${rules.formats.toLowerCase()}.`; } if (file.size > rules.maxBytes) { return `This file is too large. ${kind === "image" ? "Images" : "Videos"} must be ${rules.maxLabel} or smaller.`; } return null; } function useMediaPreview(file: File | null, kind: MediaType) { const [previewUrl, setPreviewUrl] = useState(null); const [metadata, setMetadata] = useState(null); useEffect(() => { setPreviewUrl(null); setMetadata(null); if (!file) return; let disposed = false; let image: HTMLImageElement | null = null; let video: HTMLVideoElement | null = null; const objectUrl = URL.createObjectURL(file); setPreviewUrl(objectUrl); if (kind === "image") { image = new Image(); image.onload = () => { if (!disposed && image) setMetadata(`${image.naturalWidth} × ${image.naturalHeight}`); }; image.src = objectUrl; } else { video = document.createElement("video"); video.preload = "metadata"; video.onloadedmetadata = () => { if (!disposed && video && Number.isFinite(video.duration)) { setMetadata(formatDuration(video.duration)); } }; video.src = objectUrl; } return () => { disposed = true; if (image) image.onload = null; if (video) video.onloadedmetadata = null; URL.revokeObjectURL(objectUrl); }; }, [file, kind]); return { previewUrl, metadata }; } function UploadArrow() { return ( ); } function ForwardArrow() { return ( ); } export default function UploadPanel({ kind, file, error, errorTitle, loading, loadingMessage, onFileChange, onValidationError, onAnalyze }: UploadPanelProps) { const inputRef = useRef(null); const dragDepth = useRef(0); const [dragging, setDragging] = useState(false); const { previewUrl, metadata } = useMediaPreview(file, kind); const rules = FILE_RULES[kind]; const noun = kind === "image" ? "image" : "video"; const chooseFile = () => inputRef.current?.click(); const acceptCandidate = (candidate: File | null) => { if (!candidate) return; const validationMessage = validateFile(candidate, kind); if (validationMessage) { onFileChange(null); onValidationError(validationMessage); if (inputRef.current) inputRef.current.value = ""; return; } onValidationError(null); onFileChange(candidate); }; const handleDragEnter = (event: DragEvent) => { event.preventDefault(); if (loading) return; dragDepth.current += 1; setDragging(true); }; const handleDragLeave = (event: DragEvent) => { event.preventDefault(); dragDepth.current -= 1; if (dragDepth.current <= 0) { dragDepth.current = 0; setDragging(false); } }; const handleDrop = (event: DragEvent) => { event.preventDefault(); dragDepth.current = 0; setDragging(false); if (!loading) acceptCandidate(event.dataTransfer.files?.[0] ?? null); }; const removeFile = () => { onFileChange(null); onValidationError(null); if (inputRef.current) inputRef.current.value = ""; }; return (
event.preventDefault()} onDragLeave={handleDragLeave} onDrop={handleDrop} > { acceptCandidate(event.currentTarget.files?.[0] ?? null); event.currentTarget.value = ""; }} />

Drag and drop {noun === "image" ? "an" : "a"} {noun} here

or

{rules.formats} up to {rules.maxLabel}

{file ? (

{file.name}

{formatBytes(file.size)}{metadata ? ` · ${metadata}` : ""}

) : null}
{loading ? (
) : null} {error ? (
{errorTitle}

{error}

) : null}

Your report will appear here after analysis.

); }