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).
134 lines
5.1 KiB
TypeScript
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 & 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><audio></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>
|
|
);
|
|
}
|