Merge pull request 'Ajout de la page dashboard' (#1) from feat-dashboard into master
Reviewed-on: https://codeberg.org/OKI/konstitisyon.la/pulls/1
This commit is contained in:
+35
-1
@@ -1,5 +1,19 @@
|
|||||||
import {redirect} from 'next/navigation'
|
import {redirect} from 'next/navigation'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Container from '@mui/material/Container'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import HomeIcon from '@mui/icons-material/Home'
|
||||||
import {auth} from '../../auth.js'
|
import {auth} from '../../auth.js'
|
||||||
|
import GetVersions from '@/components/versions/get-versions.js'
|
||||||
|
import Footer from '@/components/footer.js'
|
||||||
|
import Sign from '@/components/session/sign.js'
|
||||||
|
|
||||||
|
const navButton = {
|
||||||
|
title: 'Accueil',
|
||||||
|
path: '/',
|
||||||
|
color: 'success',
|
||||||
|
icon: <HomeIcon fontSize='large' />
|
||||||
|
}
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
const session = await auth()
|
const session = await auth()
|
||||||
@@ -9,6 +23,26 @@ export default async function DashboardPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>DashboardPage</div>
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minHeight: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
<Typography
|
||||||
|
textTransform='uppercase'
|
||||||
|
mt={1}
|
||||||
|
component='h1'
|
||||||
|
textAlign='center'
|
||||||
|
variant='h4'
|
||||||
|
>
|
||||||
|
Tableau de bord
|
||||||
|
</Typography>
|
||||||
|
<Sign session={session} navButton={navButton} />
|
||||||
|
<GetVersions session={session} />
|
||||||
|
</Container>
|
||||||
|
<Footer />
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -2,6 +2,7 @@ import {createDirectus, rest, readItems} from '@directus/sdk'
|
|||||||
import Container from '@mui/material/Container'
|
import Container from '@mui/material/Container'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
|
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'
|
||||||
import {auth} from '../auth.js'
|
import {auth} from '../auth.js'
|
||||||
import Konstitisyon from '@/components/konstitisyon/index.js'
|
import Konstitisyon from '@/components/konstitisyon/index.js'
|
||||||
import Footer from '@/components/footer.js'
|
import Footer from '@/components/footer.js'
|
||||||
@@ -11,6 +12,13 @@ import Create from '@/components/konstitisyon/create/index.js'
|
|||||||
const apiUrl = process.env.DIRECTUS_API_URL
|
const apiUrl = process.env.DIRECTUS_API_URL
|
||||||
const appTitle = process.env.APP_TITLE
|
const appTitle = process.env.APP_TITLE
|
||||||
|
|
||||||
|
const navButton = {
|
||||||
|
title: 'Tableau de bord',
|
||||||
|
path: '/dashboard',
|
||||||
|
color: 'success',
|
||||||
|
icon: <AdminPanelSettingsIcon fontSize='large' />
|
||||||
|
}
|
||||||
|
|
||||||
async function getData() {
|
async function getData() {
|
||||||
if (!apiUrl) {
|
if (!apiUrl) {
|
||||||
throw new Error('DIRECTUS_API_URL is required')
|
throw new Error('DIRECTUS_API_URL is required')
|
||||||
@@ -67,7 +75,7 @@ export default async function Page() {
|
|||||||
>
|
>
|
||||||
<Container maxWidth='sm'>
|
<Container maxWidth='sm'>
|
||||||
<Typography mt={1} component='h1' textAlign='center' variant='h4'>{appTitle.toUpperCase()}</Typography>
|
<Typography mt={1} component='h1' textAlign='center' variant='h4'>{appTitle.toUpperCase()}</Typography>
|
||||||
<Sign session={session} />
|
<Sign session={session} navButton={navButton} />
|
||||||
{session && (
|
{session && (
|
||||||
<Create session={session} titres={titres} />
|
<Create session={session} titres={titres} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
|
|
||||||
|
export function Loading() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display='flex'
|
||||||
|
justifyContent='center'
|
||||||
|
alignItems='center'
|
||||||
|
height='100vh'
|
||||||
|
>
|
||||||
|
<CircularProgress color='warning' />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ import Tooltip, {tooltipClasses} from '@mui/material/Tooltip'
|
|||||||
import LogoutIcon from '@mui/icons-material/Logout'
|
import LogoutIcon from '@mui/icons-material/Logout'
|
||||||
import LoginIcon from '@mui/icons-material/Login'
|
import LoginIcon from '@mui/icons-material/Login'
|
||||||
import PersonAddIcon from '@mui/icons-material/PersonAdd'
|
import PersonAddIcon from '@mui/icons-material/PersonAdd'
|
||||||
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'
|
|
||||||
import ConfirmationAlert from './confirmation-alert.js'
|
import ConfirmationAlert from './confirmation-alert.js'
|
||||||
|
|
||||||
const LightTooltip = styled(({className, ...props}) => (
|
const LightTooltip = styled(({className, ...props}) => (
|
||||||
@@ -26,7 +25,7 @@ const LightTooltip = styled(({className, ...props}) => (
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export default function Sign({session}) {
|
export default function Sign({session, navButton}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
@@ -45,9 +44,9 @@ export default function Sign({session}) {
|
|||||||
<LogoutIcon fontSize='large' />
|
<LogoutIcon fontSize='large' />
|
||||||
</Fab>
|
</Fab>
|
||||||
</LightTooltip>
|
</LightTooltip>
|
||||||
<LightTooltip title='Tableau de bord' placement='right'>
|
<LightTooltip title={navButton.title} placement='right'>
|
||||||
<Fab sx={{mr: 3}} size='large' color='warning' onClick={() => router.push('/dashboard')}>
|
<Fab sx={{mr: 3}} size='large' color={navButton.color} onClick={() => router.push(navButton.path)}>
|
||||||
<AdminPanelSettingsIcon fontSize='large' />
|
{navButton.icon}
|
||||||
</Fab>
|
</Fab>
|
||||||
</LightTooltip>
|
</LightTooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -78,5 +77,6 @@ export default function Sign({session}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Sign.propTypes = {
|
Sign.propTypes = {
|
||||||
session: PropTypes.object
|
session: PropTypes.object,
|
||||||
|
navButton: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {useState, useRef, useEffect} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import AuthAlert from '../auth-form/auth-alert.js'
|
||||||
|
import LogoutCountdown from '../session/logout-countdown.js'
|
||||||
|
import {Loading} from '../loading.js'
|
||||||
|
import ListVersions from './list-versions.js'
|
||||||
|
import {listVersions} from '@/lib/directus.js'
|
||||||
|
|
||||||
|
export default function GetVersions({session}) {
|
||||||
|
const {accessToken, userId} = session.user
|
||||||
|
|
||||||
|
const [versions, setVersions] = useState(null)
|
||||||
|
const [isErrorAlertOpen, setIsErrorAlertOpen] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const countdownRef = useRef()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchVersions() {
|
||||||
|
const data = await listVersions({
|
||||||
|
accessToken, userId, countdownRef, setError, setIsErrorAlertOpen
|
||||||
|
})
|
||||||
|
setVersions(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchVersions()
|
||||||
|
}, [accessToken, userId])
|
||||||
|
|
||||||
|
if (!versions) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{error && <AuthAlert
|
||||||
|
isOpen={isErrorAlertOpen}
|
||||||
|
setIsOpen={setIsErrorAlertOpen}
|
||||||
|
message={error}
|
||||||
|
severity='error'
|
||||||
|
/>}
|
||||||
|
<Loading />
|
||||||
|
<LogoutCountdown ref={countdownRef} setError={setError} setIsErrorAlertOpen={setIsErrorAlertOpen} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const titres = versions?.filter(({collection}) => collection === 'titres')
|
||||||
|
const articles = versions?.filter(({collection}) => collection === 'articles')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{error && <AuthAlert
|
||||||
|
isOpen={isErrorAlertOpen}
|
||||||
|
setIsOpen={setIsErrorAlertOpen}
|
||||||
|
message={error}
|
||||||
|
severity='error'
|
||||||
|
/>}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
border: '2px solid #ccc',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: 1,
|
||||||
|
my: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
gutterBottom
|
||||||
|
variant='h5'
|
||||||
|
component='h2'
|
||||||
|
textAlign='center'
|
||||||
|
sx={{display: 'block', fontWeight: 'bold', textTransform: 'uppercase'}}
|
||||||
|
>
|
||||||
|
Liste des versions
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={2}
|
||||||
|
justifyContent={
|
||||||
|
(titres.length > 0 && articles.length === 0) || (articles.length > 0 && titres.length === 0)
|
||||||
|
? 'center'
|
||||||
|
: 'flex-start'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{titres.length > 0 && (
|
||||||
|
<Grid size={{xs: 12, md: articles.length > 0 ? 6 : 12}}>
|
||||||
|
<ListVersions collection='Titres' data={titres} />
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{articles.length > 0 && (
|
||||||
|
<Grid size={{xs: 12, md: titres.length > 0 ? 6 : 12}}>
|
||||||
|
<ListVersions collection='Articles' data={articles} />
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
<LogoutCountdown ref={countdownRef} setError={setError} setIsErrorAlertOpen={setIsErrorAlertOpen} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GetVersions.propTypes = {
|
||||||
|
session: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Table from '@mui/material/Table'
|
||||||
|
import TableBody from '@mui/material/TableBody'
|
||||||
|
import TableCell from '@mui/material/TableCell'
|
||||||
|
import TableContainer from '@mui/material/TableContainer'
|
||||||
|
import TableHead from '@mui/material/TableHead'
|
||||||
|
import TableRow from '@mui/material/TableRow'
|
||||||
|
import Paper from '@mui/material/Paper'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import {TableVirtuoso} from 'react-virtuoso'
|
||||||
|
import {Box, Typography} from '@mui/material'
|
||||||
|
import {formatDate} from '@/lib/format.js'
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
width: 200,
|
||||||
|
label: 'Version',
|
||||||
|
dataKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 120,
|
||||||
|
label: 'Créée le',
|
||||||
|
dataKey: 'date_created',
|
||||||
|
numeric: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const VirtuosoTableComponents = {
|
||||||
|
Scroller: React.forwardRef((props, ref) => (
|
||||||
|
<TableContainer component={Paper} {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
Table: props => (
|
||||||
|
<Table {...props} sx={{borderCollapse: 'separate', tableLayout: 'fixed'}} />
|
||||||
|
),
|
||||||
|
TableHead: React.forwardRef((props, ref) => <TableHead {...props} ref={ref} />),
|
||||||
|
TableRow,
|
||||||
|
TableBody: React.forwardRef((props, ref) => <TableBody {...props} ref={ref} />),
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixedHeaderContent() {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
{columns.map(column => (
|
||||||
|
<TableCell
|
||||||
|
key={column.dataKey}
|
||||||
|
variant='head'
|
||||||
|
align={column.numeric || false ? 'right' : 'left'}
|
||||||
|
style={{width: column.width}}
|
||||||
|
sx={{backgroundColor: 'background.paper'}}
|
||||||
|
>
|
||||||
|
{column.label}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowContent(_index, row) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{columns.map(column => (
|
||||||
|
<TableCell
|
||||||
|
key={column.dataKey}
|
||||||
|
align={column.numeric || false ? 'right' : 'left'}
|
||||||
|
>
|
||||||
|
{column.dataKey === 'date_created' ? formatDate(row[column.dataKey], 'Pp') : <Button variant='outlined' color='success'>{row[column.dataKey]}</Button>}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ListVersions({collection, data}) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant='button' textAlign='center' sx={{display: 'block', fontWeight: 'bold'}}>
|
||||||
|
{collection}
|
||||||
|
</Typography>
|
||||||
|
<Paper style={{height: 350, width: '100%', marginBlock: 5}}>
|
||||||
|
<TableVirtuoso
|
||||||
|
data={data}
|
||||||
|
components={VirtuosoTableComponents}
|
||||||
|
fixedHeaderContent={fixedHeaderContent}
|
||||||
|
itemContent={rowContent}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ListVersions.propTypes = {
|
||||||
|
collection: PropTypes.oneOf(['titres', 'articles']).isRequired,
|
||||||
|
data: PropTypes.array.isRequired
|
||||||
|
}
|
||||||
@@ -69,6 +69,39 @@ export async function handleSubmit({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listVersions({
|
||||||
|
accessToken,
|
||||||
|
userId,
|
||||||
|
countdownRef,
|
||||||
|
setError,
|
||||||
|
setIsErrorAlertOpen
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
await handleUserStatus(accessToken, userId)
|
||||||
|
|
||||||
|
const versions = await directusClient.request(
|
||||||
|
withToken(
|
||||||
|
accessToken,
|
||||||
|
readContentVersions({
|
||||||
|
sort: '-date_created'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return versions
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error', error)
|
||||||
|
|
||||||
|
if (error?.errors[0]?.message === 'Token expired.') {
|
||||||
|
countdownRef.current.startCountdown()
|
||||||
|
} else {
|
||||||
|
console.log(error?.errors[0]?.message)
|
||||||
|
setError(error?.errors[0]?.message)
|
||||||
|
setIsErrorAlertOpen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function createVersion({
|
export async function createVersion({
|
||||||
accessToken,
|
accessToken,
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
+2
-1
@@ -18,7 +18,8 @@
|
|||||||
"next": "^14.2.3",
|
"next": "^14.2.3",
|
||||||
"next-auth": "^5.0.0-beta.18",
|
"next-auth": "^5.0.0-beta.18",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1",
|
||||||
|
"react-virtuoso": "^4.10.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-config-xo-nextjs": "^6.0.0",
|
"eslint-config-xo-nextjs": "^6.0.0",
|
||||||
|
|||||||
@@ -3115,6 +3115,11 @@ react-transition-group@^4.4.5:
|
|||||||
loose-envify "^1.4.0"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
|
react-virtuoso@^4.10.2:
|
||||||
|
version "4.10.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.10.2.tgz#a27308a3c4cfeb24722032acc0b6a46055c26967"
|
||||||
|
integrity sha512-os6n9QKeKRF+8mnQR/vGy/xrFf6vXIzuaAVL54q5k2st2d5QIEwI+KDKaflMUmMvnDbPxf68bs+CF5bY3YI7qA==
|
||||||
|
|
||||||
react@^18.3.1:
|
react@^18.3.1:
|
||||||
version "18.3.1"
|
version "18.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
|
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
|
||||||
|
|||||||
Reference in New Issue
Block a user