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 (
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user