From 0e313db2e3149d4b907590c49955c30841ae4eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=B6rdle=20Bot?= Date: Tue, 25 Nov 2025 11:52:52 +0100 Subject: [PATCH] Implement News System with Admin UI and Homepage Integration --- README.md | 8 + app/admin/page.tsx | 277 ++++ app/api/news/route.ts | 146 ++ app/page.tsx | 4 + components/NewsSection.tsx | 191 +++ package-lock.json | 1180 ++++++++++++++++- package.json | 3 +- .../migration.sql | 15 + prisma/schema.prisma | 15 + 9 files changed, 1834 insertions(+), 5 deletions(-) create mode 100644 app/api/news/route.ts create mode 100644 components/NewsSection.tsx create mode 100644 prisma/migrations/20251125101602_add_news_model/migration.sql diff --git a/README.md b/README.md index 771ddca..fdacbc6 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,15 @@ Eine Web-App inspiriert von Heardle, bei der Nutzer täglich einen Song anhand k - Live-Vorschau beim Hovern über die Waveform. - Playback-Cursor zeigt aktuelle Abspielposition. - Einzelne Segmente zum Testen abspielen. + - Einzelne Segmente zum Testen abspielen. - Manuelle Speicherung mit visueller Bestätigung. +- **News & Announcements:** + - Integriertes News-System für Ankündigungen (z.B. neue Specials, Features). + - **Markdown Support:** Formatierung von Texten, Links und Listen. + - **Homepage Integration:** Dezentrale Anzeige auf der Startseite (collapsible). + - **Featured News:** Hervorhebung wichtiger Ankündigungen. + - **Special-Verknüpfung:** Direkte Links zu Specials in News-Beiträgen. + - Verwaltung über das Admin-Dashboard. ## Spielregeln & Punktesystem diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 56e8712..49bc6d1 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -51,6 +51,20 @@ interface Song { excludeFromGlobal: boolean; } +interface News { + id: number; + title: string; + content: string; + author: string | null; + publishedAt: string; + featured: boolean; + specialId: number | null; + special: { + id: number; + name: string; + } | null; +} + type SortField = 'id' | 'title' | 'artist' | 'createdAt' | 'releaseYear' | 'activations' | 'averageRating'; type SortDirection = 'asc' | 'desc'; @@ -92,6 +106,20 @@ export default function AdminPage() { const [editSpecialEndDate, setEditSpecialEndDate] = useState(''); const [editSpecialCurator, setEditSpecialCurator] = useState(''); + // News state + const [news, setNews] = useState([]); + const [newNewsTitle, setNewNewsTitle] = useState(''); + const [newNewsContent, setNewNewsContent] = useState(''); + const [newNewsAuthor, setNewNewsAuthor] = useState(''); + const [newNewsFeatured, setNewNewsFeatured] = useState(false); + const [newNewsSpecialId, setNewNewsSpecialId] = useState(null); + const [editingNewsId, setEditingNewsId] = useState(null); + const [editNewsTitle, setEditNewsTitle] = useState(''); + const [editNewsContent, setEditNewsContent] = useState(''); + const [editNewsAuthor, setEditNewsAuthor] = useState(''); + const [editNewsFeatured, setEditNewsFeatured] = useState(false); + const [editNewsSpecialId, setEditNewsSpecialId] = useState(null); + // Edit state const [editingId, setEditingId] = useState(null); const [editTitle, setEditTitle] = useState(''); @@ -142,6 +170,8 @@ export default function AdminPage() { fetchSongs(); fetchGenres(); fetchDailyPuzzles(); + fetchSpecials(); + fetchNews(); } }, []); @@ -156,6 +186,8 @@ export default function AdminPage() { fetchSongs(); fetchGenres(); fetchDailyPuzzles(); + fetchSpecials(); + fetchNews(); } else { alert('Wrong password'); } @@ -394,6 +426,94 @@ export default function AdminPage() { } }; + // News functions + const fetchNews = async () => { + const res = await fetch('/api/news', { + headers: getAuthHeaders() + }); + if (res.ok) { + const data = await res.json(); + setNews(data); + } + }; + + const handleCreateNews = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newNewsTitle.trim() || !newNewsContent.trim()) return; + + const res = await fetch('/api/news', { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ + title: newNewsTitle, + content: newNewsContent, + author: newNewsAuthor || null, + featured: newNewsFeatured, + specialId: newNewsSpecialId + }), + }); + + if (res.ok) { + setNewNewsTitle(''); + setNewNewsContent(''); + setNewNewsAuthor(''); + setNewNewsFeatured(false); + setNewNewsSpecialId(null); + fetchNews(); + } else { + alert('Failed to create news'); + } + }; + + const startEditNews = (newsItem: News) => { + setEditingNewsId(newsItem.id); + setEditNewsTitle(newsItem.title); + setEditNewsContent(newsItem.content); + setEditNewsAuthor(newsItem.author || ''); + setEditNewsFeatured(newsItem.featured); + setEditNewsSpecialId(newsItem.specialId); + }; + + const saveEditedNews = async () => { + if (editingNewsId === null) return; + + const res = await fetch('/api/news', { + method: 'PUT', + headers: getAuthHeaders(), + body: JSON.stringify({ + id: editingNewsId, + title: editNewsTitle, + content: editNewsContent, + author: editNewsAuthor || null, + featured: editNewsFeatured, + specialId: editNewsSpecialId + }), + }); + + if (res.ok) { + setEditingNewsId(null); + fetchNews(); + } else { + alert('Failed to update news'); + } + }; + + const handleDeleteNews = async (id: number) => { + if (!confirm('Delete this news item?')) return; + + const res = await fetch('/api/news', { + method: 'DELETE', + headers: getAuthHeaders(), + body: JSON.stringify({ id }), + }); + + if (res.ok) { + fetchNews(); + } else { + alert('Failed to delete news'); + } + }; + // Load specials after auth useEffect(() => { if (isAuthenticated) fetchSpecials(); @@ -1182,6 +1302,163 @@ export default function AdminPage() { )} + {/* News Management */} +
+

Manage News & Announcements

+
+
+ setNewNewsTitle(e.target.value)} + placeholder="News Title" + className="form-input" + required + /> +