refactor: convert admin panel to Tailwind CSS
- Convert admin layout, sidebar, and navigation to Tailwind classes - Convert dashboard components (StatusCard, DiagnosticRow, etc.) to Tailwind - Convert all ~48 admin page files from admin CSS classes to Tailwind - Remove unused admin CSS from globals.css (973 → 712 lines) - Convert developers page badges to Tailwind - Remove <style jsx> block from OnlineUsersWidget
This commit is contained in:
1 parent
64f50b2dde
commit
bd299be55d
52 files changed
+277
-644
No files matched your search
@@ -74,7 +74,7 @@ export default async function AdminRadioApiKeysPage() {
|
||||
<Link href="/admin/radio/monitoring">Monitoring</Link>
|
||||
</nav>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h1>Radio API Keys</h1>
|
||||
<p className="muted" style={{ marginTop: "-0.4rem" }}>
|
||||
API keys for external radio integrations (<code>radio_api_keys</code>). Keys are generated
|
||||
@@ -88,7 +88,7 @@ export default async function AdminRadioApiKeysPage() {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-stats">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label="Total keys" value={total} icon="🔑" />
|
||||
<StatusCard
|
||||
label="Active"
|
||||
@@ -106,7 +106,7 @@ export default async function AdminRadioApiKeysPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Generate key</h2>
|
||||
<p className="muted" style={{ marginTop: "-0.4rem" }}>
|
||||
The secret key is generated automatically with cryptographic randomness — you do not enter
|
||||
@@ -161,7 +161,7 @@ export default async function AdminRadioApiKeysPage() {
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Existing keys ({total})</h2>
|
||||
{total === 0 ? (
|
||||
<p className="muted">No API keys yet — generate one above.</p>
|
||||
@@ -194,7 +194,7 @@ export default async function AdminRadioApiKeysPage() {
|
||||
<td className="muted">{formatDate(k.lastUsedAt)}</td>
|
||||
<td className="muted">{formatDate(k.expiresAt)}</td>
|
||||
<td>
|
||||
<span className={`admin-badge ${k.isActive ? "ok" : "danger"}`}>
|
||||
<span className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${k.isActive ? "bg-green-600/20 text-[var(--color-text)]" : "bg-red-500/22 text-[var(--color-text)]"}`}>
|
||||
{k.isActive ? "active" : "inactive"}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -76,7 +76,7 @@ export default async function AdminRadioAutoDjPage() {
|
||||
<Link href="/admin/radio/autodj">AutoDJ</Link>
|
||||
</nav>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Radio AutoDJ</h2>
|
||||
<p className="muted" style={{ marginTop: '-0.4rem' }}>
|
||||
Fallback playlist (<code>radio_auto_dj_playlist</code>) the radio rotates
|
||||
@@ -90,7 +90,7 @@ export default async function AdminRadioAutoDjPage() {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-stats">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label="Tracks" value={tracks.length} icon="🎵" />
|
||||
<StatusCard
|
||||
label="Active"
|
||||
@@ -108,7 +108,7 @@ export default async function AdminRadioAutoDjPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Add track</h2>
|
||||
<form action={createTrack} className="card">
|
||||
<div className="grid cols-2">
|
||||
@@ -186,7 +186,7 @@ export default async function AdminRadioAutoDjPage() {
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Playlist ({tracks.length})</h2>
|
||||
{tracks.length === 0 ? (
|
||||
<p className="muted">No AutoDJ tracks yet — add one above.</p>
|
||||
@@ -232,7 +232,7 @@ export default async function AdminRadioAutoDjPage() {
|
||||
<td className="muted">{t.playCount}</td>
|
||||
<td className="muted">{formatDate(t.lastPlayedAt)}</td>
|
||||
<td>
|
||||
<span className={`admin-badge ${t.isActive ? 'ok' : 'danger'}`}>
|
||||
<span className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${t.isActive ? 'bg-green-600/20 text-[var(--color-text)]' : 'bg-red-500/22 text-[var(--color-text)]'}`}>
|
||||
{t.isActive ? 'active' : 'inactive'}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default async function AdminRadioBannersPage() {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-stats">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label="Banners" value={banners.length} icon="🖼️" />
|
||||
<StatusCard
|
||||
label="Active"
|
||||
@@ -89,7 +89,7 @@ export default async function AdminRadioBannersPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Add banner</h2>
|
||||
<form action={createRadioBanner} className="card">
|
||||
<div className="grid cols-2">
|
||||
@@ -148,7 +148,7 @@ export default async function AdminRadioBannersPage() {
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Existing banners ({banners.length})</h2>
|
||||
{banners.length === 0 ? (
|
||||
<p className="muted">No banners yet — add one above.</p>
|
||||
@@ -170,7 +170,7 @@ export default async function AdminRadioBannersPage() {
|
||||
style={{ display: 'inline-flex', gap: '0.5rem', alignItems: 'center' }}
|
||||
>
|
||||
by user {String(b.userId)} · {formatDate(b.createdAt)} ·{' '}
|
||||
<span className={`admin-badge ${b.isActive ? 'ok' : 'danger'}`}>
|
||||
<span className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${b.isActive ? 'bg-green-600/20 text-[var(--color-text)]' : 'bg-red-500/22 text-[var(--color-text)]'}`}>
|
||||
{b.isActive ? 'active' : 'hidden'}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -37,7 +37,7 @@ export function EmbedSnippet({
|
||||
const lines = snippet.split('\n').length;
|
||||
|
||||
return (
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||
<h2 style={{ flex: 1, margin: 0 }}>{title}</h2>
|
||||
<button type="button" className="btn btn-primary" onClick={copy}>
|
||||
|
||||
@@ -53,7 +53,7 @@ export default async function AdminRadioEmbedPage() {
|
||||
<Link href="/admin/radio/history">History</Link>
|
||||
</nav>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h1>Radio Embed</h1>
|
||||
<p className="muted" style={{ marginTop: '-0.4rem' }}>
|
||||
Copy-ready embed code for the live stream. Read-only — nothing here
|
||||
@@ -62,7 +62,7 @@ export default async function AdminRadioEmbedPage() {
|
||||
<code>radio_stream_url</code>).
|
||||
</p>
|
||||
|
||||
<div className="admin-stats">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard
|
||||
label="Stream URL"
|
||||
value={hasStream ? 'Configured' : 'Missing'}
|
||||
@@ -80,7 +80,7 @@ export default async function AdminRadioEmbedPage() {
|
||||
</section>
|
||||
|
||||
{!hasStream ? (
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>No stream URL configured</h2>
|
||||
<p className="muted">
|
||||
There is no <code>radio_stream_url</code> set, so there is nothing to
|
||||
@@ -98,7 +98,7 @@ export default async function AdminRadioEmbedPage() {
|
||||
</section>
|
||||
) : (
|
||||
<>
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Live preview</h2>
|
||||
<p className="muted" style={{ marginTop: '-0.4rem' }}>
|
||||
This is the bare <code><audio></code> player as visitors will
|
||||
|
||||
@@ -95,7 +95,7 @@ export default async function AdminRadioHistoryPage() {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-stats">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label="Shows logged" value={history.length} icon="🎙️" />
|
||||
<StatusCard
|
||||
label="On air now"
|
||||
@@ -113,7 +113,7 @@ export default async function AdminRadioHistoryPage() {
|
||||
) : null}
|
||||
|
||||
{history.length > 0 ? (
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Show log ({history.length})</h2>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -139,7 +139,7 @@ export default async function AdminRadioHistoryPage() {
|
||||
{h.endedAt ? (
|
||||
<span className="muted">{formatDate(h.endedAt)}</span>
|
||||
) : (
|
||||
<span className="admin-badge ok">on air</span>
|
||||
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]">on air</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="muted">{formatDuration(h.startedAt, h.endedAt)}</td>
|
||||
|
||||
@@ -51,11 +51,11 @@ export default async function AdminRadioModerationPage() {
|
||||
Review and remove radio shouts (50 most recent).
|
||||
</p>
|
||||
|
||||
<div className="admin-stats">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label="Recent shouts" value={shouts.length} icon="📣" />
|
||||
</div>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Recent Shouts ({shouts.length})</h2>
|
||||
{shouts.length === 0 ? (
|
||||
<p className="muted">No shouts to moderate.</p>
|
||||
|
||||
@@ -224,7 +224,7 @@ export default async function AdminRadioMonitoringPage() {
|
||||
<Link href="/admin/radio/history">History</Link>
|
||||
</nav>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Radio Monitoring</h2>
|
||||
<p className="muted" style={{ marginTop: '-0.4rem' }}>
|
||||
Read-only live probe of the configured radio endpoints
|
||||
@@ -234,7 +234,7 @@ export default async function AdminRadioMonitoringPage() {
|
||||
<Link href="/admin/radio/settings">Settings</Link>.
|
||||
</p>
|
||||
|
||||
<div className="admin-stats">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard
|
||||
label="Stream"
|
||||
value={
|
||||
@@ -261,14 +261,14 @@ export default async function AdminRadioMonitoringPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Endpoint diagnostics</h2>
|
||||
<p className="muted" style={{ marginTop: '-0.4rem' }}>
|
||||
<span className="admin-badge ok">OK</span> reachable ·{' '}
|
||||
<span className="admin-badge">WARN</span> not configured ·{' '}
|
||||
<span className="admin-badge danger">ERROR</span> fetch failed / timed out.
|
||||
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]">OK</span> reachable ·{' '}
|
||||
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]">WARN</span> not configured ·{' '}
|
||||
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]">ERROR</span> fetch failed / timed out.
|
||||
</p>
|
||||
<div className="admin-diag">
|
||||
<div className="flex flex-col">
|
||||
{endpoints.map((ep) => {
|
||||
let state: 'ok' | 'warn' | 'danger';
|
||||
let detail: string;
|
||||
|
||||
@@ -121,7 +121,7 @@ export default async function AdminRadioPage() {
|
||||
Latest shouts, DJ applications and on-air schedules (25 most recent of each).
|
||||
</p>
|
||||
|
||||
<div className="admin-stats">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label="Recent shouts" value={shouts.length} icon="📣" />
|
||||
<StatusCard
|
||||
label="Pending applications"
|
||||
@@ -138,7 +138,7 @@ export default async function AdminRadioPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Recent Shouts ({shouts.length})</h2>
|
||||
{shouts.length === 0 ? (
|
||||
<p className="muted">No shouts to moderate.</p>
|
||||
@@ -175,7 +175,7 @@ export default async function AdminRadioPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>DJ Applications ({applications.length})</h2>
|
||||
{applications.length === 0 ? (
|
||||
<p className="muted">No applications submitted.</p>
|
||||
@@ -203,7 +203,7 @@ export default async function AdminRadioPage() {
|
||||
<td>{a.musicStyle ?? '—'}</td>
|
||||
<td>{a.discordUsername ?? '—'}</td>
|
||||
<td>
|
||||
<span className={`admin-badge ${statusBadgeClass(a.status)}`}>
|
||||
<span className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${statusBadgeClass(a.status) === 'ok' ? 'bg-green-600/20 text-[var(--color-text)]' : statusBadgeClass(a.status) === 'danger' ? 'bg-red-500/22 text-[var(--color-text)]' : 'bg-[var(--color-primary)]/18 text-[var(--color-text)]'}`}>
|
||||
{a.status}
|
||||
</span>
|
||||
</td>
|
||||
@@ -215,7 +215,7 @@ export default async function AdminRadioPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Schedules ({schedules.length})</h2>
|
||||
{schedules.length === 0 ? (
|
||||
<p className="muted">No schedules configured.</p>
|
||||
@@ -242,7 +242,7 @@ export default async function AdminRadioPage() {
|
||||
<td>{formatTime(sc.endTime)}</td>
|
||||
<td>{sc.showName ?? '—'}</td>
|
||||
<td>
|
||||
<span className={`admin-badge ${sc.isActive ? 'ok' : 'danger'}`}>
|
||||
<span className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${sc.isActive ? 'bg-green-600/20 text-[var(--color-text)]' : 'bg-red-500/22 text-[var(--color-text)]'}`}>
|
||||
{sc.isActive ? 'active' : 'inactive'}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -66,7 +66,7 @@ export default async function AdminRadioPointsPage({
|
||||
<Link href="/admin/radio/history">History</Link>
|
||||
</nav>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Radio Listener Points</h2>
|
||||
<p className="muted" style={{ marginTop: "-0.4rem" }}>
|
||||
Reward listeners with currency for time spent on the radio. Edits write
|
||||
@@ -77,11 +77,11 @@ export default async function AdminRadioPointsPage({
|
||||
|
||||
{saved === "1" ? (
|
||||
<p>
|
||||
<span className="admin-badge ok">Saved</span>
|
||||
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]">Saved</span>
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="admin-stats">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard
|
||||
label="Points"
|
||||
value={enabled ? "Enabled" : "Disabled"}
|
||||
@@ -103,7 +103,7 @@ export default async function AdminRadioPointsPage({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Settings</h2>
|
||||
<form action={savePoints}>
|
||||
<div className="grid cols-2">
|
||||
|
||||
@@ -61,7 +61,7 @@ export default async function AdminRadioRanksPage() {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-stats">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label="Ranks" value={ranks.length} icon="🎖️" />
|
||||
<StatusCard
|
||||
label="Active"
|
||||
@@ -72,7 +72,7 @@ export default async function AdminRadioRanksPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Add rank</h2>
|
||||
<form action={createRadioRank} className="card">
|
||||
<div className="grid cols-3">
|
||||
@@ -108,7 +108,7 @@ export default async function AdminRadioRanksPage() {
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className="admin-section">
|
||||
<section className="mt-6">
|
||||
<h2>Existing ranks ({ranks.length})</h2>
|
||||
{ranks.length === 0 ? (
|
||||
<p className="muted">No ranks yet — add one above.</p>
|
||||
@@ -126,7 +126,7 @@ export default async function AdminRadioRanksPage() {
|
||||
}}
|
||||
>
|
||||
<strong>#{String(r.id)}</strong>
|
||||
<span className={`admin-badge ${r.isActive ? 'ok' : 'danger'}`}>
|
||||
<span className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${r.isActive ? 'bg-green-600/20 text-[var(--color-text)]' : 'bg-red-500/22 text-[var(--color-text)]'}`}>
|
||||
{r.isActive ? 'active' : 'inactive'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -241,7 +241,7 @@ export default async function AdminRadioSettingsPage() {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-stats">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard
|
||||
label="Radio"
|
||||
value={radioEnabled ? 'Enabled' : 'Disabled'}
|
||||
|
||||
Reference in new issue
Block a user