'use client' import { useState, useRef } from 'react' import type { Destination } from '@/lib/constants' import { DESTINATIONS } from '@/lib/constants' import type { FolderEntry } from '@/lib/client-types' import type { FileDiff } from '@/lib/types' import { useSecret } from '@/hooks/useSecret' import { useFolderEntries } from '@/hooks/useFolderEntries' import SecretInput from './upload/SecretInput' import DestinationPicker from './upload/DestinationPicker' import FolderDropzone from './upload/FolderDropzone' import FolderCard from './upload/FolderCard' import ActionButtons from './upload/ActionButtons' import OverwriteConfirmModal from './upload/OverwriteConfirmModal' import NoChangesModal from './upload/NoChangesModal' // --------------------------------------------------------------------------- // API helpers // --------------------------------------------------------------------------- interface CheckResult { exists: boolean diffs: FileDiff[] } async function checkFolderDiffs( folder: FolderEntry, destination: string, secret: string, signal?: AbortSignal, ): Promise { try { const params = new URLSearchParams({ folderName: folder.folderName, destination }) const res = await fetch(`/api/upload/check?${params}`, { headers: { 'x-upload-secret': secret.trim() }, signal, }) const data = await res.json() if (!data.success || !data.exists) { return { exists: false, diffs: [] } } const remoteFiles: { name: string; size: number }[] = data.files || [] const remoteMap = new Map(remoteFiles.map((f) => [f.name.toLowerCase(), f.size])) const diffs: FileDiff[] = [] const localNames = new Set() // Model: skip size comparison (compression changes the size). // We only check if it exists on remote or not. const modelKey = folder.modelFile.name.toLowerCase() localNames.add(modelKey) if (!remoteMap.has(modelKey)) { diffs.push({ name: folder.modelFile.name, status: 'new' }) } // If model exists on remote → don't add to diffs (we can't know if it changed) // Textures: compare by size (not compressed, so size is reliable) for (const tex of folder.textures) { const key = tex.name.toLowerCase() localNames.add(key) const remoteSize = remoteMap.get(key) if (remoteSize === undefined) { diffs.push({ name: tex.name, status: 'new' }) } else if (remoteSize !== tex.file.size) { diffs.push({ name: tex.name, status: 'changed' }) } } // Files on remote but not in local → deleted for (const [name] of remoteMap) { if (!localNames.has(name)) { diffs.push({ name, status: 'deleted' }) } } return { exists: true, diffs } } catch { return { exists: false, diffs: [] } } } async function uploadFolder( folder: FolderEntry, secret: string, destination: string, onProgress: (pct: number) => void, signal?: AbortSignal, ): Promise<{ success: boolean; filename?: string; error?: string }> { const formData = new FormData() formData.append('folderName', folder.folderName) formData.append('destination', destination) formData.append('files', folder.modelFile) formData.append('fileTypes', 'model') formData.append('textureNames', '') 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/git', { method: 'POST', headers: { 'x-upload-secret': secret.trim() }, body: formData, signal, }) 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 (err) { if (err instanceof DOMException && err.name === 'AbortError') { return { success: false, error: 'Upload annule' } } return { success: false, error: 'Erreur reseau' } } } // --------------------------------------------------------------------------- // UploadZone — orchestrator // --------------------------------------------------------------------------- export default function UploadZone() { const { secret, secretError, secretVisible, isSecretEmpty, setSecretError, handleSecretChange, toggleSecretVisible, } = useSecret() const { entries, setEntries, updateEntry, removeEntry, resetEntries, allDone, hasErrors, } = useFolderEntries() const [isUploading, setIsUploading] = useState(false) const [globalError, setGlobalError] = useState(null) const [destination, setDestination] = useState(DESTINATIONS[0].value) const [overwriteConfirm, setOverwriteConfirm] = useState<{ folderName: string diffs: FileDiff[] } | null>(null) const [noChangesFolder, setNoChangesFolder] = useState(null) const abortRef = useRef(null) // -- Handlers -- const handleFolderSelected = (entry: FolderEntry) => { setGlobalError(null) setEntries([entry]) } const handleToggleViewer = (index: number) => { const entry = entries[index] if (entry?.modelUrl) { updateEntry(index, { viewerOpen: !entry.viewerOpen }) } } const handleUpload = async () => { if (!secret.trim()) { setSecretError("La cle d'acces est requise") return } if (entries.length === 0) return setSecretError(null) setGlobalError(null) const folder = entries[0] const check = await checkFolderDiffs(folder, destination, secret, abortRef.current?.signal) if (check.exists) { if (check.diffs.length === 0) { setNoChangesFolder(folder.folderName) return } setOverwriteConfirm({ folderName: folder.folderName, diffs: check.diffs }) return } await proceedUpload() } const proceedUpload = async () => { setOverwriteConfirm(null) setIsUploading(true) setGlobalError(null) const controller = new AbortController() abortRef.current = controller for (let i = 0; i < entries.length; i++) { if (entries[i].status === 'success') continue if (controller.signal.aborted) break updateEntry(i, { status: 'uploading', progress: 0, error: undefined }) const result = await uploadFolder( entries[i], secret, destination, (pct) => updateEntry(i, { progress: pct }), controller.signal, ) updateEntry(i, { status: result.success ? 'success' : 'error', progress: result.success ? 100 : 0, error: result.success ? undefined : result.error, filename: result.filename, }) } abortRef.current = null setIsUploading(false) } const handleCancel = () => { abortRef.current?.abort() abortRef.current = null setIsUploading(false) } const handleReset = () => { resetEntries() setGlobalError(null) setIsUploading(false) } const hasPendingOrErrors = entries.some((f) => f.status === 'pending' || f.status === 'error') // -- Render -- return (
{entries.length === 0 && ( )} {globalError && (

{globalError}

)} {entries.length > 0 && (
{entries.map((entry, i) => ( ))}
)} {overwriteConfirm && ( setOverwriteConfirm(null)} onConfirm={proceedUpload} /> )} {noChangesFolder && ( { setNoChangesFolder(null) handleReset() }} onModify={() => setNoChangesFolder(null)} /> )}
) }