Files
upload-gltf/components/upload/FolderCard.tsx
T
Tom Boullay 78f4aa83e0 refactor: full codebase audit — extract modules, fix type safety, clean dead code
- Extract API helpers from UploadZone into lib/upload-api.ts (FormData builder, checkFolderDiffs, uploadDrive, uploadGit)
- Extract upload orchestration into hooks/useUploadOrchestrator.ts (UploadZone: 489 → 162 lines)
- Extract file diff classification into lib/diff-files.ts (from git route)
- Extract shared SVG icons into components/ui/icons.tsx (7 icons, 0 duplication)
- Extract shared modal wrapper into components/ui/Modal.tsx + ModalActions
- Extract DriveStatusLine sub-component from FolderCard
- Fix checkFolderDiffs silently swallowing auth/network errors (now throws)
- Fix type safety: remove as never casts, add isHttpError type guard, use discriminated union for validateFolder
- Fix nextcloud: cache getConfig, add max bound to findNextVersion, optimize mkdirRecursive (skip PROPFIND)
- Fix drive route: remove req.clone(), extend parseMultiUpload to return extra fields
- Fix commit message: model shown as unchanged with ↔️ on updates (not falsely marked as modified)
- Clean dead code: unused folderExists import, FileStatus/DriveStatus exports, ParsedFile.textureName, getConfig basePath
- Add security headers in next.config.ts (HSTS, X-Content-Type-Options, X-Frame-Options, etc.)
- Update README with new project structure
2026-04-14 17:19:10 +02:00

113 lines
4.5 KiB
TypeScript

import dynamic from 'next/dynamic'
import type { FolderEntry } from '@/lib/client-types'
import { formatBytes } from '@/lib/format-bytes'
import { SpinnerIcon, CheckIcon, XIcon, ChevronIcon } from '@/components/ui/icons'
import DriveStatusLine from './DriveStatusLine'
import WarningBanner from './WarningBanner'
const ModelViewer = dynamic(() => import('../ModelViewer'), { ssr: false })
interface FolderCardProps {
entry: FolderEntry
index: number
onToggleViewer: (index: number) => void
onRemove: (index: number) => void
}
export default function FolderCard({ entry, index, onToggleViewer, onRemove }: FolderCardProps) {
return (
<div>
<div className="flex items-center gap-3 bg-black-800 border border-white/20 rounded-xl px-4 py-3">
{/* Status icon */}
<div className="shrink-0">
{entry.status === 'success' ? (
<div className="w-8 h-8 rounded-full bg-green-900/30 flex items-center justify-center">
<CheckIcon className="w-4 h-4 text-green-400" />
</div>
) : entry.status === 'error' ? (
<div className="w-8 h-8 rounded-full bg-red-900/30 flex items-center justify-center">
<XIcon className="w-4 h-4 text-red-400" />
</div>
) : entry.status === 'uploading' ? (
<div className="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center">
<SpinnerIcon className="w-4 h-4 text-gray-300" />
</div>
) : (
<button
onClick={() => entry.modelUrl ? onToggleViewer(index) : undefined}
aria-label={entry.viewerOpen ? 'Fermer la preview 3D' : 'Ouvrir la preview 3D'}
className={`shrink-0 w-8 h-8 rounded-full flex items-center justify-center transition ${
entry.modelUrl ? 'bg-black-700 hover:bg-gray-700 cursor-pointer' : 'bg-black-700 cursor-default'
}`}
>
<ChevronIcon className={`w-4 h-4 text-gray-500 transition-transform ${entry.viewerOpen ? 'rotate-180' : ''}`} />
</button>
)}
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-200 font-mono truncate">{entry.folderName}/</span>
<span className="shrink-0 text-xs px-1.5 py-0.5 rounded-full bg-gray-700 text-gray-300">Dossier</span>
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-gray-500">model : {entry.modelFile.name}</span>
{entry.status === 'error' && entry.error && (
<span className="text-xs text-red-400 truncate">{entry.error}</span>
)}
{entry.status === 'success' && entry.filename && (
<span className="text-xs text-green-400 font-mono">Drive + Git OK</span>
)}
</div>
{/* Drive status sub-line (only during upload, not after success) */}
{entry.status !== 'success' && entry.driveStatus && entry.driveStatus !== 'pending' && (
<DriveStatusLine driveStatus={entry.driveStatus} driveError={entry.driveError} />
)}
{entry.status === 'uploading' && (
<div className="mt-1.5 w-full h-1 bg-black-700 rounded-full overflow-hidden">
<div
className="h-full bg-gray-400 rounded-full transition-all duration-200"
style={{ width: `${entry.progress}%` }}
/>
</div>
)}
</div>
{/* Remove button */}
{entry.status !== 'uploading' && (
<button
onClick={() => onRemove(index)}
aria-label="Supprimer le dossier"
className="shrink-0 text-gray-600 hover:text-red-400 transition"
>
<XIcon className="w-4 h-4" />
</button>
)}
</div>
{/* Warning banner */}
{entry.status !== 'success' && (
<WarningBanner warnings={entry.warnings} />
)}
{/* 3D preview */}
{entry.modelUrl && entry.status !== 'success' && (
<div
className={`transition-all duration-300 ease-in-out overflow-hidden ${
entry.viewerOpen ? 'max-h-[500px] opacity-100 mt-2' : 'max-h-0 opacity-0 pointer-events-none'
}`}
>
<ModelViewer
url={entry.modelUrl}
filename={entry.modelFile.name}
size={formatBytes(entry.modelFile.size)}
/>
</div>
)}
</div>
)
}