Checkpoint: Unification de l'ergonomie de chargement de fichiers dans toute l'application : le composant FileUpload utilise maintenant la même interface élégante que la boîte de dialogue d'attestations. Affichage immédiat du nom de fichier, animation de chargement avec barre de progression, feedback visuel cohérent (gris → bleu → vert). Cette amélioration s'applique automatiquement à toutes les pièces jointes des rappels (2 par rappel). Déployé sur le VPS de production.
This commit is contained in:
@@ -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<string | null>(null);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [selectedFileName, setSelectedFileName] = useState<string | null>(null);
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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
|
||||
<div className="space-y-2">
|
||||
<Label>{label}</Label>
|
||||
|
||||
{!value ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
accept={accept}
|
||||
disabled={uploading}
|
||||
className="flex-1"
|
||||
/>
|
||||
{uploading && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full" />
|
||||
Upload en cours...
|
||||
{!value && !uploading ? (
|
||||
<label className="block">
|
||||
<div className="relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all duration-300 ease-in-out border-gray-300 bg-gray-50 hover:border-blue-400 hover:bg-blue-50">
|
||||
<div className="space-y-3">
|
||||
<div className="mx-auto w-14 h-14 bg-gradient-to-br from-blue-100 to-purple-100 rounded-full flex items-center justify-center">
|
||||
<FileText className="w-7 h-7 text-blue-600" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-700 mb-1">
|
||||
Cliquez pour choisir un fichier
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
ou glissez-déposez votre fichier ici
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 text-xs text-gray-400">
|
||||
{accept && (
|
||||
<span className="px-2 py-1 bg-white rounded-full border border-gray-200">
|
||||
{accept}
|
||||
</span>
|
||||
)}
|
||||
<span className="px-2 py-1 bg-white rounded-full border border-gray-200">
|
||||
Max {maxSize} Mo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept={accept}
|
||||
className="hidden"
|
||||
disabled={uploading}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</label>
|
||||
) : uploading ? (
|
||||
<div className="relative border-2 border-dashed rounded-xl p-6 text-center border-blue-400 bg-blue-50">
|
||||
<div className="space-y-3">
|
||||
<div className="mx-auto w-14 h-14 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center animate-pulse">
|
||||
<Upload className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-blue-600 mb-2">
|
||||
Upload en cours...
|
||||
</p>
|
||||
{selectedFileName && (
|
||||
<p className="text-xs text-gray-600 font-medium mb-3 px-4 break-all">
|
||||
{selectedFileName}
|
||||
</p>
|
||||
)}
|
||||
<div className="w-full max-w-xs mx-auto">
|
||||
<div className="flex justify-between text-xs text-gray-600 mb-1">
|
||||
<span className="font-medium">Progression</span>
|
||||
<span className="font-bold text-blue-600">{uploadProgress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 overflow-hidden shadow-inner">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-blue-500 to-purple-600 rounded-full transition-all duration-300 ease-out shadow-lg"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Taille maximale : {maxSize} Mo
|
||||
{accept && ` • Formats acceptés : ${accept}`}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 p-3 border rounded-lg bg-muted/50">
|
||||
<FileText className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||
<div className="flex items-center gap-3 p-4 border-2 rounded-xl bg-green-50 border-green-400">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<FileText className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{value.nomFichier}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-sm font-semibold text-green-600 mb-1">✓ Fichier prêt</p>
|
||||
<p className="text-sm text-gray-700 font-medium truncate">{value.nomFichier}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{formatFileSize(value.tailleFichier)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -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"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user