feat: ajouté export PDF pour versions
- Composant ExportPdfButton avec jsPDF + html2canvas - Support multi-pages avec formatage professionnel - Intégré dans VersionPage, ListVersions et VersionTimeline - Métadonnées complètes: nom, auteur, date, statut vote - Dynamic imports pour optimiser bundle size
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
'use client'
|
||||
|
||||
import {useState} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Button from '@mui/material/Button'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'
|
||||
import {formatDate} from '@/lib/format.js'
|
||||
|
||||
export default function ExportPdfButton({versionData, size = 'medium', variant = 'outlined'}) {
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
|
||||
const handleExportPdf = async () => {
|
||||
setIsExporting(true)
|
||||
|
||||
try {
|
||||
const {default: jsPDF} = await import('jspdf')
|
||||
const {default: html2canvas} = await import('html2canvas')
|
||||
|
||||
// Créer un élément temporaire avec le contenu formaté
|
||||
const tempDiv = document.createElement('div')
|
||||
tempDiv.style.cssText = `
|
||||
padding: 40px;
|
||||
font-family: 'Roboto', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: white;
|
||||
width: 210mm;
|
||||
position: fixed;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
z-index: -1;
|
||||
`
|
||||
|
||||
const authorName = versionData.user_created?.split('-')[0] || 'Système'
|
||||
const createdAt = new Date(versionData.date_created)
|
||||
const threeDaysAgo = new Date(Date.now() - (3 * 24 * 60 * 60 * 1000))
|
||||
const voteStatus = createdAt < threeDaysAgo ? 'fermé' : 'ouvert'
|
||||
const voteColor = voteStatus === 'ouvert' ? '#2e7d32' : '#d32f2f'
|
||||
|
||||
tempDiv.innerHTML = `
|
||||
<div style="margin-bottom: 30px; border-bottom: 2px solid #1976d2; padding-bottom: 20px;">
|
||||
<h1 style="color: #1976d2; margin: 0 0 10px 0; font-size: 28px; font-weight: bold;">
|
||||
${versionData.name}
|
||||
</h1>
|
||||
<div style="margin-top: 15px;">
|
||||
<p style="margin: 5px 0; color: #666; font-size: 14px;">
|
||||
<strong>Auteur :</strong> @${authorName}
|
||||
</p>
|
||||
<p style="margin: 5px 0; color: #666; font-size: 14px;">
|
||||
<strong>Date de création :</strong> ${formatDate(versionData.date_created, 'PPpp')}
|
||||
</p>
|
||||
<p style="margin: 5px 0; color: #666; font-size: 14px;">
|
||||
<strong>Statut du vote :</strong>
|
||||
<span style="color: ${voteColor}; font-weight: bold;">${voteStatus}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h2 style="color: #333; font-size: 20px; margin-bottom: 15px; font-weight: bold;">
|
||||
Contenu
|
||||
</h2>
|
||||
<div style="white-space: pre-wrap; font-size: 14px; line-height: 1.8; text-align: justify;">
|
||||
${versionData.delta?.contenu || 'Aucun contenu disponible'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; font-size: 12px; color: #888; text-align: center;">
|
||||
Exporté depuis Konstitisyon.la le ${formatDate(new Date(), 'PPpp')}
|
||||
</div>
|
||||
`
|
||||
|
||||
document.body.append(tempDiv)
|
||||
|
||||
// Générer le canvas avec une meilleure qualité
|
||||
const canvas = await html2canvas(tempDiv, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
backgroundColor: '#ffffff',
|
||||
width: tempDiv.scrollWidth,
|
||||
height: tempDiv.scrollHeight
|
||||
})
|
||||
|
||||
tempDiv.remove()
|
||||
|
||||
// Créer le PDF
|
||||
const imgData = canvas.toDataURL('image/png', 1)
|
||||
const pdf = new jsPDF('p', 'mm', 'a4') // eslint-disable-line new-cap
|
||||
|
||||
const pageWidth = pdf.internal.pageSize.getWidth()
|
||||
const pageHeight = pdf.internal.pageSize.getHeight()
|
||||
const imgWidth = pageWidth
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width
|
||||
|
||||
let heightLeft = imgHeight
|
||||
let position = 0
|
||||
|
||||
// Ajouter la première page
|
||||
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
|
||||
heightLeft -= pageHeight
|
||||
|
||||
// Ajouter des pages supplémentaires si nécessaire
|
||||
while (heightLeft >= 0) {
|
||||
position = heightLeft - imgHeight
|
||||
pdf.addPage()
|
||||
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
|
||||
heightLeft -= pageHeight
|
||||
}
|
||||
|
||||
// Nom du fichier sécurisé
|
||||
const fileName = `${versionData.name.replaceAll(/[^a-zA-Z\d\s]/g, '').replaceAll(/\s+/g, '_')}_${formatDate(new Date(), 'yyyy-MM-dd')}.pdf`
|
||||
pdf.save(fileName)
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'export PDF:', error)
|
||||
} finally {
|
||||
setIsExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={variant}
|
||||
size={size}
|
||||
startIcon={isExporting ? <CircularProgress size={16} /> : <PictureAsPdfIcon />}
|
||||
disabled={isExporting}
|
||||
sx={{
|
||||
minWidth: size === 'small' ? 'auto' : undefined,
|
||||
'&:disabled': {
|
||||
opacity: 0.6
|
||||
}
|
||||
}}
|
||||
onClick={handleExportPdf}
|
||||
>
|
||||
{isExporting ? 'Export...' : (size === 'small' ? 'PDF' : 'Exporter PDF')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
ExportPdfButton.propTypes = {
|
||||
versionData: PropTypes.object.isRequired,
|
||||
size: PropTypes.oneOf(['small', 'medium', 'large']),
|
||||
variant: PropTypes.oneOf(['text', 'outlined', 'contained'])
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import VersionSearch from './version-search.js'
|
||||
import VersionFilters from './version-filters.js'
|
||||
import CopyButton from './copy-button.js'
|
||||
import ShareButton from './share-button.js'
|
||||
import ExportPdfButton from './export-pdf-button.js'
|
||||
import {formatDate} from '@/lib/format.js'
|
||||
import {compareVersion} from '@/lib/directus.js'
|
||||
import {filterVersions, getFilterStats} from '@/lib/version-utils.js'
|
||||
@@ -122,6 +123,11 @@ function rowContent({
|
||||
versionName={row.name}
|
||||
hasSnackbarVisible={false}
|
||||
/>
|
||||
<ExportPdfButton
|
||||
versionData={row}
|
||||
size='small'
|
||||
variant='text'
|
||||
/>
|
||||
<Button
|
||||
size='small'
|
||||
variant='outlined'
|
||||
|
||||
@@ -21,6 +21,7 @@ import {Loading} from '../loading.js'
|
||||
import Footer from '../footer.js'
|
||||
import VoteButtons from './vote-buttons.js'
|
||||
import CopyButton from './copy-button.js'
|
||||
import ExportPdfButton from './export-pdf-button.js'
|
||||
import VersionComparison from './version-comparison.js'
|
||||
import {getVersion, compareVersion} from '@/lib/directus.js'
|
||||
import {formatDate} from '@/lib/format.js'
|
||||
@@ -222,6 +223,7 @@ export default function VersionPage({session, versionId, viewMode}) {
|
||||
</Button>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<ExportPdfButton versionData={versionData} size='medium' />
|
||||
<Tooltip title='Partager cette version'>
|
||||
<IconButton color='primary' onClick={handleShare}>
|
||||
<ShareIcon />
|
||||
|
||||
@@ -27,6 +27,7 @@ import VersionDialog from './version-dialog.js'
|
||||
import VoteButtons from './vote-buttons.js'
|
||||
import CopyButton from './copy-button.js'
|
||||
import ShareButton from './share-button.js'
|
||||
import ExportPdfButton from './export-pdf-button.js'
|
||||
import {formatDate} from '@/lib/format.js'
|
||||
import {compareVersion} from '@/lib/directus.js'
|
||||
|
||||
@@ -198,6 +199,11 @@ function VersionCard({
|
||||
versionName={version.name}
|
||||
hasSnackbarVisible={false}
|
||||
/>
|
||||
<ExportPdfButton
|
||||
versionData={version}
|
||||
size='small'
|
||||
variant='text'
|
||||
/>
|
||||
<VoteButtons hasCountsVisible versionId={version.id} onVoteResult={onVoteResult} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user