Files
Epicnabbo-Catalogus-Updated…/src/app/admin/radio/embed/page.tsx
T
Simo 0cd4d06ff6 Fill the remaining gaps: badge upload, radio tools, VPN, writeable boxes
Built the admin tools previously listed as missing:
- Badge upload (/admin/badges): uploads a <code>.gif into the emulator's
  badge dir via BADGE_UPLOAD_DIR (node:fs); validated code/type/size,
  logged. Made configurable rather than skipped.
- Radio tools: /admin/radio/api-keys (CRUD, server-generated keys),
  /admin/radio/autodj (Auto-DJ playlist CRUD), /admin/radio/embed (embed
  snippet generator), /admin/radio/points (points settings),
  /admin/radio/monitoring (live stream/now-playing/listeners status).
  radio_api_keys + radio_auto_dj_playlist already had real columns.
- /admin/vpn: VPN/proxy detection config (block toggle + provider + key),
  complementing /admin/ip's raw blacklist.
- Writeable boxes: new website_writeable_boxes table (model + migration
  0006) + /admin/writeable-boxes CRUD; active boxes render on the public
  home page. env: BADGE_UPLOAD_DIR.

Verified live (prod, amx_test): all 8 pages render with real data; a test
writeable box appeared on the public home and was reverted. tsc 0,
vitest 49/49, next build 0 (7 new admin routes).
2026-06-28 21:04:34 +02:00

134 lines
5.1 KiB
TypeScript

import Link from 'next/link';
import { StatusCard } from '@/components/admin/dashboard';
import { siteSettings } from '@/lib/services/site-settings';
import { requireStaff } from '@/lib/admin/guard';
import { EmbedSnippet } from './embed-snippet';
export const dynamic = 'force-dynamic';
export const metadata = { title: 'Radio Embed' };
export default async function AdminRadioEmbedPage() {
await requireStaff();
// Read radio embed-related website_settings. siteSettings.get() already
// swallows DB errors and serves fallbacks, but wrap defensively anyway so a
// throw never blanks the page.
let streamUrl = '';
let radioName = 'Radio';
let nowPlayingApiUrl = '';
try {
streamUrl = (await siteSettings.get('radio_stream_url', '')) ?? '';
radioName =
(await siteSettings.get('radio_name', null)) ??
(await siteSettings.get('hotel_name', 'Radio')) ??
'Radio';
nowPlayingApiUrl = (await siteSettings.get('radio_now_playing_api_url', '')) ?? '';
} catch {
streamUrl = '';
}
streamUrl = streamUrl.trim();
nowPlayingApiUrl = nowPlayingApiUrl.trim();
const hasStream = streamUrl !== '';
// Copy-ready embed snippets. Built server-side from the configured stream URL
// so what staff copy matches what the preview renders.
const audioSnippet = hasStream
? `<audio controls preload="none" src="${streamUrl}">\n Your browser does not support the audio element.\n</audio>`
: '';
const iframeSnippet = hasStream
? `<iframe\n src="/radio/embed"\n title="${radioName}"\n width="320"\n height="120"\n frameborder="0"\n allow="autoplay"\n style="border:0;border-radius:8px;overflow:hidden"\n></iframe>`
: '';
return (
<main>
<nav className="muted" style={{ marginBottom: '0.5rem' }}>
<Link href="/admin/radio">Radio</Link> ·{' '}
<Link href="/admin/radio/settings">Settings</Link> ·{' '}
<Link href="/admin/radio/embed">Embed</Link> ·{' '}
<Link href="/admin/radio/banners">Banners</Link> ·{' '}
<Link href="/admin/radio/ranks">Ranks</Link> ·{' '}
<Link href="/admin/radio/history">History</Link>
</nav>
<section className="admin-section">
<h1>Radio Embed</h1>
<p className="muted" style={{ marginTop: '-0.4rem' }}>
Copy-ready embed code for the live stream. Read-only — nothing here
writes to the database. Configure the stream URL in{' '}
<Link href="/admin/radio/settings">Radio Settings</Link> (
<code>radio_stream_url</code>).
</p>
<div className="admin-stats">
<StatusCard
label="Stream URL"
value={hasStream ? 'Configured' : 'Missing'}
state={hasStream ? 'ok' : 'danger'}
icon="📻"
/>
<StatusCard label="Radio name" value={radioName} icon="🎙️" />
<StatusCard
label="Now-playing API"
value={nowPlayingApiUrl ? 'Set' : 'Not set'}
state={nowPlayingApiUrl ? 'ok' : 'neutral'}
icon="🎵"
/>
</div>
</section>
{!hasStream ? (
<section className="admin-section">
<h2>No stream URL configured</h2>
<p className="muted">
There is no <code>radio_stream_url</code> set, so there is nothing to
embed yet. Open{' '}
<Link href="/admin/radio/settings">Radio Settings</Link> and fill in
the <strong>Main stream URL</strong> field under{' '}
<em>AzureCast &amp; Stream</em>. Once saved, return here to copy the
embed snippet and preview the player.
</p>
<p>
<Link href="/admin/radio/settings" className="btn btn-primary">
Go to Radio Settings
</Link>
</p>
</section>
) : (
<>
<section className="admin-section">
<h2>Live preview</h2>
<p className="muted" style={{ marginTop: '-0.4rem' }}>
This is the bare <code>&lt;audio&gt;</code> player as visitors will
see it. Press play to confirm the stream is reachable.
</p>
<div className="card">
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<audio controls preload="none" src={streamUrl} style={{ width: '100%' }}>
Your browser does not support the audio element.
</audio>
<p className="muted" style={{ margin: '0.5rem 0 0', wordBreak: 'break-all' }}>
Source: <code>{streamUrl}</code>
</p>
</div>
</section>
<EmbedSnippet
title="Audio player snippet"
description="Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed."
snippet={audioSnippet}
/>
<EmbedSnippet
title="Iframe wrapper snippet"
description="Embeds the site's own radio player page in an iframe — keeps your styling and now-playing info. Adjust width/height to taste."
snippet={iframeSnippet}
/>
</>
)}
</main>
);
}