Public Access
feat: add What's new feature
Dashboard dialog shows unseen news entries. Users mark them seen with "Got it" or revisit via the /whats-new page. Admins publish, edit, and delete entries on /admin/news. Full i18n support and test coverage included.
This commit is contained in:
1 parent
79b086c525
commit
b811255c16
17 files changed
+690
-1
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {toast} from 'sonner';
|
||||
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog';
|
||||
import NewsEntry from './NewsEntry.jsx';
|
||||
import {fetchNews, markNewsSeen} from '../utils/news.js';
|
||||
|
||||
/**
|
||||
* The news entries the user has not seen, newest first. Only "Got it" marks them seen: closing the dialog any other
|
||||
* way shows them again on the next dashboard visit.
|
||||
*/
|
||||
export default function WhatsNewDialog() {
|
||||
const {t} = useTranslation();
|
||||
const [unseen, setUnseen] = useState([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// A failed load shows nothing, not an error.
|
||||
fetchNews()
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((data) => {
|
||||
const entries = (data?.results ?? []).filter((entry) => !entry.seen);
|
||||
setUnseen(entries);
|
||||
setOpen(entries.length > 0);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, []);
|
||||
|
||||
const gotIt = async () => {
|
||||
setOpen(false);
|
||||
try {
|
||||
const response = await markNewsSeen();
|
||||
if (!response.ok) toast.error(t('NEWS.SAVE_FAILED'));
|
||||
} catch {
|
||||
toast.error(t('INFO.ERROR.NETWORK_FAILURE'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('NEWS.TITLE')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
{unseen.map((entry) => <NewsEntry key={entry.id} entry={entry}/>)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={gotIt}>{t('NEWS.GOT_IT')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user