diff --git a/client/src/components/FileUpload.tsx b/client/src/components/FileUpload.tsx index 5db97bb..efc471f 100644 --- a/client/src/components/FileUpload.tsx +++ b/client/src/components/FileUpload.tsx @@ -1,8 +1,8 @@ import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { X, Upload, FileText } from "lucide-react"; import { useState } from "react"; +import { toast } from "sonner"; interface FileUploadProps { value?: { @@ -26,55 +26,82 @@ interface FileUploadProps { export function FileUpload({ value, onChange, accept, maxSize = 10, label = "Pièce jointe (optionnel)" }: FileUploadProps) { const [uploading, setUploading] = useState(false); - const [error, setError] = useState(null); + const [uploadProgress, setUploadProgress] = useState(0); + const [selectedFileName, setSelectedFileName] = useState(null); - const handleFileChange = async (e: React.ChangeEvent) => { + const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; + // Afficher IMMÉDIATEMENT le nom du fichier sélectionné + setSelectedFileName(file.name); + // Vérifier la taille const fileSizeMB = file.size / (1024 * 1024); if (fileSizeMB > maxSize) { - setError(`Le fichier est trop volumineux (max ${maxSize} Mo)`); + toast.error(`Le fichier est trop volumineux (max ${maxSize} Mo)`); + setSelectedFileName(null); + e.target.value = ''; return; } - setError(null); + // Afficher immédiatement l'état de chargement setUploading(true); + setUploadProgress(0); - try { - const formData = new FormData(); - formData.append("file", file); + // Utiliser requestAnimationFrame pour garantir que le navigateur + // affiche l'animation AVANT de bloquer sur le fetch + requestAnimationFrame(() => { + requestAnimationFrame(async () => { + try { + const formData = new FormData(); + formData.append("file", file); - const response = await fetch("/api/upload-file", { - method: "POST", - body: formData, + const progressInterval = setInterval(() => { + setUploadProgress(prev => { + if (prev >= 90) return prev; + return prev + 10; + }); + }, 200); + + const response = await fetch("/api/upload-file", { + method: "POST", + body: formData, + }); + + clearInterval(progressInterval); + setUploadProgress(100); + + if (!response.ok) { + throw new Error("Erreur lors de l'upload"); + } + + const data = await response.json(); + + onChange({ + nomFichier: file.name, + urlFichier: data.url, + s3Key: data.key, + typeFichier: file.type, + tailleFichier: file.size, + }); + + toast.success("Fichier uploadé avec succès"); + } catch (err) { + console.error("Erreur upload:", err); + toast.error("Erreur lors de l'upload du fichier"); + setUploadProgress(0); + } finally { + setUploading(false); + setSelectedFileName(null); + } }); - - if (!response.ok) { - throw new Error("Erreur lors de l'upload"); - } - - const data = await response.json(); - - onChange({ - nomFichier: file.name, - urlFichier: data.url, - s3Key: data.key, - typeFichier: file.type, - tailleFichier: file.size, - }); - } catch (err) { - console.error("Erreur upload:", err); - setError("Erreur lors de l'upload du fichier"); - } finally { - setUploading(false); - } + }); }; const handleRemove = () => { onChange(null); - setError(null); + setSelectedFileName(null); }; const formatFileSize = (bytes: number) => { @@ -87,37 +114,80 @@ export function FileUpload({ value, onChange, accept, maxSize = 10, label = "Pi
- {!value ? ( -
-
- - {uploading && ( -
-
- Upload en cours... + {!value && !uploading ? ( + + ) : uploading ? ( +
+
+
+ +
+
+

+ Upload en cours... +

+ {selectedFileName && ( +

+ {selectedFileName} +

+ )} +
+
+ Progression + {uploadProgress}% +
+
+
+
+
+
- {error && ( -

{error}

- )} -

- Taille maximale : {maxSize} Mo - {accept && ` • Formats acceptés : ${accept}`} -

) : ( -
- +
+
+ +
-

{value.nomFichier}

-

+

✓ Fichier prêt

+

{value.nomFichier}

+

{formatFileSize(value.tailleFichier)}

@@ -126,9 +196,9 @@ export function FileUpload({ value, onChange, accept, maxSize = 10, label = "Pi variant="ghost" size="icon" onClick={handleRemove} - className="flex-shrink-0" + className="flex-shrink-0 text-gray-500 hover:text-red-600 hover:bg-red-50" > - +
)} diff --git a/todo.md b/todo.md index 3e3fd73..0973f31 100644 --- a/todo.md +++ b/todo.md @@ -1480,3 +1480,5 @@ - [x] Corriger la latence avec requestAnimationFrame pour affichage avant fetch bloquant - [x] Afficher immédiatement le nom du fichier sélectionné dans la boîte de dialogue + +- [x] Appliquer la même ergonomie de boîte de dialogue pour les pièces jointes des rappels