From 529452b0fb8361a79f7311d0b02746a8de6be8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Wed, 23 Jul 2025 19:54:10 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20cr=C3=A9=C3=A9=20route=20dynamique=20et?= =?UTF-8?q?=20page=20version=20individuelle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Route app/dashboard/versions/[id]/ pour URLs spécifiques - Composant VersionPage avec affichage et partage natif - Support modes comparison/content via paramètre URL --- app/dashboard/versions/[id]/page.js | 32 ++++ components/versions/share-button.js | 137 ++++++++++++++++ components/versions/version-page.js | 243 ++++++++++++++++++++++++++++ 3 files changed, 412 insertions(+) create mode 100644 app/dashboard/versions/[id]/page.js create mode 100644 components/versions/share-button.js create mode 100644 components/versions/version-page.js 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 + + + + + + ) + } + + 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 */} + + + + + + + + + + + + + {/* 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'} + + + )} + + + +