diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f3c1c62 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +.next +.git +*.md +.env* +.DS_Store diff --git a/app/globals.css b/app/globals.css index 688a985..1069260 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,13 +1,10 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap'); - @tailwind base; @tailwind components; @tailwind utilities; @layer base { html { - font-family: 'Inter', system-ui, sans-serif; + font-family: var(--font-inter), system-ui, sans-serif; } body { diff --git a/app/layout.tsx b/app/layout.tsx index 1a9aa47..dfd8302 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,8 +1,21 @@ import type { Metadata } from 'next' +import { Inter, JetBrains_Mono } from 'next/font/google' import './globals.css' +const inter = Inter({ + subsets: ['latin'], + variable: '--font-inter', + display: 'swap', +}) + +const jetbrainsMono = JetBrains_Mono({ + subsets: ['latin'], + variable: '--font-jetbrains-mono', + display: 'swap', +}) + export const metadata: Metadata = { - title: ' Upload GLTF', + title: 'Upload GLTF', description: 'Interface de depot securise pour fichiers 3D (.glb, .gltf) avec versionnement automatique sur GitHub', } @@ -12,7 +25,7 @@ export default function RootLayout({ children: React.ReactNode }) { return ( - +
{children} ) diff --git a/components/UploadZone.tsx b/components/UploadZone.tsx index 98fe6f0..3796fb7 100644 --- a/components/UploadZone.tsx +++ b/components/UploadZone.tsx @@ -1,94 +1,23 @@ 'use client' -import { useState } from 'react' -import dynamic from 'next/dynamic' +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' -const ModelViewer = dynamic(() => import('./ModelViewer'), { ssr: false }) +// --------------------------------------------------------------------------- +// Client-side SHA computation (same as `git hash-object`) +// --------------------------------------------------------------------------- -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 -} - -/** Compute the git blob SHA1 for a file (same as `git hash-object`) */ async function computeGitBlobSha(file: File): Promise{secretError}
- )} -- Deposez votre dossier ici - ou cliquez pour parcourir -
-- Contenu attendu : model.glb/gltf + textures (roughness, normal, metalness, color, displace) -
-{globalError}
)} - {files.length > 0 && ( + {entries.length > 0 && (- {destination}/{overwriteConfirm.folderName} existe deja sur le repo -
-Modifications detectees :
-+ Deposez votre dossier ici + ou cliquez pour parcourir +
++ Contenu attendu : model.glb/gltf + textures (roughness, normal, metalness, color, displace) +
++ {destination}/{folderName} existe deja sur le repo +
+Modifications detectees :
+{secretError}
+ )} +