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:
@@ -58,103 +58,196 @@ function UploadAttestationDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent className="sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader className="space-y-3">
|
||||||
<DialogTitle>Charger une attestation</DialogTitle>
|
<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">
|
||||||
<DialogDescription>
|
<Upload className="w-8 h-8 text-white" />
|
||||||
Sélectionnez un fichier PDF à uploader pour cet apprenant
|
</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>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</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') {
|
<div className="space-y-6 py-4">
|
||||||
toast.error('Seuls les fichiers PDF sont acceptés');
|
<div className="relative">
|
||||||
return;
|
<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) {
|
{uploading && (
|
||||||
toast.error('Le fichier ne doit pas dépasser 10 Mo');
|
<div className="space-y-4">
|
||||||
return;
|
<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);
|
{fileData && !uploading && (
|
||||||
setUploadProgress(0);
|
<div className="space-y-4">
|
||||||
try {
|
<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">
|
||||||
const formData = new FormData();
|
<FileText className="w-10 h-10 text-white" />
|
||||||
formData.append('file', file);
|
</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
|
<input
|
||||||
const progressInterval = setInterval(() => {
|
type="file"
|
||||||
setUploadProgress(prev => {
|
accept=".pdf"
|
||||||
if (prev >= 90) return prev;
|
className="hidden"
|
||||||
return prev + 10;
|
disabled={uploading}
|
||||||
});
|
onChange={async (e) => {
|
||||||
}, 200);
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
const response = await fetch('/api/upload-file', {
|
if (file.type !== 'application/pdf') {
|
||||||
method: 'POST',
|
toast.error('Seuls les fichiers PDF sont acceptés');
|
||||||
body: formData,
|
return;
|
||||||
});
|
|
||||||
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
setUploadProgress(100);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Erreur lors de l\'upload');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
if (file.size > 10 * 1024 * 1024) {
|
||||||
setFileData({
|
toast.error('Le fichier ne doit pas dépasser 10 Mo');
|
||||||
nomFichier: file.name,
|
return;
|
||||||
urlFichier: data.url,
|
}
|
||||||
s3Key: data.key,
|
|
||||||
typeFichier: file.type,
|
setUploading(true);
|
||||||
tailleFichier: file.size,
|
|
||||||
});
|
|
||||||
toast.success('Fichier uploadé avec succès');
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Erreur lors de l\'upload du fichier');
|
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
} finally {
|
try {
|
||||||
setUploading(false);
|
const formData = new FormData();
|
||||||
}
|
formData.append('file', file);
|
||||||
}}
|
|
||||||
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"
|
const progressInterval = setInterval(() => {
|
||||||
/>
|
setUploadProgress(prev => {
|
||||||
{uploading && (
|
if (prev >= 90) return prev;
|
||||||
<div className="mt-4">
|
return prev + 10;
|
||||||
<div className="flex justify-between text-sm text-muted-foreground mb-1">
|
});
|
||||||
<span>Upload en cours...</span>
|
}, 200);
|
||||||
<span>{uploadProgress}%</span>
|
|
||||||
</div>
|
const response = await fetch('/api/upload-file', {
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
method: 'POST',
|
||||||
<div
|
body: formData,
|
||||||
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
|
});
|
||||||
style={{ width: `${uploadProgress}%` }}
|
|
||||||
/>
|
clearInterval(progressInterval);
|
||||||
</div>
|
setUploadProgress(100);
|
||||||
</div>
|
|
||||||
)}
|
if (!response.ok) {
|
||||||
{fileData && !uploading && (
|
throw new Error('Erreur lors de l\'upload');
|
||||||
<p className="text-sm text-muted-foreground mt-2">
|
}
|
||||||
Fichier sélectionné : {fileData.nomFichier}
|
|
||||||
</p>
|
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>
|
||||||
<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
|
Annuler
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleUpload} disabled={!fileData || uploading}>
|
<Button
|
||||||
{uploading ? 'Chargement...' : 'Valider'}
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
todo.md
2
todo.md
@@ -1460,3 +1460,5 @@
|
|||||||
- [x] Intégrer le template dans le code d'envoi des attestations
|
- [x] Intégrer le template dans le code d'envoi des attestations
|
||||||
- [x] Déployer sur VPS (migration SQL + fichiers)
|
- [x] Déployer sur VPS (migration SQL + fichiers)
|
||||||
- [x] Créer un checkpoint
|
- [x] Créer un checkpoint
|
||||||
|
|
||||||
|
- [x] Moderniser l'interface de chargement des attestations (design coloré et ergonomique)
|
||||||
|
|||||||
Reference in New Issue
Block a user