feat: add What's new feature
Build, Test & Push Frontend / quality-check (push) Successful in 25s
Build, Test & Push Frontend / docker (push) Successful in 39s

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:
lobo committed 2026-10-01 10:03:39 +02:00
1 parent 79b086c525
commit b811255c16
17 files changed
+690 -1

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
import {useEffect, useState} from 'react';
import {useTranslation} from 'react-i18next';
import {Loader2} from 'lucide-react';
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
import NewsEntry from '../components/NewsEntry.jsx';
import PageContainer from '../components/PageContainer.jsx';
import {fetchNews, markNewsSeen} from '../utils/news.js';
/**
* Every news entry, newest first. Opening the page marks them seen.
*/
export default function WhatsNewPage() {
const {t} = useTranslation();
const [entries, setEntries] = useState([]);
const [status, setStatus] = useState('loading');
useEffect(() => {
(async () => {
try {
const response = await fetchNews();
if (response.status === 401) return;
if (!response.ok) {
setStatus('error');
return;
}
setEntries((await response.json()).results);
setStatus('ready');
// A failed mark is silent: the dashboard dialog shows the entries again.
markNewsSeen().catch(() => undefined);
} catch (err) {
console.error('News error:', err);
setStatus('error');
}
})();
}, []);
return (
<PageContainer className="space-y-4 px-4 py-6 md:px-8">
<Card>
<CardHeader>
<CardTitle>{t('NEWS.TITLE')}</CardTitle>
</CardHeader>
<CardContent>
{status === 'loading' && <Loader2 className="size-5 animate-spin text-muted-foreground"/>}
{status === 'error' && <p className="text-sm text-destructive">{t('NEWS.LOAD_ERROR')}</p>}
{status === 'ready' && entries.length === 0 && <p className="text-sm text-muted-foreground">{t('NEWS.EMPTY')}</p>}
{status === 'ready' && entries.length > 0 && (
<ul className="divide-y">
{entries.map((entry) => <li key={entry.id} className="py-3"><NewsEntry entry={entry}/></li>)}
</ul>
)}
</CardContent>
</Card>
</PageContainer>
);
}