Files
konstitisyon.nu/components/versions/version-timeline.js
T
cedric 43f1f6e9f2 a11y: corrections accessibilité WCAG 2.1 (critères 4.1.2, 4.1.3, 1.3.1)
sign.js :
- aria-label sur les 4 Fab (Se déconnecter, dashboard, Se connecter, S'enregistrer)
- Correction des guillemets typographiques U+2018/U+2019 en ASCII (empêchaient le parsing JSX)
- Suppression de useMemo inutilisé
- IIFE async ;() → startSubscription() nommée + .catch() explicite (semi-style + no-void)

auth-form/index.js :
- aria-label des IconButton visibility traduits en français avec état dynamique :
  'Afficher/Masquer le mot de passe' et 'Afficher/Masquer la vérification'

version-timeline.js :
- aria-label='Comparer les versions' sur IconButton Comparer
- aria-label dynamique + aria-expanded sur le bouton expand/collapse
- Correction object-curly-newline et jsx-closing-bracket-location (pré-existants)

version-search.js :
- inputProps aria-label='Rechercher dans les versions' (placeholder seul insuffisant)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 14:36:37 +04:00

340 lines
9.0 KiB
JavaScript

import {useRef, useState, useEffect} from 'react'
import PropTypes from 'prop-types'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import IconButton from '@mui/material/IconButton'
import Collapse from '@mui/material/Collapse'
import Snackbar from '@mui/material/Snackbar'
import Alert from '@mui/material/Alert'
import CompareArrowsIcon from '@mui/icons-material/CompareArrows'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import ExpandLessIcon from '@mui/icons-material/ExpandLess'
import SessionExpired from '../session/session-expired.js'
import VersionDialog from './version-dialog.js'
import VoteButtons from './vote-buttons.js'
import CopyButton from './copy-button.js'
import {formatDate} from '@/lib/format.js'
import {compareVersion} from '@/lib/directus.js'
function getStatusColor(isOutdated, index) {
if (isOutdated) {
return '#D32F2F'
}
if (index === 0) {
return '#1976D2'
}
return '#9E9E9E'
}
function VersionItem({
version,
index,
accessToken,
userId,
countdownRef,
setError,
setIsErrorAlertOpen,
setIsOpenComparison,
setVersionCompare,
onVoteResult
}) {
const [isOutdated, setIsOutdated] = useState(false)
const [expanded, setExpanded] = useState(false)
useEffect(() => {
async function fetchStatus() {
try {
const comparisonData = await compareVersion({
accessToken,
userId,
versionId: version.id,
countdownRef,
setError,
setIsErrorAlertOpen
})
if (comparisonData) {
setIsOutdated(comparisonData.outdated)
}
} catch {
setIsOutdated(false)
}
}
fetchStatus()
}, [version.id, accessToken, userId, countdownRef, setError, setIsErrorAlertOpen])
const statusColor = getStatusColor(isOutdated, index)
const createdAt = new Date(version.date_created)
const threeDaysAgo = new Date(Date.now() - (3 * 24 * 60 * 60 * 1000))
const isVoteDisabled = createdAt < threeDaysAgo || isOutdated
const handleCompare = async () => {
const comparisonData = await compareVersion({
accessToken,
userId,
versionId: version.id,
countdownRef,
setError,
setIsErrorAlertOpen
})
if (comparisonData) {
setVersionCompare({...comparisonData, versionId: version.id})
setIsOpenComparison(true)
}
}
return (
<Box
sx={{
display: 'flex',
gap: 1.5,
py: 1.5,
borderBottom: '1px solid',
borderColor: 'divider',
'&:last-child': {borderBottom: 'none'}
}}
>
{/* Status indicator */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
pt: 0.5
}}
>
<Box
sx={{
width: 12,
height: 12,
borderRadius: '50%',
bgcolor: statusColor,
flexShrink: 0
}}
/>
<Box
sx={{
width: 2,
flex: 1,
bgcolor: 'divider',
mt: 0.5,
display: index === 0 ? 'none' : 'block'
}}
/>
</Box>
{/* Content */}
<Box sx={{flex: 1, minWidth: 0}}>
{/* Header row */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 1
}}
>
<Box sx={{minWidth: 0, flex: 1}}>
<Typography
variant='body2'
sx={{
fontWeight: 600,
color: statusColor,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{version.name}
</Typography>
<Typography variant='caption' color='text.secondary'>
{formatDate(version.date_created, 'dd/MM/yy HH:mm')}
</Typography>
</Box>
{/* Actions */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
flexShrink: 0
}}
>
<VoteButtons
hasCountsVisible
versionId={version.id}
isDisabled={isVoteDisabled}
onVoteResult={onVoteResult}
/>
<IconButton size='small' aria-label='Comparer les versions' title='Comparer' onClick={handleCompare}>
<CompareArrowsIcon fontSize='small' />
</IconButton>
<IconButton
size='small'
aria-label={expanded ? 'Réduire les détails' : 'Afficher les détails'}
aria-expanded={expanded}
onClick={() => setExpanded(!expanded)}
>
{expanded ? <ExpandLessIcon fontSize='small' /> : <ExpandMoreIcon fontSize='small' />}
</IconButton>
</Box>
</Box>
{/* Expanded content */}
<Collapse in={expanded}>
<Box
sx={{
mt: 1.5,
display: 'flex',
flexDirection: 'column',
gap: 1
}}
>
{/* Preview */}
{version.delta?.contenu && (
<Typography
variant='caption'
color='text.secondary'
sx={{
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
fontStyle: 'italic'
}}
>
{version.delta.contenu}
</Typography>
)}
{/* Actions row */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'wrap'
}}
>
<CopyButton
content={version.delta?.contenu || version.name || ''}
label='Copier'
hasSnackbarVisible={false}
/>
</Box>
</Box>
</Collapse>
</Box>
</Box>
)
}
export default function VersionTimeline({
data,
accessToken,
userId,
setError,
setIsErrorAlertOpen,
onVoteSuccess
}) {
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, versionId) => {
setSnackbar({
open: true,
message: result.message,
severity: result.success ? 'success' : 'error'
})
if (result.success && onVoteSuccess && versionId) {
onVoteSuccess(versionId)
}
}
return (
<>
<Box sx={{maxWidth: 500, mx: 'auto'}}>
{data.map((version, index) => (
<VersionItem
key={version.id}
version={version}
index={index}
accessToken={accessToken}
userId={userId}
countdownRef={countdownRef}
setError={setError}
setIsErrorAlertOpen={setIsErrorAlertOpen}
setIsOpenComparison={setIsOpenComparison}
setVersionCompare={setVersionCompare}
onVoteResult={result => handleVoteResult(result, version.id)}
/>
))}
</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={() => setSnackbar(prev => ({...prev, open: false}))}
>
<Alert
variant='filled'
severity={snackbar.severity}
sx={{width: '100%'}}
onClose={() => setSnackbar(prev => ({...prev, open: false}))}
>
{snackbar.message}
</Alert>
</Snackbar>
</>
)
}
VersionTimeline.propTypes = {
data: PropTypes.array.isRequired,
accessToken: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
setError: PropTypes.func.isRequired,
setIsErrorAlertOpen: PropTypes.func.isRequired,
onVoteSuccess: PropTypes.func
}
VersionItem.propTypes = {
version: PropTypes.object.isRequired,
index: 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
}