'use client' import { useState } from 'react' import dynamic from 'next/dynamic' const ModelViewer = dynamic(() => import('./ModelViewer'), { ssr: false }) type FileStatus = 'pending' | 'uploading' | 'success' | 'error' interface TextureFile { name: string file: File } interface FolderEntry { folderName: string modelFile: File textures: TextureFile[] status: FileStatus progress: number error?: string filename?: string modelUrl?: string viewerOpen?: boolean warnings: string[] } const REQUIRED_TEXTURES = ['roughness', 'normal', 'metalness', 'color', 'displace'] const TEXTURE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp'] const DESTINATIONS = [ { value: 'farm', label: 'Farm' }, { value: 'map', label: 'Map' }, { value: 'powergrid', label: 'Powergrid' }, { value: 'workshop', label: 'Workshop' }, { value: 'general', label: 'General' }, { value: 'environment', label: 'Environment' }, ] as const function formatBytes(bytes: number): string { if (bytes === 0) return '0 B' const k = 1024 const sizes = ['B', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] } function getTextureType(filename: string): string | null { const name = filename.toLowerCase().replace(/\.[^.]+$/, '') if (REQUIRED_TEXTURES.includes(name)) return name return null } function validateFolder(files: File[]): { model?: File; textures: TextureFile[]; errors: string[]; warnings: string[] } { const result: { model?: File; textures: TextureFile[]; errors: string[]; warnings: string[] } = { textures: [], errors: [], warnings: [] } const modelFiles = files.filter(f => { const name = f.name.toLowerCase() return name === 'model.glb' || name === 'model.gltf' }) if (modelFiles.length === 0) { result.errors.push('model.glb ou model.gltf manquant (obligatoire)') } else { result.model = modelFiles[0] } const textureFiles = files.filter(f => { const ext = f.name.slice(f.name.lastIndexOf('.')).toLowerCase() return TEXTURE_EXTENSIONS.includes(ext) && getTextureType(f.name) !== null }) for (const tf of textureFiles) { result.textures.push({ name: tf.name, file: tf }) } const foundTextures = new Set(result.textures.map(t => t.name.toLowerCase().replace(/\.[^.]+$/, ''))) for (const req of REQUIRED_TEXTURES) { if (!foundTextures.has(req)) { result.warnings.push(`${req}.webp/png/jpg manquant`) } } return result } async function checkFolderExists( folderName: string, destination: string, secret: string, ): Promise<{ exists: boolean; files: string[] }> { try { const params = new URLSearchParams({ folderName, destination }) const res = await fetch(`/api/upload?${params}`, { headers: { 'x-upload-secret': secret.trim() }, }) const data = await res.json() if (data.success && data.exists) { return { exists: true, files: data.files || [] } } return { exists: false, files: [] } } catch { return { exists: false, files: [] } } } async function uploadFolder( folder: FolderEntry, secret: string, destination: string, onProgress: (pct: number) => void ): Promise<{ success: boolean; filename?: string; error?: string }> { const formData = new FormData() formData.append('folderName', folder.folderName) formData.append('destination', destination) // Model file formData.append('files', folder.modelFile) formData.append('fileTypes', 'model') formData.append('textureNames', '') // Texture files for (const tex of folder.textures) { formData.append('files', tex.file) formData.append('fileTypes', 'texture') formData.append('textureNames', tex.name) } onProgress(10) try { const res = await fetch('/api/upload', { method: 'POST', headers: { 'x-upload-secret': secret.trim() }, body: formData, }) onProgress(80) const data = await res.json() if (!data.success) { return { success: false, error: data.error } } onProgress(100) return { success: true, filename: folder.folderName } } catch { return { success: false, error: 'Erreur reseau' } } } export default function UploadZone() { const [files, setFiles] = useState([]) const [isUploading, setIsUploading] = useState(false) const [secret, setSecret] = useState('') const [secretVisible, setSecretVisible] = useState(false) const [globalError, setGlobalError] = useState(null) const [secretError, setSecretError] = useState(null) const [destination, setDestination] = useState(DESTINATIONS[0].value) const [abortController, setAbortController] = useState(null) const [overwriteConfirm, setOverwriteConfirm] = useState<{ folderName: string; files: string[] } | null>(null) const isSecretEmpty = !secret.trim() const updateFile = (index: number, patch: Partial) => { setFiles((prev) => prev.map((f, i) => i === index ? { ...f, ...patch } : f)) } const handleUpload = async () => { if (!secret.trim()) { setSecretError('La cle d\'acces est requise') return } if (files.length === 0) return setSecretError(null) setGlobalError(null) // Check if folder already exists on remote const folder = files[0] const check = await checkFolderExists(folder.folderName, destination, secret) if (check.exists) { setOverwriteConfirm({ folderName: folder.folderName, files: check.files }) return } await proceedUpload() } const proceedUpload = async () => { setOverwriteConfirm(null) setIsUploading(true) setGlobalError(null) for (let i = 0; i < files.length; i++) { if (files[i].status === 'success') continue updateFile(i, { status: 'uploading', progress: 0, error: undefined }) const result = await uploadFolder( files[i], secret, destination, (pct) => updateFile(i, { progress: pct }) ) updateFile(i, { status: result.success ? 'success' : 'error', progress: result.success ? 100 : 0, error: result.success ? undefined : result.error, filename: result.filename, }) } setIsUploading(false) } const handleCancel = () => { abortController?.abort() } const removeFile = (index: number) => { const file = files[index] if (file.modelUrl) URL.revokeObjectURL(file.modelUrl) setFiles((prev) => prev.filter((_, i) => i !== index)) } const handleReset = () => { files.forEach((f) => { if (f.modelUrl) URL.revokeObjectURL(f.modelUrl) }) setFiles([]) setGlobalError(null) setIsUploading(false) } const allDone = files.length > 0 && files.every((f) => f.status === 'success') const hasErrors = files.some((f) => f.status === 'error') return (
{ setSecret(e.target.value) if (secretError) setSecretError(null) }} placeholder="Entrez la cle secrete..." disabled={isUploading} className={`w-full bg-black-800 border rounded-xl px-4 py-2.5 pr-12 text-gray-100 placeholder-gray-500 text-sm focus:outline-none focus:ring-2 focus:border-white/50 disabled:opacity-50 disabled:cursor-not-allowed transition ${secretError ? 'border-red-500/70 focus:ring-red-500/50' : 'border-white/30 focus:ring-white/50' }`} />
{secretError && (

{secretError}

)}
{DESTINATIONS.map((dest) => ( ))}
)} multiple className="hidden" onChange={(e) => { const files = e.target.files if (files && files.length > 0) { const fileArray = Array.from(files) const folderName = fileArray[0].webkitRelativePath?.split('/')[0] || 'folder' const validation = validateFolder(fileArray) if (validation.errors.length > 0) { setGlobalError(validation.errors.join(' | ')) return } setGlobalError(null) const entry: FolderEntry = { folderName, modelFile: validation.model!, textures: validation.textures, status: 'pending', progress: 0, warnings: validation.warnings, modelUrl: URL.createObjectURL(validation.model!), viewerOpen: true, } setFiles([entry]) } }} /> {files.length === 0 && (
document.getElementById('folder-input')?.click()} className={` relative border-2 border-dashed rounded-2xl p-8 text-center cursor-pointer transition-all duration-200 bg-black-800 ${isUploading ? 'cursor-not-allowed opacity-60 border-white/20' : ''} ${!isUploading ? 'border-white/30 hover:border-white/50 hover:bg-black-700' : ''} `} >

Deposez votre dossier ici ou cliquez pour parcourir

Contenu attendu : model.glb/gltf + textures (roughness, normal, metalness, color, displace)

)} {globalError && (

{globalError}

)} {files.length > 0 && (
{files.map((entry, i) => (
{entry.status === 'success' ? (
) : entry.status === 'error' ? (
) : entry.status === 'uploading' ? (
) : ( )}
{entry.folderName}/ Dossier
modele : {entry.modelFile.name} {entry.status === 'error' && entry.error && ( {entry.error} )} {entry.status === 'success' && entry.filename && ( {entry.filename} )}
{entry.status === 'uploading' && (
)}
{entry.status !== 'uploading' && ( )}
{entry.warnings.length > 0 && entry.status !== 'success' && (
Textures manquantes : {entry.warnings.join(', ')}
)} {entry.modelUrl && entry.status !== 'success' && (
)}
))}
)}
{!isUploading && files.some((f) => f.status === 'pending' || f.status === 'error') && ( )} {isUploading && ( )} {(allDone || hasErrors) && !isUploading && ( )}
{overwriteConfirm && (

Dossier deja existant

{destination}/{overwriteConfirm.folderName} existe deja sur le repo.

{overwriteConfirm.files.length > 0 && (

Fichiers existants qui seront remplaces :

    {overwriteConfirm.files.map((f) => (
  • {f}
  • ))}
)}

Les anciens fichiers seront supprimes et remplaces par les nouveaux. Cette action est irreversible.

)}
) }