import React, { useMemo, useRef, useState } from 'react' function formatBytes(bytes) { const value = Number(bytes || 0) if (!Number.isFinite(value) || value <= 0) return null if (value < 1024 * 1024) return `${Math.round(value / 1024)} KB` return `${(value / (1024 * 1024)).toFixed(1)} MB` } export default function WorldMediaUploadField({ label, slot, value, previewUrl, emptyLabel, helperText, uploadUrl, deleteUrl, worldId = null, onChange, isTemporaryValue = false, accept = 'image/jpeg,image/png,image/webp', maxFileSizeMb = 6, }) { const inputRef = useRef(null) const [dragging, setDragging] = useState(false) const [uploading, setUploading] = useState(false) const [error, setError] = useState('') const [meta, setMeta] = useState(null) const csrfToken = useMemo(() => { if (typeof document === 'undefined') { return '' } return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' }, []) const deleteTemporaryUpload = async (path) => { if (!deleteUrl || !path) return const response = await fetch(deleteUrl, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken, Accept: 'application/json', }, credentials: 'same-origin', body: JSON.stringify({ path, world_id: worldId || undefined, }), }) const payload = await response.json().catch(() => ({})) if (!response.ok) { throw new Error(payload?.message || payload?.error || 'Could not remove uploaded image.') } } const handleFile = async (file) => { if (!file || uploading) return const allowed = ['image/jpeg', 'image/png', 'image/webp'] if (!allowed.includes(String(file.type || '').toLowerCase())) { setError('Use a JPG, PNG, or WEBP image.') return } if (file.size > maxFileSizeMb * 1024 * 1024) { setError(`Image is too large. Maximum allowed size is ${maxFileSizeMb} MB.`) return } setUploading(true) setError('') try { if (value && isTemporaryValue) { await deleteTemporaryUpload(value) } const body = new FormData() body.append('slot', slot) body.append('image', file) if (worldId) { body.append('world_id', String(worldId)) } const response = await fetch(uploadUrl, { method: 'POST', headers: { 'X-CSRF-TOKEN': csrfToken, Accept: 'application/json', }, credentials: 'same-origin', body, }) const payload = await response.json().catch(() => ({})) if (!response.ok) { throw new Error(payload?.message || payload?.error || 'Upload failed.') } setMeta({ width: payload?.width || null, height: payload?.height || null, size: formatBytes(payload?.size_bytes), }) onChange?.({ path: payload?.path || '', url: payload?.url || '' }) } catch (uploadError) { setError(uploadError?.message || 'Upload failed.') } finally { setUploading(false) if (inputRef.current) { inputRef.current.value = '' } } } return (
{label} {value ? ( ) : null}
!uploading && inputRef.current?.click()} onKeyDown={(event) => { if (uploading) return if (event.key === 'Enter' || event.key === ' ') { event.preventDefault() inputRef.current?.click() } }} onDragOver={(event) => { event.preventDefault() if (!uploading) setDragging(true) }} onDragEnter={(event) => { event.preventDefault() if (!uploading) setDragging(true) }} onDragLeave={(event) => { event.preventDefault() setDragging(false) }} onDrop={(event) => { event.preventDefault() setDragging(false) void handleFile(event.dataTransfer?.files?.[0]) }} className={[ 'rounded-[24px] border border-dashed px-5 py-5 transition outline-none', uploading ? 'cursor-progress border-sky-300/35 bg-sky-400/10' : dragging ? 'cursor-pointer border-sky-300/50 bg-sky-400/12' : 'cursor-pointer border-white/10 bg-black/20 hover:border-white/20 hover:bg-white/[0.04]', ].join(' ')} >
{uploading ? 'Uploading image…' : 'Drop image here or browse'}
{helperText}
JPG PNG WEBP Max {maxFileSizeMb} MB
{previewUrl ? ( ) : (
{emptyLabel}
)}
{value ?
Stored path: {value}
: null} {meta ?
Optimized to {meta.width}×{meta.height}{meta.size ? ` • ${meta.size}` : ''}
: null} {error ?
{error}
: null} { void handleFile(event.target.files?.[0]) }} />
) }