feat: ajouté impression optimisée pour versions
- Composant PrintButton avec fenêtre popup dédiée - Styles CSS print-optimized pour A4 avec media queries - Support markdown complet avec rendu approprié - Interface print avec boutons Imprimer/Fermer - Feedback snackbar cohérent avec autres composants - Intégré dans VersionPage, ListVersions et VersionTimeline
This commit is contained in:
@@ -22,6 +22,7 @@ import VersionFilters from './version-filters.js'
|
|||||||
import CopyButton from './copy-button.js'
|
import CopyButton from './copy-button.js'
|
||||||
import ShareButton from './share-button.js'
|
import ShareButton from './share-button.js'
|
||||||
import ExportPdfButton from './export-pdf-button.js'
|
import ExportPdfButton from './export-pdf-button.js'
|
||||||
|
import PrintButton from './print-button.js'
|
||||||
import {formatDate} from '@/lib/format.js'
|
import {formatDate} from '@/lib/format.js'
|
||||||
import {compareVersion} from '@/lib/directus.js'
|
import {compareVersion} from '@/lib/directus.js'
|
||||||
import {filterVersions, getFilterStats} from '@/lib/version-utils.js'
|
import {filterVersions, getFilterStats} from '@/lib/version-utils.js'
|
||||||
@@ -128,6 +129,11 @@ function rowContent({
|
|||||||
size='small'
|
size='small'
|
||||||
variant='text'
|
variant='text'
|
||||||
/>
|
/>
|
||||||
|
<PrintButton
|
||||||
|
versionData={row}
|
||||||
|
size='small'
|
||||||
|
variant='text'
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
'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
|
||||||
|
const {marked} = await import('marked')
|
||||||
|
|
||||||
|
// 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 marked(content)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to parse markdown, falling back to plain text:', error)
|
||||||
|
return content.replaceAll('\n', '<br>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PrintButton({versionData, 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 voteStatus = createdAt < threeDaysAgo ? 'fermé' : 'ouvert'
|
||||||
|
const voteColor = voteStatus === 'ouvert' ? '#2e7d32' : '#d32f2f'
|
||||||
|
|
||||||
|
// 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.la</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">
|
||||||
|
<strong>Date de création :</strong> ${formatDate(versionData.date_created, 'PPpp')}
|
||||||
|
</div>
|
||||||
|
<div class="metadata">
|
||||||
|
<strong>Statut du vote :</strong>
|
||||||
|
<span class="vote-status">${voteStatus}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-section">
|
||||||
|
<h2 class="content-title">Contenu</h2>
|
||||||
|
<div class="content">
|
||||||
|
${renderedContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Imprimé depuis Konstitisyon.la le ${formatDate(new Date(), 'PPpp')}
|
||||||
|
</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,
|
||||||
|
size: PropTypes.oneOf(['small', 'medium', 'large']),
|
||||||
|
variant: PropTypes.oneOf(['text', 'outlined', 'contained'])
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import Footer from '../footer.js'
|
|||||||
import VoteButtons from './vote-buttons.js'
|
import VoteButtons from './vote-buttons.js'
|
||||||
import CopyButton from './copy-button.js'
|
import CopyButton from './copy-button.js'
|
||||||
import ExportPdfButton from './export-pdf-button.js'
|
import ExportPdfButton from './export-pdf-button.js'
|
||||||
|
import PrintButton from './print-button.js'
|
||||||
import VersionComparison from './version-comparison.js'
|
import VersionComparison from './version-comparison.js'
|
||||||
import {getVersion, compareVersion} from '@/lib/directus.js'
|
import {getVersion, compareVersion} from '@/lib/directus.js'
|
||||||
import {formatDate} from '@/lib/format.js'
|
import {formatDate} from '@/lib/format.js'
|
||||||
@@ -224,6 +225,7 @@ export default function VersionPage({session, versionId, viewMode}) {
|
|||||||
|
|
||||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||||
<ExportPdfButton versionData={versionData} size='medium' />
|
<ExportPdfButton versionData={versionData} size='medium' />
|
||||||
|
<PrintButton versionData={versionData} size='medium' />
|
||||||
<Tooltip title='Partager cette version'>
|
<Tooltip title='Partager cette version'>
|
||||||
<IconButton color='primary' onClick={handleShare}>
|
<IconButton color='primary' onClick={handleShare}>
|
||||||
<ShareIcon />
|
<ShareIcon />
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import VoteButtons from './vote-buttons.js'
|
|||||||
import CopyButton from './copy-button.js'
|
import CopyButton from './copy-button.js'
|
||||||
import ShareButton from './share-button.js'
|
import ShareButton from './share-button.js'
|
||||||
import ExportPdfButton from './export-pdf-button.js'
|
import ExportPdfButton from './export-pdf-button.js'
|
||||||
|
import PrintButton from './print-button.js'
|
||||||
import {formatDate} from '@/lib/format.js'
|
import {formatDate} from '@/lib/format.js'
|
||||||
import {compareVersion} from '@/lib/directus.js'
|
import {compareVersion} from '@/lib/directus.js'
|
||||||
|
|
||||||
@@ -219,6 +220,11 @@ function VersionCard({
|
|||||||
size='small'
|
size='small'
|
||||||
variant='text'
|
variant='text'
|
||||||
/>
|
/>
|
||||||
|
<PrintButton
|
||||||
|
versionData={version}
|
||||||
|
size='small'
|
||||||
|
variant='text'
|
||||||
|
/>
|
||||||
<VoteButtons
|
<VoteButtons
|
||||||
hasCountsVisible
|
hasCountsVisible
|
||||||
versionId={version.id}
|
versionId={version.id}
|
||||||
|
|||||||
Reference in New Issue
Block a user