Checkpoint: Interface modernisée de chargement d'attestations avec design coloré (dégradés bleu-violet), icônes grandes et expressives, zone de drop stylisée, et meilleure hiérarchie visuelle. Déployé sur le VPS de production.

This commit is contained in:
Manus
2026-01-30 08:04:36 -05:00
parent 806366db01
commit 7032fd890f
2 changed files with 182 additions and 87 deletions

View File

@@ -58,103 +58,196 @@ function UploadAttestationDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Charger une attestation</DialogTitle>
<DialogDescription>
Sélectionnez un fichier PDF à uploader pour cet apprenant
<DialogContent className="sm:max-w-[500px]">
<DialogHeader className="space-y-3">
<div className="mx-auto w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl flex items-center justify-center shadow-lg">
<Upload className="w-8 h-8 text-white" />
</div>
<DialogTitle className="text-2xl font-bold text-center bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Charger une attestation
</DialogTitle>
<DialogDescription className="text-center text-base">
Sélectionnez un fichier PDF pour cet apprenant
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Fichier PDF</Label>
<input
type="file"
accept=".pdf"
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
if (file.type !== 'application/pdf') {
toast.error('Seuls les fichiers PDF sont acceptés');
return;
<div className="space-y-6 py-4">
<div className="relative">
<label className="block">
<div className={`
relative border-3 border-dashed rounded-2xl p-8 text-center cursor-pointer
transition-all duration-300 ease-in-out
${
uploading
? 'border-blue-400 bg-blue-50'
: fileData
? 'border-green-400 bg-green-50 hover:bg-green-100'
: 'border-gray-300 bg-gray-50 hover:border-blue-400 hover:bg-blue-50'
}
`}>
{!fileData && !uploading && (
<div className="space-y-4">
<div className="mx-auto w-20 h-20 bg-gradient-to-br from-blue-100 to-purple-100 rounded-full flex items-center justify-center">
<FileText className="w-10 h-10 text-blue-600" />
</div>
<div>
<p className="text-lg font-semibold text-gray-700 mb-1">
Cliquez pour choisir un fichier
</p>
<p className="text-sm text-gray-500">
ou glissez-déposez votre PDF ici
</p>
</div>
<div className="flex items-center justify-center gap-2 text-xs text-gray-400">
<span className="px-3 py-1 bg-white rounded-full border border-gray-200">
PDF uniquement
</span>
<span className="px-3 py-1 bg-white rounded-full border border-gray-200">
Max 10 Mo
</span>
</div>
</div>
)}
if (file.size > 10 * 1024 * 1024) {
toast.error('Le fichier ne doit pas dépasser 10 Mo');
return;
}
{uploading && (
<div className="space-y-4">
<div className="mx-auto w-20 h-20 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center animate-pulse">
<Upload className="w-10 h-10 text-white" />
</div>
<div>
<p className="text-lg font-semibold text-blue-600 mb-2">
Upload en cours...
</p>
<div className="w-full max-w-xs mx-auto">
<div className="flex justify-between text-sm text-gray-600 mb-2">
<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-3 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>
)}
setUploading(true);
setUploadProgress(0);
try {
const formData = new FormData();
formData.append('file', file);
{fileData && !uploading && (
<div className="space-y-4">
<div className="mx-auto w-20 h-20 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full flex items-center justify-center">
<FileText className="w-10 h-10 text-white" />
</div>
<div>
<p className="text-lg font-semibold text-green-600 mb-1">
Fichier prêt
</p>
<p className="text-sm text-gray-600 font-medium break-all px-4">
{fileData.nomFichier}
</p>
<p className="text-xs text-gray-400 mt-2">
{(fileData.tailleFichier / 1024 / 1024).toFixed(2)} Mo
</p>
</div>
<Button
variant="ghost"
size="sm"
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
onClick={(e) => {
e.preventDefault();
setFileData(null);
}}
>
Changer de fichier
</Button>
</div>
)}
</div>
// Simuler la progression de l'upload
const progressInterval = setInterval(() => {
setUploadProgress(prev => {
if (prev >= 90) return prev;
return prev + 10;
});
}, 200);
<input
type="file"
accept=".pdf"
className="hidden"
disabled={uploading}
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
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');
if (file.type !== 'application/pdf') {
toast.error('Seuls les fichiers PDF sont acceptés');
return;
}
const data = await response.json();
setFileData({
nomFichier: file.name,
urlFichier: data.url,
s3Key: data.key,
typeFichier: file.type,
tailleFichier: file.size,
});
toast.success('Fichier uploadé avec succès');
} catch (error) {
toast.error('Erreur lors de l\'upload du fichier');
if (file.size > 10 * 1024 * 1024) {
toast.error('Le fichier ne doit pas dépasser 10 Mo');
return;
}
setUploading(true);
setUploadProgress(0);
} finally {
setUploading(false);
}
}}
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-primary file:text-primary-foreground hover:file:bg-primary/90"
/>
{uploading && (
<div className="mt-4">
<div className="flex justify-between text-sm text-muted-foreground mb-1">
<span>Upload en cours...</span>
<span>{uploadProgress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
/>
</div>
</div>
)}
{fileData && !uploading && (
<p className="text-sm text-muted-foreground mt-2">
Fichier sélectionné : {fileData.nomFichier}
</p>
)}
try {
const formData = new FormData();
formData.append('file', file);
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();
setFileData({
nomFichier: file.name,
urlFichier: data.url,
s3Key: data.key,
typeFichier: file.type,
tailleFichier: file.size,
});
toast.success('Fichier uploadé avec succès');
} catch (error) {
toast.error('Erreur lors de l\'upload du fichier');
setUploadProgress(0);
} finally {
setUploading(false);
}
}}
/>
</label>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={uploading}>
<div className="flex justify-end gap-3 pt-2">
<Button
variant="outline"
onClick={() => {
onOpenChange(false);
setFileData(null);
setUploadProgress(0);
}}
disabled={uploading}
className="px-6 border-2 hover:bg-gray-50"
>
Annuler
</Button>
<Button onClick={handleUpload} disabled={!fileData || uploading}>
{uploading ? 'Chargement...' : 'Valider'}
<Button
onClick={handleUpload}
disabled={!fileData || uploading}
className="px-8 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold shadow-lg hover:shadow-xl transition-all duration-300"
>
{uploading ? 'Chargement...' : '✓ Valider'}
</Button>
</div>
</div>

View File

@@ -1460,3 +1460,5 @@
- [x] Intégrer le template dans le code d'envoi des attestations
- [x] Déployer sur VPS (migration SQL + fichiers)
- [x] Créer un checkpoint
- [x] Moderniser l'interface de chargement des attestations (design coloré et ergonomique)