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).
This commit is contained in:
1 parent
e004dfedaf
commit
0cd4d06ff6
21 files changed
+2443
-61
No files matched your search
@@ -0,0 +1,133 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user