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:
1 parent
0de0ac6a3c
commit
e9ea19795a
47 files changed
+2264
-1714
No files matched your search
+88
-64
@@ -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’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)} – {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)} – {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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user