[Frontend] new admin view to manage banners (BanMan)

This commit is contained in:
joonas-a
2026-07-13 16:49:38 +03:00
parent 826dd15fd3
commit 4bdbf4ad4d
2 changed files with 263 additions and 0 deletions
@@ -0,0 +1,143 @@
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Chip from '@mui/material/Chip'
import FormControl from '@mui/material/FormControl'
import Grid2 from '@mui/material/Grid2'
import InputLabel from '@mui/material/InputLabel'
import MenuItem from '@mui/material/MenuItem'
import Select from '@mui/material/Select'
import Stack from '@mui/material/Stack'
import TextField from '@mui/material/TextField'
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'
import dayjs from 'dayjs'
import { useState } from 'react'
import { Banner } from '@/components/Banner/Banner'
import { Section } from '@/components/Section'
import { Banner as TBanner } from '@oodikone/shared/models/kone'
const generateFallbackValues = () => {
const now = new Date()
const inAWeek = new Date()
inAWeek.setDate(now.getDate() + 7)
return {
startDate: now,
endDate: inAWeek,
text: 'A very important piece of knowledge to share with the world',
color: 'info',
lightness: 'main',
}
}
export const BannerEditForm = ({ initialValues, onSave }) => {
const isNew = initialValues === null
const [values, setValues] = useState<TBanner>({
...generateFallbackValues(),
...initialValues,
})
const handleValueChange = (key: string, value: Date | string | null) => {
if (!value) return
setValues(prev => ({
...prev,
[key]: value,
}))
}
return (
<>
<Box sx={{ position: 'fixed', top: 0, left: 0, width: '100%', display: 'flex', flexDirection: 'column' }}>
<Banner banner={values} />
<Chip
color="secondary"
label="Preview mode"
sx={{ my: 2, outline: '1px solid white', borderRadius: 0, mx: 'auto' }}
/>
</Box>
<Section title="DKN-BE (Oodikone Banner Editor)">
<Stack spacing={2}>
<TextField
fullWidth
label="Text content"
minRows={3}
multiline
name="text"
onChange={({ target: { value, name } }) => handleValueChange(name, value)}
type="color"
value={values.text}
/>
<Grid2 container>
<Grid2 size={6} sx={{ pr: 1 }}>
<FormControl fullWidth>
<InputLabel id="color-label">Color</InputLabel>
<Select
label="Color"
labelId="Color"
name="color"
onChange={({ target: { value, name } }) => handleValueChange(name, value)}
type="color"
value={values.color}
>
<MenuItem value={'white'}>White</MenuItem>
<MenuItem value={'dark'}>Black</MenuItem>
<MenuItem value={'info'}>Blue</MenuItem>
<MenuItem value={'success'}>Green</MenuItem>
<MenuItem value={'warning'}>Orange</MenuItem>
<MenuItem value={'error'}>Red</MenuItem>
</Select>
</FormControl>
</Grid2>
<Grid2 size={6} sx={{ pl: 1 }}>
<FormControl fullWidth>
<InputLabel id="lightness-label">Color brightness</InputLabel>
<Select
fullWidth
label="Color brightness"
labelId="lightness-label"
name="lightness"
onChange={({ target: { value, name } }) => handleValueChange(name, value)}
value={values.lightness}
>
<MenuItem value={'light'}>Light</MenuItem>
<MenuItem value={'main'}>Regular</MenuItem>
<MenuItem value={'dark'}>Dark</MenuItem>
</Select>
</FormControl>
</Grid2>
</Grid2>
<Grid2 container>
<Grid2 size={6} sx={{ pr: 1 }}>
<DateTimePicker
format="L HH:mm"
label="Start date"
name="startDate"
onChange={date => {
if (date) handleValueChange('startDate', date.toDate())
}}
sx={{ width: '100%' }}
value={dayjs(values.startDate)}
/>
</Grid2>
<Grid2 size={6} sx={{ pl: 1 }}>
<DateTimePicker
format="L HH:mm"
label="End date"
name="endDate"
onChange={date => {
if (date) handleValueChange('endDate', date.toDate())
}}
sx={{ width: '100%' }}
value={dayjs(values.endDate)}
/>
</Grid2>
</Grid2>
<Button onClick={() => onSave(values, false, isNew)} type="submit" variant="contained">
{isNew ? 'Create' : 'Save'}
</Button>
</Stack>
</Section>
</>
)
}
@@ -0,0 +1,120 @@
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Dialog from '@mui/material/Dialog'
import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography'
import dayjs from 'dayjs'
import { useState } from 'react'
import { Banner } from '@/components/Banner/Banner'
import { PageLayout } from '@/components/common/PageLayout'
import { PageTitle } from '@/components/common/PageTitle'
import { Section } from '@/components/Section'
import { useTitle } from '@/hooks/title'
import { useCreateBannerMutation, useGetAllBannersQuery, useUpdateBannerMutation } from '@/redux/banners'
import { Banner as TBanner } from '@oodikone/shared/models/kone'
import { BannerEditForm } from './EditForm'
const bannerInfoText = `
Banners are visible to all Oodikone users.
To locally make a closed banner visible again, nuke "cleared-banners" key from local storage.
`
export const Banners = () => {
useTitle('Manage banners')
const { data: allBanners } = useGetAllBannersQuery()
const [createBanner] = useCreateBannerMutation()
const [modifyBanner] = useUpdateBannerMutation()
const [formOpen, setFormOpen] = useState(false)
const [selectedBanner, setSelectedBanner] = useState<Partial<TBanner> | null>(null)
const now = dayjs()
const activeBanners = allBanners?.filter(b => now.isAfter(dayjs(b.startDate)) && now.isBefore(dayjs(b.endDate)))
const scheduledBanners = allBanners?.filter(b => now.isBefore(dayjs(b.startDate)) && b.startDate < b.endDate)
const openEditor = (b: Partial<TBanner> | null) => {
setSelectedBanner(b)
setFormOpen(true)
}
const onClose = () => {
setFormOpen(false)
setSelectedBanner(null)
}
// Handler for POST/PUT/"DELETE" (which is also PUT)
const onSave = (values: TBanner, setToEnd = false, isNew = false) => {
if (setToEnd) {
void modifyBanner({
...values,
endDate: new Date(),
})
} else if (isNew) {
void createBanner(values)
} else {
void modifyBanner(values)
}
onClose()
}
return (
<>
<Dialog component="div" fullWidth onClose={onClose} open={formOpen} sx={{ bgcolor: '#00000080' }}>
<BannerEditForm initialValues={selectedBanner} onSave={onSave} />
</Dialog>
<PageLayout maxWidth="lg">
<PageTitle title="Manage banners" />
<Stack spacing={3}>
<Box>
<Button onClick={() => openEditor(null)} variant="contained">
Create new
</Button>
</Box>
<Section infoBoxContent={bannerInfoText} title="Currently active banners">
{activeBanners?.length ? (
activeBanners.map(banner => (
<Box key={banner.id} sx={{ p: 1, mt: 2, border: '1px solid #e0e0e0' }}>
<Banner
adminView
banner={banner}
clearBanner={() => openEditor(banner)}
deleteBanner={() => onSave(banner, true)}
/>
<Typography fontWeight="normal" sx={{ mt: 1 }}>
{`Visible: ${new Date(banner.startDate).toLocaleString()} - ${new Date(banner.endDate).toLocaleString()}.
Latest modification by: ${banner.lastModifiedBy}.`}
</Typography>
</Box>
))
) : (
<Typography>No currently active banners</Typography>
)}
</Section>
<Section title="Scheduled (not yet visible) banners">
{scheduledBanners?.length ? (
scheduledBanners.map(banner => (
<Box key={banner.id} sx={{ p: 1, mt: 2, border: '1px solid #e0e0e0' }}>
<Banner
adminView
banner={banner}
clearBanner={() => openEditor(banner)}
deleteBanner={() => onSave(banner, true)}
/>
<Typography fontWeight="normal" sx={{ mt: 1 }}>
{`Visible: ${new Date(banner.startDate).toLocaleString()} - ${new Date(banner.endDate).toLocaleString()}.
Latest modification by: ${banner.lastModifiedBy}.`}
</Typography>
</Box>
))
) : (
<Typography>No scheduled banners</Typography>
)}
</Section>
</Stack>
</PageLayout>
</>
)
}