Files
konstitisyon.nu/components/versions/print-button.js
T

462 lines
14 KiB
JavaScript
Raw Normal View History

'use client'
import {useState} from 'react'
import PropTypes from 'prop-types'
import Button from '@mui/material/Button'
import CircularProgress from '@mui/material/CircularProgress'
import Snackbar from '@mui/material/Snackbar'
import Alert from '@mui/material/Alert'
import PrintIcon from '@mui/icons-material/Print'
import {formatDate} from '@/lib/format.js'
// Helper function to render markdown to HTML (reused from PDF export)
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 and sanitizer
const [{marked}, {default: DOMPurify}] = await Promise.all([
import('marked'),
import('dompurify')
])
// Configure marked for better print rendering
marked.setOptions({
breaks: true, // Convert \n to <br>
gfm: true, // GitHub flavored markdown
headerIds: false, // No header IDs needed for print
mangle: false // Don't mangle email addresses
})
return DOMPurify.sanitize(marked(content), {
ALLOWED_TAGS: [
'p',
'strong',
'em',
'b',
'i',
'ul',
'ol',
'li',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'blockquote',
'code',
'pre',
'br',
'hr',
'a',
'table',
'thead',
'tbody',
'tr',
'th',
'td',
],
ALLOWED_ATTR: [
'href',
'target',
'rel',
],
ALLOW_DATA_ATTR: false,
})
} catch (error) {
console.warn('Failed to parse markdown, falling back to plain text:', error)
return content.replaceAll('\n', '<br>')
}
}
2026-01-24 23:35:48 +04:00
export default function PrintButton({versionData, isOutdated = false, voteCounts = null, size = 'medium', variant = 'outlined'}) {
const [isPrinting, setIsPrinting] = useState(false)
const [snackbar, setSnackbar] = useState({open: false, message: '', severity: 'success'})
const handlePrint = async () => {
setIsPrinting(true)
try {
// Calculate vote status
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 isExpired = createdAt < threeDaysAgo
const voteStatus = (isExpired || isOutdated) ? 'fermé' : 'ouvert'
const voteColor = voteStatus === 'ouvert' ? '#2e7d32' : '#d32f2f'
2026-01-24 23:35:48 +04:00
// Vote counts display
const voteTotal = voteCounts ? voteCounts.total : 0
const voteTotalColor = voteTotal > 0 ? '#2e7d32' : (voteTotal < 0 ? '#d32f2f' : '#666')
const voteTotalSign = voteTotal >= 0 ? '+' : ''
// Render markdown content to HTML
const renderedContent = await renderMarkdownToHtml(versionData.delta?.contenu)
// Create print window
const printWindow = window.open('', '_blank', 'width=800,height=600')
if (!printWindow) {
throw new Error('Impossible d\'ouvrir la fenêtre d\'impression. Vérifiez que les popups ne sont pas bloqués.')
}
// Build print-optimized HTML
const printHtml = `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${versionData.name} - Konstitisyon.nu</title>
<style>
/* Print-optimized styles */
@media print {
* {
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
}
@page {
margin: 2cm;
size: A4;
}
}
body {
font-family: 'Times New Roman', serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: white;
}
.header {
border-bottom: 3px solid #1976d2;
padding-bottom: 20px;
margin-bottom: 30px;
page-break-inside: avoid;
}
.title {
color: #1976d2;
font-size: 28px;
font-weight: bold;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.metadata {
font-size: 14px;
color: #666;
margin: 5px 0;
}
.vote-status {
font-weight: bold;
color: ${voteColor};
}
.content-section {
margin: 30px 0;
}
.content-title {
color: #333;
font-size: 22px;
font-weight: bold;
margin-bottom: 20px;
page-break-after: avoid;
}
.content {
font-size: 16px;
line-height: 1.8;
text-align: justify;
hyphens: auto;
}
/* Markdown elements styles */
.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 {
margin: 25px 0 15px 0;
font-weight: bold;
color: #333;
page-break-after: avoid;
}
.content h1 { font-size: 24px; color: #1976d2; }
.content h2 { font-size: 20px; }
.content h3 { font-size: 18px; }
.content h4 { font-size: 16px; }
.content p { margin: 15px 0; }
.content strong, .content b { font-weight: bold; }
.content em, .content i { font-style: italic; }
.content ul, .content ol {
margin: 15px 0;
padding-left: 30px;
}
.content li {
margin: 8px 0;
page-break-inside: avoid;
}
.content blockquote {
margin: 20px 0;
padding: 15px 25px;
border-left: 4px solid #1976d2;
background-color: #f8f9fa;
font-style: italic;
page-break-inside: avoid;
}
.content code {
background-color: #f8f9fa;
padding: 3px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.content pre {
background-color: #f8f9fa;
padding: 20px;
border-radius: 5px;
overflow-x: auto;
margin: 20px 0;
page-break-inside: avoid;
}
.content pre code {
background: none;
padding: 0;
}
.content a {
color: #1976d2;
text-decoration: underline;
}
.content hr {
border: none;
border-top: 1px solid #ccc;
margin: 30px 0;
}
.content table {
border-collapse: collapse;
margin: 20px 0;
width: 100%;
page-break-inside: avoid;
}
.content th, .content td {
border: 1px solid #ccc;
padding: 12px;
text-align: left;
}
.content th {
background-color: #f8f9fa;
font-weight: bold;
}
.footer {
margin-top: 50px;
padding-top: 20px;
border-top: 1px solid #eee;
font-size: 12px;
color: #888;
text-align: center;
page-break-inside: avoid;
}
/* Screen-only styles */
@media screen {
.print-actions {
position: fixed;
top: 20px;
right: 20px;
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
}
.print-button {
background: #1976d2;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
}
.close-button {
background: #666;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.print-button:hover { background: #1565c0; }
.close-button:hover { background: #555; }
}
@media print {
.print-actions { display: none !important; }
}
</style>
</head>
<body>
<div class="print-actions">
<button class="print-button" onclick="window.print()">🖨️ Imprimer</button>
<button class="close-button" onclick="window.close()">✕ Fermer</button>
</div>
<div class="header">
<h1 class="title">${versionData.name}</h1>
<div class="metadata">
<strong>Auteur :</strong> @${authorName}
</div>
<div class="metadata">
2026-01-24 00:40:38 +04:00
<strong>Date de création :</strong> ${formatDate(versionData.date_created, 'PPpp', {withTimezone: true})}
</div>
<div class="metadata">
2026-01-24 23:35:48 +04:00
<strong>Statut du vote :</strong>
<span class="vote-status">${voteStatus}</span>
</div>
2026-01-24 23:35:48 +04:00
${voteCounts ? `
<div style="margin-top: 15px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; border: 1px solid #e0e0e0;">
<p style="margin: 0 0 10px 0; font-size: 16px; font-weight: bold; color: #333;">
📊 Résultats des votes
</p>
<p style="margin: 5px 0; font-size: 14px;">
👍 Votes positifs : <strong style="color: #2e7d32;">${voteCounts.positive}</strong>
</p>
<p style="margin: 5px 0; font-size: 14px;">
👎 Votes négatifs : <strong style="color: #d32f2f;">${voteCounts.negative}</strong>
</p>
<p style="margin: 10px 0 0 0; font-size: 16px; font-weight: bold;">
🏆 Total : <span style="color: ${voteTotalColor};">${voteTotalSign}${voteTotal}</span>
</p>
</div>
` : ''}
</div>
<div class="content-section">
<h2 class="content-title">Contenu</h2>
<div class="content">
${renderedContent}
</div>
</div>
<div class="footer">
2026-01-24 00:40:38 +04:00
Imprimé depuis Konstitisyon.nu le ${formatDate(new Date(), 'PPpp', {withTimezone: true})}
</div>
</body>
</html>
`
printWindow.document.write(printHtml)
printWindow.document.close()
// Auto-focus the print window
printWindow.focus()
setSnackbar({
open: true,
message: 'Fenêtre d\'impression ouverte',
severity: 'success'
})
} catch (error) {
console.error('Erreur lors de l\'impression:', error)
setSnackbar({
open: true,
message: error.message || 'Une erreur est survenue lors de l\'impression',
severity: 'error'
})
} finally {
setIsPrinting(false)
}
}
const handleCloseSnackbar = () => {
setSnackbar(prev => ({...prev, open: false}))
}
return (
<>
<Button
variant={variant}
size={size}
startIcon={isPrinting ? <CircularProgress size={16} /> : <PrintIcon />}
disabled={isPrinting}
sx={{
minWidth: size === 'small' ? 'auto' : undefined,
'&:disabled': {
opacity: 0.6
}
}}
onClick={handlePrint}
>
{isPrinting ? 'Préparation...' : (size === 'small' ? 'Print' : 'Imprimer')}
</Button>
<Snackbar
open={snackbar.open}
autoHideDuration={4000}
anchorOrigin={{vertical: 'bottom', horizontal: 'center'}}
onClose={handleCloseSnackbar}
>
<Alert
variant='filled'
severity={snackbar.severity}
sx={{width: '100%'}}
onClose={handleCloseSnackbar}
>
{snackbar.message}
</Alert>
</Snackbar>
</>
)
}
PrintButton.propTypes = {
versionData: PropTypes.object.isRequired,
isOutdated: PropTypes.bool,
2026-01-24 23:35:48 +04:00
voteCounts: PropTypes.shape({
positive: PropTypes.number,
negative: PropTypes.number,
total: PropTypes.number
}),
size: PropTypes.oneOf(['small', 'medium', 'large']),
variant: PropTypes.oneOf(['text', 'outlined', 'contained'])
}