diff --git a/app/dashboard/versions/[id]/page.js b/app/dashboard/versions/[id]/page.js
new file mode 100644
index 0000000..a306804
--- /dev/null
+++ b/app/dashboard/versions/[id]/page.js
@@ -0,0 +1,32 @@
+import {redirect} from 'next/navigation'
+import PropTypes from 'prop-types'
+import {auth} from '../../../auth.js'
+import VersionPage from '@/components/versions/version-page.js'
+
+export default async function VersionDetailPage({params, searchParams}) {
+ const session = await auth()
+
+ if (!session) {
+ redirect('/login')
+ }
+
+ const {id: versionId} = params
+ const viewMode = searchParams?.view || 'comparison'
+
+ return (
+
+ )
+}
+
+VersionDetailPage.propTypes = {
+ params: PropTypes.shape({
+ id: PropTypes.string.isRequired
+ }).isRequired,
+ searchParams: PropTypes.shape({
+ view: PropTypes.string
+ })
+}
diff --git a/components/versions/share-button.js b/components/versions/share-button.js
new file mode 100644
index 0000000..8190af4
--- /dev/null
+++ b/components/versions/share-button.js
@@ -0,0 +1,137 @@
+import {useState} from 'react'
+import PropTypes from 'prop-types'
+import IconButton from '@mui/material/IconButton'
+import Tooltip from '@mui/material/Tooltip'
+import Snackbar from '@mui/material/Snackbar'
+import Alert from '@mui/material/Alert'
+import ShareIcon from '@mui/icons-material/Share'
+import LinkIcon from '@mui/icons-material/Link'
+
+export default function ShareButton({
+ versionId,
+ versionName = 'Version',
+ size = 'small',
+ hasSnackbarVisible = true,
+ onShareSuccess = null,
+ onShareError = null
+}) {
+ const [shared, setShared] = useState(false)
+ const [snackbar, setSnackbar] = useState({open: false, message: '', severity: 'success'})
+
+ const handleShare = async () => {
+ const url = `${window.location.origin}/dashboard/versions/${versionId}`
+
+ try {
+ if (navigator.share) {
+ // Use native share API if available
+ await navigator.share({
+ title: `Version: ${versionName}`,
+ text: 'Découvrez cette version sur Konstitisyon.la',
+ url
+ })
+ } else 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')
+ }
+ }
+
+ // Success feedback
+ setShared(true)
+ setTimeout(() => setShared(false), 2000)
+
+ if (hasSnackbarVisible) {
+ setSnackbar({
+ open: true,
+ message: navigator.share ? 'Version partagée' : 'Lien copié dans le presse-papier',
+ severity: 'success'
+ })
+ }
+
+ if (onShareSuccess) {
+ onShareSuccess()
+ }
+ } catch (error) {
+ console.error('Failed to share:', error)
+
+ if (hasSnackbarVisible) {
+ setSnackbar({
+ open: true,
+ message: 'Impossible de partager cette version',
+ severity: 'error'
+ })
+ }
+
+ if (onShareError) {
+ onShareError(error)
+ }
+ }
+ }
+
+ const handleCloseSnackbar = () => {
+ setSnackbar(prev => ({...prev, open: false}))
+ }
+
+ return (
+ <>
+
+
+ {shared ? : }
+
+
+
+ {hasSnackbarVisible && (
+
+
+ {snackbar.message}
+
+
+ )}
+ >
+ )
+}
+
+ShareButton.propTypes = {
+ versionId: PropTypes.string.isRequired,
+ versionName: PropTypes.string,
+ size: PropTypes.oneOf(['small', 'medium', 'large']),
+ hasSnackbarVisible: PropTypes.bool,
+ onShareSuccess: PropTypes.func,
+ onShareError: PropTypes.func
+}
diff --git a/components/versions/version-page.js b/components/versions/version-page.js
new file mode 100644
index 0000000..24bbca3
--- /dev/null
+++ b/components/versions/version-page.js
@@ -0,0 +1,243 @@
+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'
+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'
+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)
+
+ useEffect(() => {
+ async function fetchVersionData() {
+ try {
+ setLoading(true)
+
+ // Fetch version details
+ const version = await getVersion({
+ accessToken,
+ userId,
+ versionId
+ })
+
+ setVersionData(version)
+
+ // If in comparison mode, also fetch comparison data
+ if (viewMode === 'comparison') {
+ const comparison = await compareVersion({
+ accessToken,
+ userId,
+ versionId,
+ countdownRef,
+ setError,
+ setIsErrorAlertOpen
+ })
+
+ if (comparison) {
+ setVersionCompare({...comparison, versionId})
+ }
+ }
+ } catch (error) {
+ console.error('Failed to fetch version:', error)
+ setError('Impossible de charger cette version')
+ setIsErrorAlertOpen(true)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchVersionData()
+ }, [accessToken, userId, versionId, viewMode])
+
+ const handleBack = () => {
+ router.push('/dashboard')
+ }
+
+ 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
+ })
+ } else {
+ // Fallback: copy URL to clipboard
+ await navigator.clipboard.writeText(url)
+ // Could show a toast notification here
+ }
+ } catch (error) {
+ console.error('Failed to share:', error)
+ }
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+ )
+ }
+
+ if (error && !versionData) {
+ return (
+
+
+
+
+ Version introuvable
+
+ } onClick={handleBack}>
+ Retour au tableau de bord
+
+
+
+
+ )
+ }
+
+ 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 (
+
+
+ {error && (
+
+ )}
+
+ {/* Header */}
+
+ }
+ variant='outlined'
+ onClick={handleBack}
+ >
+ Retour
+
+
+
+
+
+
+
+
+
+
+
+ {/* Version Info */}
+
+
+
+
+ {versionData.name}
+
+
+ Créée par @{authorName} le {formatDate(versionData.date_created, 'PPpp')}
+
+
+
+
+
+
+ {!isVoteDisabled && (
+
+ )}
+
+
+
+
+ {/* Content */}
+ {viewMode === 'comparison' && versionCompare ? (
+
+ ) : (
+
+
+ Contenu
+
+
+ {versionData.delta?.contenu || 'Aucun contenu disponible'}
+
+
+ )}
+
+
+
+
+
+ )
+}
+
+VersionPage.propTypes = {
+ session: PropTypes.object.isRequired,
+ versionId: PropTypes.string.isRequired,
+ viewMode: PropTypes.oneOf(['comparison', 'content']).isRequired
+}