2025-07-23 19:59:51 +04:00
|
|
|
'use client'
|
|
|
|
|
|
2025-07-23 19:54:10 +04:00
|
|
|
import {useState, useEffect, useRef} from 'react'
|
|
|
|
|
import PropTypes from 'prop-types'
|
|
|
|
|
import Box from '@mui/material/Box'
|
|
|
|
|
import Container from '@mui/material/Container'
|
|
|
|
|
import Typography from '@mui/material/Typography'
|
|
|
|
|
import Button from '@mui/material/Button'
|
|
|
|
|
import Paper from '@mui/material/Paper'
|
|
|
|
|
import Chip from '@mui/material/Chip'
|
|
|
|
|
import IconButton from '@mui/material/IconButton'
|
|
|
|
|
import Tooltip from '@mui/material/Tooltip'
|
2025-07-23 20:10:01 +04:00
|
|
|
import Snackbar from '@mui/material/Snackbar'
|
|
|
|
|
import Alert from '@mui/material/Alert'
|
2025-07-23 19:54:10 +04:00
|
|
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
|
|
|
|
|
import ShareIcon from '@mui/icons-material/Share'
|
|
|
|
|
import {useRouter} from 'next/navigation'
|
|
|
|
|
import SessionExpired from '../session/session-expired.js'
|
|
|
|
|
import AuthAlert from '../auth-form/auth-alert.js'
|
|
|
|
|
import {Loading} from '../loading.js'
|
|
|
|
|
import Footer from '../footer.js'
|
|
|
|
|
import VoteButtons from './vote-buttons.js'
|
|
|
|
|
import CopyButton from './copy-button.js'
|
2025-07-23 20:31:55 +04:00
|
|
|
import ExportPdfButton from './export-pdf-button.js'
|
2025-07-24 08:08:15 +04:00
|
|
|
import PrintButton from './print-button.js'
|
2025-07-23 19:54:10 +04:00
|
|
|
import VersionComparison from './version-comparison.js'
|
|
|
|
|
import {getVersion, compareVersion} from '@/lib/directus.js'
|
|
|
|
|
import {formatDate} from '@/lib/format.js'
|
|
|
|
|
|
|
|
|
|
export default function VersionPage({session, versionId, viewMode}) {
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const {accessToken, userId} = session.user
|
|
|
|
|
const countdownRef = useRef()
|
|
|
|
|
|
|
|
|
|
const [versionData, setVersionData] = useState(null)
|
|
|
|
|
const [versionCompare, setVersionCompare] = useState(null)
|
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
const [error, setError] = useState('')
|
|
|
|
|
const [isErrorAlertOpen, setIsErrorAlertOpen] = useState(false)
|
2025-07-23 20:10:01 +04:00
|
|
|
const [snackbar, setSnackbar] = useState({open: false, message: '', severity: 'success'})
|
2025-07-23 20:13:07 +04:00
|
|
|
const [voteRefreshKey, setVoteRefreshKey] = useState(0)
|
2025-07-23 19:54:10 +04:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
async function fetchVersionData() {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
|
|
|
|
// Fetch version details
|
|
|
|
|
const version = await getVersion({
|
|
|
|
|
accessToken,
|
|
|
|
|
userId,
|
|
|
|
|
versionId
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
setVersionData(version)
|
|
|
|
|
|
2026-01-04 13:06:30 +04:00
|
|
|
// Fetch comparison data (needed for outdated status even if not in comparison mode)
|
|
|
|
|
const comparison = await compareVersion({
|
|
|
|
|
accessToken,
|
|
|
|
|
userId,
|
|
|
|
|
versionId,
|
|
|
|
|
countdownRef,
|
|
|
|
|
setError,
|
|
|
|
|
setIsErrorAlertOpen
|
|
|
|
|
})
|
2025-07-23 19:54:10 +04:00
|
|
|
|
2026-01-04 13:06:30 +04:00
|
|
|
if (comparison) {
|
|
|
|
|
setVersionCompare({...comparison, versionId})
|
2025-07-23 19:54:10 +04:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to fetch version:', error)
|
|
|
|
|
setError('Impossible de charger cette version')
|
|
|
|
|
setIsErrorAlertOpen(true)
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fetchVersionData()
|
2026-01-04 13:06:30 +04:00
|
|
|
}, [accessToken, userId, versionId])
|
2025-07-23 19:54:10 +04:00
|
|
|
|
|
|
|
|
const handleBack = () => {
|
|
|
|
|
router.push('/dashboard')
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-23 20:10:01 +04:00
|
|
|
const handleVoteResult = result => {
|
|
|
|
|
setSnackbar({
|
|
|
|
|
open: true,
|
|
|
|
|
message: result.message,
|
|
|
|
|
severity: result.success ? 'success' : 'error'
|
|
|
|
|
})
|
2025-07-23 20:13:07 +04:00
|
|
|
// Force refresh of both VoteButtons components by changing the key
|
|
|
|
|
setVoteRefreshKey(prev => prev + 1)
|
2025-07-23 20:10:01 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCloseSnackbar = () => {
|
|
|
|
|
setSnackbar(prev => ({...prev, open: false}))
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-23 19:54:10 +04:00
|
|
|
const handleShare = async () => {
|
|
|
|
|
const url = window.location.href
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (navigator.share) {
|
|
|
|
|
// Use native share API if available
|
|
|
|
|
await navigator.share({
|
|
|
|
|
title: `Version: ${versionData?.name || 'Version'}`,
|
|
|
|
|
text: 'Découvrez cette version sur Konstitisyon.la',
|
|
|
|
|
url
|
|
|
|
|
})
|
2025-07-23 20:14:53 +04:00
|
|
|
setSnackbar({
|
|
|
|
|
open: true,
|
|
|
|
|
message: 'Version partagée',
|
|
|
|
|
severity: 'success'
|
|
|
|
|
})
|
2025-07-23 19:54:10 +04:00
|
|
|
} else {
|
|
|
|
|
// Fallback: copy URL to clipboard
|
2025-07-23 20:14:53 +04:00
|
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
|
|
|
await navigator.clipboard.writeText(url)
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback for older browsers
|
|
|
|
|
const textArea = document.createElement('textarea')
|
|
|
|
|
textArea.value = url
|
|
|
|
|
textArea.style.position = 'fixed'
|
|
|
|
|
textArea.style.left = '-999999px'
|
|
|
|
|
textArea.style.top = '-999999px'
|
|
|
|
|
document.body.append(textArea)
|
|
|
|
|
textArea.focus()
|
|
|
|
|
textArea.select()
|
|
|
|
|
|
|
|
|
|
const result = document.execCommand('copy')
|
|
|
|
|
textArea.remove()
|
|
|
|
|
|
|
|
|
|
if (!result) {
|
|
|
|
|
throw new Error('Copy command failed')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSnackbar({
|
|
|
|
|
open: true,
|
|
|
|
|
message: 'Lien copié dans le presse-papier',
|
|
|
|
|
severity: 'success'
|
|
|
|
|
})
|
2025-07-23 19:54:10 +04:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to share:', error)
|
2025-07-23 20:14:53 +04:00
|
|
|
setSnackbar({
|
|
|
|
|
open: true,
|
|
|
|
|
message: 'Impossible de partager cette version',
|
|
|
|
|
severity: 'error'
|
|
|
|
|
})
|
2025-07-23 19:54:10 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<Container>
|
|
|
|
|
<Loading />
|
|
|
|
|
<SessionExpired ref={countdownRef} setError={setError} setIsErrorAlertOpen={setIsErrorAlertOpen} />
|
|
|
|
|
</Container>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error && !versionData) {
|
|
|
|
|
return (
|
|
|
|
|
<Container>
|
|
|
|
|
<AuthAlert
|
|
|
|
|
isOpen={isErrorAlertOpen}
|
|
|
|
|
setIsOpen={setIsErrorAlertOpen}
|
|
|
|
|
message={error}
|
|
|
|
|
severity='error'
|
|
|
|
|
/>
|
|
|
|
|
<Box sx={{textAlign: 'center', py: 4}}>
|
|
|
|
|
<Typography gutterBottom variant='h6'>
|
|
|
|
|
Version introuvable
|
|
|
|
|
</Typography>
|
|
|
|
|
<Button variant='contained' startIcon={<ArrowBackIcon />} onClick={handleBack}>
|
|
|
|
|
Retour au tableau de bord
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
<SessionExpired ref={countdownRef} setError={setError} setIsErrorAlertOpen={setIsErrorAlertOpen} />
|
|
|
|
|
</Container>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const createdAt = new Date(versionData.date_created)
|
|
|
|
|
const threeDaysAgo = new Date(Date.now() - (3 * 24 * 60 * 60 * 1000))
|
|
|
|
|
const isVoteDisabled = createdAt < threeDaysAgo
|
|
|
|
|
const authorName = versionData.user_created?.split('-')[0] || 'Système'
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box sx={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
minHeight: '100vh'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Container sx={{flex: 1}}>
|
|
|
|
|
{error && (
|
|
|
|
|
<AuthAlert
|
|
|
|
|
isOpen={isErrorAlertOpen}
|
|
|
|
|
setIsOpen={setIsErrorAlertOpen}
|
|
|
|
|
message={error}
|
|
|
|
|
severity='error'
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<Box sx={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
my: 3
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Button
|
|
|
|
|
startIcon={<ArrowBackIcon />}
|
|
|
|
|
variant='outlined'
|
|
|
|
|
onClick={handleBack}
|
|
|
|
|
>
|
|
|
|
|
Retour
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
2025-07-23 20:31:55 +04:00
|
|
|
<ExportPdfButton versionData={versionData} size='medium' />
|
2025-07-24 08:08:15 +04:00
|
|
|
<PrintButton versionData={versionData} size='medium' />
|
2025-07-23 19:54:10 +04:00
|
|
|
<Tooltip title='Partager cette version'>
|
|
|
|
|
<IconButton color='primary' onClick={handleShare}>
|
|
|
|
|
<ShareIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
{/* Version Info */}
|
|
|
|
|
<Paper sx={{p: 3, mb: 3}}>
|
|
|
|
|
<Box sx={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
|
mb: 2
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Box>
|
|
|
|
|
<Typography gutterBottom variant='h4'>
|
|
|
|
|
{versionData.name}
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography gutterBottom variant='body2' color='text.secondary'>
|
|
|
|
|
Créée par @{authorName} le {formatDate(versionData.date_created, 'PPpp')}
|
|
|
|
|
</Typography>
|
|
|
|
|
<Chip
|
|
|
|
|
label={isVoteDisabled ? 'Vote fermé' : 'Vote ouvert'}
|
|
|
|
|
color={isVoteDisabled ? 'default' : 'success'}
|
|
|
|
|
size='small'
|
|
|
|
|
variant='outlined'
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<Box sx={{display: 'flex', alignItems: 'center', gap: 1}}>
|
|
|
|
|
<CopyButton
|
|
|
|
|
content={versionData.delta?.contenu || versionData.name || ''}
|
|
|
|
|
label='Copier le contenu de cette version'
|
|
|
|
|
/>
|
|
|
|
|
{!isVoteDisabled && (
|
2025-07-23 20:13:07 +04:00
|
|
|
<VoteButtons
|
|
|
|
|
key={`vote-header-${voteRefreshKey}`}
|
|
|
|
|
versionId={versionId}
|
|
|
|
|
onVoteResult={handleVoteResult}
|
|
|
|
|
/>
|
2025-07-23 19:54:10 +04:00
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
</Paper>
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
{viewMode === 'comparison' && versionCompare ? (
|
|
|
|
|
<VersionComparison
|
|
|
|
|
versionData={versionData}
|
|
|
|
|
versionCompare={versionCompare}
|
2025-07-23 20:13:07 +04:00
|
|
|
voteRefreshKey={voteRefreshKey}
|
|
|
|
|
onVoteResult={handleVoteResult}
|
2025-07-23 19:54:10 +04:00
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<Paper sx={{p: 3}}>
|
|
|
|
|
<Typography gutterBottom variant='h6'>
|
|
|
|
|
Contenu
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant='body1' sx={{whiteSpace: 'pre-wrap'}}>
|
|
|
|
|
{versionData.delta?.contenu || 'Aucun contenu disponible'}
|
|
|
|
|
</Typography>
|
|
|
|
|
</Paper>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<SessionExpired ref={countdownRef} setError={setError} setIsErrorAlertOpen={setIsErrorAlertOpen} />
|
|
|
|
|
</Container>
|
|
|
|
|
<Footer />
|
2025-07-23 20:10:01 +04:00
|
|
|
|
|
|
|
|
<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>
|
2025-07-23 19:54:10 +04:00
|
|
|
</Box>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VersionPage.propTypes = {
|
|
|
|
|
session: PropTypes.object.isRequired,
|
|
|
|
|
versionId: PropTypes.string.isRequired,
|
|
|
|
|
viewMode: PropTypes.oneOf(['comparison', 'content']).isRequired
|
|
|
|
|
}
|