Adapt + improve all admin pages to the new design system
Swept the whole housekeeping panel (34 pages) so it's a consistent, improved adaptation of the old Filament admin rather than bare tables: - Live status tiles (StatusCard) summarising each page's data — counts and stateful metrics (users online, active/permanent bans, pending applications, settings, radio shouts, etc.). - Status pills (admin-badge ok/danger) replacing ad-hoc text: online/ offline, account/ip ban types, permanent vs timed, command OK/FAIL, alert severity & read/new, enabled/disabled, open/closed. - Sections wrapped in .admin-section with h2 headers; redundant inline table styling removed (the global .admin layer handles it). - alerts/logs/emulator hand-built as the reference pattern; the rest via parallel agents that read the schema and preserved every server action, query and field name. Behaviour unchanged (same actions, queries, field names). Verified in a real authed admin session (prod, amx_test): pages render with live data (Users tiles show 7 real accounts + online/offline pills), no errors. tsc 0, vitest 49/49, next build 0.
This commit is contained in:
1 parent
d419731566
commit
0de0ac6a3c
34 files changed
+2357
-1396
No files matched your search
@@ -1,5 +1,7 @@
|
||||
import Link from 'next/link';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { deleteShout } from '@/actions/admin-radio';
|
||||
import { StatusCard } from '@/components/admin/dashboard';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -39,6 +41,14 @@ function formatTime(d: Date | null): string {
|
||||
return d ? d.toISOString().slice(11, 16) : '—';
|
||||
}
|
||||
|
||||
// Map an application status to an ok/danger/neutral pill tone.
|
||||
function statusBadgeClass(status: string): string {
|
||||
const s = status.toLowerCase();
|
||||
if (s === 'approved' || s === 'accepted') return 'ok';
|
||||
if (s === 'rejected' || s === 'denied') return 'danger';
|
||||
return '';
|
||||
}
|
||||
|
||||
export default async function AdminRadioPage() {
|
||||
let shouts: Shout[] = [];
|
||||
let applications: Application[] = [];
|
||||
@@ -91,14 +101,47 @@ export default async function AdminRadioPage() {
|
||||
schedules = [];
|
||||
}
|
||||
|
||||
const pendingApps = applications.filter(
|
||||
(a) => a.status.toLowerCase() === 'pending',
|
||||
).length;
|
||||
const activeSchedules = schedules.filter((sc) => sc.isActive).length;
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Radio Moderation</h1>
|
||||
<nav className="muted" style={{ marginBottom: '0.5rem' }}>
|
||||
<Link href="/admin/radio">Radio</Link> ·{' '}
|
||||
<Link href="/admin/radio/settings">Settings</Link> ·{' '}
|
||||
<Link href="/admin/radio/banners">Banners</Link> ·{' '}
|
||||
<Link href="/admin/radio/ranks">Ranks</Link> ·{' '}
|
||||
<Link href="/admin/radio/history">History</Link>
|
||||
</nav>
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2>Recent Shouts</h2>
|
||||
<h1>Radio Moderation</h1>
|
||||
<p className="muted" style={{ marginTop: '-0.4rem' }}>
|
||||
Latest shouts, DJ applications and on-air schedules (25 most recent of each).
|
||||
</p>
|
||||
|
||||
<div className="admin-stats">
|
||||
<StatusCard label="Recent shouts" value={shouts.length} icon="📣" />
|
||||
<StatusCard
|
||||
label="Pending applications"
|
||||
value={pendingApps}
|
||||
state={pendingApps > 0 ? 'warn' : 'ok'}
|
||||
icon="📝"
|
||||
/>
|
||||
<StatusCard label="Schedules" value={schedules.length} icon="🗓️" />
|
||||
<StatusCard
|
||||
label="Active schedules"
|
||||
value={activeSchedules}
|
||||
state={activeSchedules > 0 ? 'ok' : 'neutral'}
|
||||
icon="🎙️"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className="admin-section">
|
||||
<h2>Recent Shouts ({shouts.length})</h2>
|
||||
{shouts.length === 0 ? (
|
||||
<p className="muted">No shouts.</p>
|
||||
<p className="muted">No shouts to moderate.</p>
|
||||
) : (
|
||||
<table>
|
||||
<thead>
|
||||
@@ -132,10 +175,10 @@ export default async function AdminRadioPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2>DJ Applications</h2>
|
||||
<section className="admin-section">
|
||||
<h2>DJ Applications ({applications.length})</h2>
|
||||
{applications.length === 0 ? (
|
||||
<p className="muted">No applications.</p>
|
||||
<p className="muted">No applications submitted.</p>
|
||||
) : (
|
||||
<table>
|
||||
<thead>
|
||||
@@ -159,7 +202,11 @@ export default async function AdminRadioPage() {
|
||||
<td>{a.age}</td>
|
||||
<td>{a.musicStyle ?? '—'}</td>
|
||||
<td>{a.discordUsername ?? '—'}</td>
|
||||
<td>{a.status}</td>
|
||||
<td>
|
||||
<span className={`admin-badge ${statusBadgeClass(a.status)}`}>
|
||||
{a.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="muted">{formatDate(a.createdAt)}</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -168,10 +215,10 @@ export default async function AdminRadioPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Schedules</h2>
|
||||
<section className="admin-section">
|
||||
<h2>Schedules ({schedules.length})</h2>
|
||||
{schedules.length === 0 ? (
|
||||
<p className="muted">No schedules.</p>
|
||||
<p className="muted">No schedules configured.</p>
|
||||
) : (
|
||||
<table>
|
||||
<thead>
|
||||
@@ -194,7 +241,11 @@ export default async function AdminRadioPage() {
|
||||
<td>{formatTime(sc.startTime)}</td>
|
||||
<td>{formatTime(sc.endTime)}</td>
|
||||
<td>{sc.showName ?? '—'}</td>
|
||||
<td>{sc.isActive ? '●' : '○'}</td>
|
||||
<td>
|
||||
<span className={`admin-badge ${sc.isActive ? 'ok' : 'danger'}`}>
|
||||
{sc.isActive ? 'active' : 'inactive'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in new issue
Block a user