2025-07-23 20:31:55 +04:00
|
|
|
'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'
|
|
|
|
|
|
2025-07-23 20:35:41 +04:00
|
|
|
// Helper function to render markdown to HTML
|
|
|
|
|
const renderMarkdownToHtml = async content => {
|
|
|
|
|
if (!content) {
|
|
|
|
|
return 'Aucun contenu disponible'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if content contains markdown syntax
|
|
|
|
|
const hasMarkdown = content.includes('**')
|
|
|
|
|
|| content.includes('*')
|
|
|
|
|
|| content.includes('#')
|
|
|
|
|
|| content.includes('[')
|
|
|
|
|
|| content.includes('`')
|
|
|
|
|
|| content.includes('> ')
|
|
|
|
|
|| content.includes('- ')
|
|
|
|
|
|| content.includes('1. ')
|
|
|
|
|
|
|
|
|
|
if (!hasMarkdown) {
|
|
|
|
|
// Simple text with line breaks
|
|
|
|
|
return content.replaceAll('\n', '<br>')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Dynamic import of markdown parser
|
|
|
|
|
const {marked} = await import('marked')
|
|
|
|
|
// Configure marked for better PDF rendering
|
|
|
|
|
marked.setOptions({
|
|
|
|
|
breaks: true, // Convert \n to <br>
|
|
|
|
|
gfm: true, // GitHub flavored markdown
|
|
|
|
|
headerIds: false, // No header IDs needed for PDF
|
|
|
|
|
mangle: false // Don't mangle email addresses
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return marked(content)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Failed to parse markdown, falling back to plain text:', error)
|
|
|
|
|
return content.replaceAll('\n', '<br>')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-04 13:06:30 +04:00
|
|
|
export default function ExportPdfButton({versionData, isOutdated = false, size = 'medium', variant = 'outlined'}) {
|
2025-07-23 20:31:55 +04:00
|
|
|
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;
|
|
|
|
|
`
|
|
|
|
|
|
2025-07-23 20:35:41 +04:00
|
|
|
// Add CSS styles for markdown elements
|
|
|
|
|
const styleElement = document.createElement('style')
|
|
|
|
|
styleElement.textContent = `
|
|
|
|
|
.pdf-content h1, .pdf-content h2, .pdf-content h3, .pdf-content h4, .pdf-content h5, .pdf-content h6 {
|
|
|
|
|
margin: 20px 0 10px 0;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
.pdf-content h1 { font-size: 24px; color: #1976d2; }
|
|
|
|
|
.pdf-content h2 { font-size: 20px; }
|
|
|
|
|
.pdf-content h3 { font-size: 18px; }
|
|
|
|
|
.pdf-content h4 { font-size: 16px; }
|
|
|
|
|
.pdf-content p { margin: 10px 0; }
|
|
|
|
|
.pdf-content strong, .pdf-content b { font-weight: bold; }
|
|
|
|
|
.pdf-content em, .pdf-content i { font-style: italic; }
|
2026-01-04 13:06:30 +04:00
|
|
|
.pdf-content ul, .pdf-content ol {
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
padding-left: 25px;
|
2025-07-23 20:35:41 +04:00
|
|
|
}
|
|
|
|
|
.pdf-content li { margin: 5px 0; }
|
2026-01-04 13:06:30 +04:00
|
|
|
.pdf-content blockquote {
|
|
|
|
|
margin: 15px 0;
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
border-left: 4px solid #1976d2;
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
font-style: italic;
|
2025-07-23 20:35:41 +04:00
|
|
|
}
|
2026-01-04 13:06:30 +04:00
|
|
|
.pdf-content code {
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
font-family: 'Courier New', monospace;
|
|
|
|
|
font-size: 13px;
|
2025-07-23 20:35:41 +04:00
|
|
|
}
|
2026-01-04 13:06:30 +04:00
|
|
|
.pdf-content pre {
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
padding: 15px;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
margin: 15px 0;
|
2025-07-23 20:35:41 +04:00
|
|
|
}
|
2026-01-04 13:06:30 +04:00
|
|
|
.pdf-content pre code {
|
|
|
|
|
background: none;
|
|
|
|
|
padding: 0;
|
2025-07-23 20:35:41 +04:00
|
|
|
}
|
|
|
|
|
.pdf-content a { color: #1976d2; text-decoration: underline; }
|
2026-01-04 13:06:30 +04:00
|
|
|
.pdf-content hr {
|
|
|
|
|
border: none;
|
|
|
|
|
border-top: 1px solid #ccc;
|
|
|
|
|
margin: 20px 0;
|
2025-07-23 20:35:41 +04:00
|
|
|
}
|
2026-01-04 13:06:30 +04:00
|
|
|
.pdf-content table {
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
margin: 15px 0;
|
|
|
|
|
width: 100%;
|
2025-07-23 20:35:41 +04:00
|
|
|
}
|
2026-01-04 13:06:30 +04:00
|
|
|
.pdf-content th, .pdf-content td {
|
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
text-align: left;
|
2025-07-23 20:35:41 +04:00
|
|
|
}
|
2026-01-04 13:06:30 +04:00
|
|
|
.pdf-content th {
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
font-weight: bold;
|
2025-07-23 20:35:41 +04:00
|
|
|
}
|
|
|
|
|
`
|
|
|
|
|
document.head.append(styleElement)
|
|
|
|
|
|
2025-07-23 20:31:55 +04:00
|
|
|
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))
|
2026-01-04 13:06:30 +04:00
|
|
|
const isExpired = createdAt < threeDaysAgo
|
|
|
|
|
const voteStatus = (isExpired || isOutdated) ? 'fermé' : 'ouvert'
|
2025-07-23 20:31:55 +04:00
|
|
|
const voteColor = voteStatus === 'ouvert' ? '#2e7d32' : '#d32f2f'
|
|
|
|
|
|
2025-07-23 20:35:41 +04:00
|
|
|
// Render markdown content to HTML
|
|
|
|
|
const renderedContent = await renderMarkdownToHtml(versionData.delta?.contenu)
|
|
|
|
|
|
2025-07-23 20:31:55 +04:00
|
|
|
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>
|
2025-07-23 20:35:41 +04:00
|
|
|
<div class="pdf-content" style="font-size: 14px; line-height: 1.8; text-align: justify;">
|
|
|
|
|
${renderedContent}
|
2025-07-23 20:31:55 +04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; font-size: 12px; color: #888; text-align: center;">
|
2026-01-10 23:41:57 +04:00
|
|
|
Exporté depuis Konstitisyon.nu le ${formatDate(new Date(), 'PPpp')}
|
2025-07-23 20:31:55 +04:00
|
|
|
</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()
|
2025-07-23 20:35:41 +04:00
|
|
|
styleElement.remove() // Clean up styles
|
2025-07-23 20:31:55 +04:00
|
|
|
|
|
|
|
|
// 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,
|
2026-01-04 13:06:30 +04:00
|
|
|
isOutdated: PropTypes.bool,
|
2025-07-23 20:31:55 +04:00
|
|
|
size: PropTypes.oneOf(['small', 'medium', 'large']),
|
|
|
|
|
variant: PropTypes.oneOf(['text', 'outlined', 'contained'])
|
|
|
|
|
}
|