Adapt + improve all public pages to the atom design system

Extended the same design-system treatment to the public site, faithful
to AtomCMS's "atom" theme. New src/components/public/ui.tsx provides the
signature atom building blocks + a scoped CSS layer:
- ContentCard: surface card with a primary-tinted header (icon circle +
  title + subtitle) and padded body — the atom content-card, used as
  every page's header and section wrapper.
- StatBlock / stat-grid, EmptyState, OnlineBadge (online/offline pill),
  RankBadge (medals for the top 3), and responsive .card-grid helpers.

Swept ~45 public pages (home/community/rankings hand-built as the
reference; the rest via parallel agents that read the schema and
preserved every query, server action, auth gate and field name):
heroes → ContentCard headers, lists → card-grids in ContentCards,
"no X" → EmptyState, status → OnlineBadge/RankBadge. The leaderboard
gained Credits/Diamonds/Duckets tabs (usersCurrency).

Behaviour unchanged. Verified on the prod server against amx_test: 15+
public pages render the content-cards/grids with real data, no errors;
computed styles confirm the tinted header, icon circle, medal + online
pills. Fixed an undefined --color-golden ref. tsc 0, vitest 49/49,
next build 0.
This commit is contained in:
Simo committed 2026-06-28 19:28:36 +02:00
1 parent 0de0ac6a3c
commit e9ea19795a
47 files changed
+2264 -1714

No files matched your search

+88 -64
View File
@@ -1,4 +1,5 @@
import Link from "next/link";
import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
@@ -6,12 +7,12 @@ import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic";
// Sub-pages linked from the radio home hub.
const RADIO_LINKS: { label: string; href: string }[] = [
{ label: "Schedule", href: "/radio/schedule" },
{ label: "Shouts", href: "/radio/shouts" },
{ label: "Requests", href: "/radio/requests" },
{ label: "Apply as DJ", href: "/radio/apply" },
{ label: "Leaderboard", href: "/radio/leaderboard" },
const RADIO_LINKS: { label: string; href: string; icon: string; text: string }[] = [
{ label: "Schedule", href: "/radio/schedule", icon: "🗓️", text: "Our weekly line-up of DJs and shows." },
{ label: "Shouts", href: "/radio/shouts", icon: "📣", text: "Shout-outs and requests from the community." },
{ label: "Requests", href: "/radio/requests", icon: "🎵", text: "Ask the DJ to spin your favourite track." },
{ label: "Apply as DJ", href: "/radio/apply", icon: "🎤", text: "Join the team and take to the airwaves." },
{ label: "Leaderboard", href: "/radio/leaderboard", icon: "🏆", text: "Top listeners ranked by points earned." },
];
// Canonical weekday order so today's slots come out chronologically and we can
@@ -69,28 +70,30 @@ export default async function RadioPage() {
: [];
const djById = new Map(djs.map((d) => [d.id, d.username]));
return (
<main>
{/* ── Hero: live stream + now playing ─────────────────────── */}
<div className="hero">
<h1 style={{ marginTop: 0 }}>Radio</h1>
<p className="muted" style={{ marginTop: 0 }}>
Tune in to the hotel radio — live DJ sets, requests and shoutouts.
</p>
const isLive = Boolean(streamUrl);
<div className="grid cols-2" style={{ marginTop: "1rem" }}>
<div>
<p style={{ margin: "0 0 0.25rem" }}>
<strong>Now playing</strong>
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
{/* ── Header: live stream + now playing ─────────────────────── */}
<ContentCard
icon="📻"
title="Radio"
subtitle="Tune in to the hotel radio — live DJ sets, requests and shoutouts."
action={<OnlineBadge online={isLive} />}
>
<div className="card-grid sm-2" style={{ alignItems: "stretch" }}>
<div className="card" style={{ display: "grid", gap: "0.35rem", alignContent: "center" }}>
<p className="muted" style={{ margin: 0, fontSize: "0.8rem", textTransform: "uppercase", letterSpacing: "0.05em" }}>
Now playing
</p>
<p style={{ margin: "0 0 0.5rem", fontSize: "1.1rem" }}>
{currentSong ? currentSong : <span className="muted">Nothing playing right now.</span>}
<p style={{ margin: 0, fontSize: "1.1rem", fontWeight: 700 }}>
{currentSong ? currentSong : <span className="muted" style={{ fontWeight: 400 }}>Nothing playing right now.</span>}
</p>
<p className="muted" style={{ margin: 0 }}>
{currentDj ? `On air: ${currentDj}` : "No DJ on air."}
</p>
</div>
<div style={{ display: "flex", alignItems: "center" }}>
<div className="card" style={{ display: "flex", alignItems: "center" }}>
{streamUrl ? (
<audio
controls
@@ -107,60 +110,81 @@ export default async function RadioPage() {
)}
</div>
</div>
</div>
</ContentCard>
{/* ── Sub-page links ──────────────────────────────────────── */}
<section style={{ marginBottom: "1.5rem" }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.6rem" }}>
<ContentCard
icon="🎚️"
title="Explore the radio"
subtitle="Schedule, shouts, requests and more"
>
<div className="card-grid sm-2 lg-3">
{RADIO_LINKS.map((l) => (
<Link key={l.href} href={l.href} className="btn btn-outline">
{l.label}
<Link
key={l.href}
href={l.href}
className="card hover"
style={{ color: "inherit", textDecoration: "none", display: "grid", gap: "0.4rem" }}
>
<p style={{ margin: 0, fontWeight: 700 }}>
<span aria-hidden>{l.icon}</span> {l.label}
</p>
<p className="muted" style={{ margin: 0 }}>
{l.text}
</p>
</Link>
))}
</div>
</section>
</ContentCard>
{/* ── Today's schedule ────────────────────────────────────── */}
<section>
<h2>Today&rsquo;s schedule{today ? ` · ${today}` : ""}</h2>
<ContentCard
icon="🗓️"
title={`Today's schedule${today ? ` · ${today}` : ""}`}
subtitle="What's on the airwaves today"
action={
<Link className="btn btn-outline" href="/radio/schedule">
Full week →
</Link>
}
padded={todaysSlots.length === 0}
>
{todaysSlots.length === 0 ? (
<p className="muted">No shows scheduled for today.</p>
<EmptyState icon="📻">No shows scheduled for today.</EmptyState>
) : (
<div className="card">
<table>
<thead>
<tr>
<th>Time</th>
<th>Show</th>
<th>DJ</th>
</tr>
</thead>
<tbody>
{todaysSlots.map((s) => {
const djName = djById.get(Number(s.userId));
return (
<tr key={String(s.id)}>
<td>
{formatTime(s.startTime)} &ndash; {formatTime(s.endTime)}
</td>
<td>
{s.showName || <span className="muted">Untitled show</span>}
</td>
<td>
{djName ? (
<Link href={`/u/${djName}`}>{djName}</Link>
) : (
<span className="muted">{`User #${s.userId}`}</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<table>
<thead>
<tr>
<th>Time</th>
<th>Show</th>
<th>DJ</th>
</tr>
</thead>
<tbody>
{todaysSlots.map((s) => {
const djName = djById.get(Number(s.userId));
return (
<tr key={String(s.id)}>
<td>
{formatTime(s.startTime)} &ndash; {formatTime(s.endTime)}
</td>
<td>
{s.showName || <span className="muted">Untitled show</span>}
</td>
<td>
{djName ? (
<Link href={`/u/${djName}`}>{djName}</Link>
) : (
<span className="muted">{`User #${s.userId}`}</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</section>
</ContentCard>
</main>
);
}