Files
konstitisyon.nu/components/versions/version-timeline.js
T
cedric af7189dd6e fix: corrigé couleur texte markdown timeline
- Ajout useTheme pour accès aux couleurs Material-UI
- Utilisation theme.palette.text.secondary pour le markdown
- Compatible thèmes sombre et clair
- Texte markdown maintenant lisible sur tous fonds
2025-07-24 11:34:08 +04:00

424 lines
12 KiB
JavaScript

import {useRef, useState} from 'react'
import {useTheme} from '@mui/material/styles'
import PropTypes from 'prop-types'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import CardActions from '@mui/material/CardActions'
import Button from '@mui/material/Button'
import Chip from '@mui/material/Chip'
import Avatar from '@mui/material/Avatar'
import Divider from '@mui/material/Divider'
import Timeline from '@mui/lab/Timeline'
import TimelineItem from '@mui/lab/TimelineItem'
import TimelineSeparator from '@mui/lab/TimelineSeparator'
import TimelineConnector from '@mui/lab/TimelineConnector'
import TimelineContent from '@mui/lab/TimelineContent'
import TimelineDot from '@mui/lab/TimelineDot'
import TimelineOppositeContent from '@mui/lab/TimelineOppositeContent'
import AccessTimeIcon from '@mui/icons-material/AccessTime'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import ErrorIcon from '@mui/icons-material/Error'
import EditIcon from '@mui/icons-material/Edit'
import Snackbar from '@mui/material/Snackbar'
import Alert from '@mui/material/Alert'
import SessionExpired from '../session/session-expired.js'
import MarkdownRenderer from '../markdown-renderer/index.js'
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 PrintButton from './print-button.js'
import {formatDate} from '@/lib/format.js'
import {compareVersion} from '@/lib/directus.js'
function getVersionStatus(version, index, totalVersions) {
// Logic to determine version status based on position and data
if (index === 0) {
return 'current' // Most recent
}
if (index === totalVersions - 1) {
return 'initial' // First version
}
return 'archived' // Intermediate versions
}
function getStatusConfig(status) {
switch (status) {
case 'current': {
return {
color: '#1976D2',
bgColor: '#E3F2FD',
icon: <EditIcon />,
label: 'En cours',
chipColor: 'primary'
}
}
case 'published': {
return {
color: '#2E7D32',
bgColor: '#E8F5E9',
icon: <CheckCircleIcon />,
label: 'Publié',
chipColor: 'success'
}
}
case 'archived': {
return {
color: '#757575',
bgColor: '#F5F5F5',
icon: <AccessTimeIcon />,
label: 'Archivé',
chipColor: 'default'
}
}
case 'outdated': {
return {
color: '#D32F2F',
bgColor: '#F9E8E8',
icon: <ErrorIcon />,
label: 'Obsolète',
chipColor: 'error'
}
}
default: {
return {
color: '#757575',
bgColor: '#F5F5F5',
icon: <AccessTimeIcon />,
label: 'Archivé',
chipColor: 'default'
}
}
}
}
function VersionCard({
version,
index,
totalVersions,
accessToken,
userId,
countdownRef,
setError,
setIsErrorAlertOpen,
setIsOpenComparison,
setVersionCompare,
onVoteResult
}) {
const theme = useTheme()
const status = getVersionStatus(version, index, totalVersions)
const statusConfig = getStatusConfig(status)
const userDisplayName = version.user_created?.split('-')[0] || 'Système'
// Check if voting is disabled (after 3 days)
const createdAt = new Date(version.date_created)
const threeDaysAgo = new Date(Date.now() - (3 * 24 * 60 * 60 * 1000))
const isVoteDisabled = createdAt < threeDaysAgo
const handleCompareClick = async () => {
const comparisonData = await compareVersion({
accessToken,
userId,
versionId: version.id,
countdownRef,
setError,
setIsErrorAlertOpen
})
if (comparisonData) {
setVersionCompare({...comparisonData, versionId: version.id})
setIsOpenComparison(true)
}
}
// Create content preview preserving markdown structure
const createContentPreview = content => {
if (!content) {
return 'Contenu non disponible'
}
// If content is short enough, return as is
if (content.length <= 150) {
return content
}
// Find a good breaking point (end of sentence, paragraph, or word)
const preview = content.slice(0, 150)
const lastSentence = Math.max(preview.lastIndexOf('.'), preview.lastIndexOf('!'), preview.lastIndexOf('?'))
const lastParagraph = preview.lastIndexOf('\n\n')
const lastWord = preview.lastIndexOf(' ')
// Choose the best breaking point
let breakPoint = 150
if (lastSentence > 100) {
breakPoint = lastSentence + 1
} else if (lastParagraph > 80) {
breakPoint = lastParagraph
} else if (lastWord > 100) {
breakPoint = lastWord
}
return content.slice(0, breakPoint) + (content.length > breakPoint ? '...' : '')
}
const contentPreview = createContentPreview(version?.delta?.contenu)
return (
<Card
sx={{
borderLeft: `4px solid ${statusConfig.color}`,
backgroundColor: statusConfig.bgColor,
mb: 2,
transition: 'all 0.2s ease-in-out',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: 3
}
}}
>
<CardContent>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
mb: 2
}}
>
<Box sx={{display: 'flex', alignItems: 'center', gap: 1}}>
<Avatar sx={{bgcolor: statusConfig.color, width: 32, height: 32}}>
{statusConfig.icon}
</Avatar>
<Box>
<Typography variant='h6' sx={{fontWeight: 'bold', color: statusConfig.color}}>
{version.name}
</Typography>
<Typography variant='caption' color='text.secondary'>
par @{userDisplayName}
</Typography>
</Box>
</Box>
<Box sx={{display: 'flex', gap: 1}}>
<Chip
label={statusConfig.label}
color={statusConfig.chipColor}
size='small'
variant='outlined'
/>
{isVoteDisabled && (
<Chip
label='Vote fermé'
color='error'
size='small'
variant='outlined'
/>
)}
</Box>
</Box>
<Box sx={{mb: 2, '& > div': {fontSize: '0.875rem', fontStyle: 'italic'}}}>
<MarkdownRenderer
content={contentPreview}
color={theme.palette.text.secondary}
fallbackComponent={({children, ...props}) => (
<Typography
variant='body2'
color='text.secondary'
sx={{fontStyle: 'italic'}}
{...props}
>
{children}
</Typography>
)}
/>
</Box>
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<Typography variant='caption' color='text.secondary'>
{formatDate(version.date_created, 'PPpp')}
</Typography>
<Box sx={{display: 'flex', alignItems: 'center', gap: 1}}>
<CopyButton
content={version.delta?.contenu || version.name || ''}
label='Copier le contenu de cette version'
hasSnackbarVisible={false}
/>
<ShareButton
versionId={version.id}
versionName={version.name}
hasSnackbarVisible={false}
/>
<ExportPdfButton
versionData={version}
size='small'
variant='text'
/>
<PrintButton
versionData={version}
size='small'
variant='text'
/>
<VoteButtons
hasCountsVisible
versionId={version.id}
isDisabled={isVoteDisabled}
onVoteResult={onVoteResult}
/>
</Box>
</Box>
</CardContent>
<Divider />
<CardActions sx={{justifyContent: 'flex-end'}}>
<Button
size='small'
variant='outlined'
color='primary'
onClick={handleCompareClick}
>
Comparer
</Button>
</CardActions>
</Card>
)
}
export default function VersionTimeline({
collection,
data,
accessToken,
userId,
setError,
setIsErrorAlertOpen
}) {
const countdownRef = useRef()
const [isOpenComparison, setIsOpenComparison] = useState(false)
const [versionCompare, setVersionCompare] = useState(null)
const [snackbar, setSnackbar] = useState({open: false, message: '', severity: 'success'})
const versionData = data.find(({id}) => id === versionCompare?.versionId)
const handleVoteResult = result => {
setSnackbar({
open: true,
message: result.message,
severity: result.success ? 'success' : 'error'
})
}
const handleCloseSnackbar = () => {
setSnackbar(prev => ({...prev, open: false}))
}
return (
<>
<Box>
<Typography variant='h5' textAlign='center' sx={{mb: 3, fontWeight: 'bold'}}>
Historique des versions - {collection}
</Typography>
<Timeline position='right'>
{data.map((version, index) => (
<TimelineItem key={version.id}>
<TimelineOppositeContent sx={{flex: 0.3, pr: 2}}>
<Typography variant='caption' color='text.secondary'>
{formatDate(version.date_created, 'dd/MM/yyyy')}
</Typography>
<br />
<Typography variant='caption' color='text.secondary'>
{formatDate(version.date_created, 'HH:mm')}
</Typography>
</TimelineOppositeContent>
<TimelineSeparator>
<TimelineDot
color={index === 0 ? 'primary' : 'grey'}
variant={index === 0 ? 'filled' : 'outlined'}
>
{index === 0 ? <EditIcon /> : <AccessTimeIcon />}
</TimelineDot>
{index < data.length - 1 && <TimelineConnector />}
</TimelineSeparator>
<TimelineContent sx={{flex: 1}}>
<VersionCard
version={version}
index={index}
totalVersions={data.length}
accessToken={accessToken}
userId={userId}
countdownRef={countdownRef}
setError={setError}
setIsErrorAlertOpen={setIsErrorAlertOpen}
setIsOpenComparison={setIsOpenComparison}
setVersionCompare={setVersionCompare}
onVoteResult={handleVoteResult}
/>
</TimelineContent>
</TimelineItem>
))}
</Timeline>
</Box>
{isOpenComparison && (
<VersionDialog
versionData={versionData}
versionCompare={versionCompare}
isOpen={isOpenComparison}
setIsOpen={setIsOpenComparison}
/>
)}
<SessionExpired
ref={countdownRef}
setError={setError}
setIsErrorAlertOpen={setIsErrorAlertOpen}
/>
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
anchorOrigin={{vertical: 'bottom', horizontal: 'center'}}
onClose={handleCloseSnackbar}
>
<Alert variant='filled' severity={snackbar.severity} sx={{width: '100%'}} onClose={handleCloseSnackbar}>
{snackbar.message}
</Alert>
</Snackbar>
</>
)
}
VersionTimeline.propTypes = {
collection: PropTypes.oneOf(['titres', 'articles']).isRequired,
data: PropTypes.array.isRequired,
accessToken: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
setError: PropTypes.func.isRequired,
setIsErrorAlertOpen: PropTypes.func.isRequired
}
VersionCard.propTypes = {
version: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
totalVersions: PropTypes.number.isRequired,
accessToken: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
countdownRef: PropTypes.object.isRequired,
setError: PropTypes.func.isRequired,
setIsErrorAlertOpen: PropTypes.func.isRequired,
setIsOpenComparison: PropTypes.func.isRequired,
setVersionCompare: PropTypes.func.isRequired,
onVoteResult: PropTypes.func.isRequired
}