${page === 'devices' ? html` <${DeviceList} diff --git a/pkg/service/soundtouchweb/static/js/components/Announcements.js b/pkg/service/soundtouchweb/static/js/components/Announcements.js new file mode 100644 index 0000000..230907e --- /dev/null +++ b/pkg/service/soundtouchweb/static/js/components/Announcements.js @@ -0,0 +1,44 @@ +import { h } from 'preact'; +import { useState, useEffect } from 'preact/hooks'; +import htm from 'htm'; + +const html = htm.bind(h); + +// Announcements renders dismissible banners for the player (target=app), +// mirroring the admin UI's banner (pkg/service/handlers/web/js/script.js, +// fetchAnnouncements/dismissAnnouncement) against the same backend endpoints +// added for #419 — see _/i419/design-admin-area-auth-gate.md. Dismissal is +// recorded server-side (not a client-only localStorage flag), so it stays +// dismissed across sessions/devices. +export function Announcements() { + const [announcements, setAnnouncements] = useState([]); + + useEffect(() => { + fetch('/api/announcements?target=app') + .then(res => res.ok ? res.json() : { announcements: [] }) + .then(data => setAnnouncements(data.announcements || [])) + .catch(err => console.error('Failed to fetch announcements:', err)); + }, []); + + async function dismiss(id) { + try { + await fetch(`/api/announcements/${encodeURIComponent(id)}/dismiss`, { method: 'POST' }); + } catch (err) { + console.error('Failed to dismiss announcement:', err); + } + setAnnouncements(prev => prev.filter(a => a.id !== id)); + } + + if (announcements.length === 0) return null; + + return html` +
+ ${announcements.map(a => html` +
+ ${a.message} + +
+ `)} +
+ `; +}