Decouple admin from public max-w-7xl via (site) route group.
Local Build and Deploy / deploy (push) Successful in 54s
Local Build and Deploy / deploy (push) Successful in 54s
Public chrome and width live only under (site). /admin and /client inherit the root layout alone, so housekeeping width is no longer tied to the public template. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
a1dc7ff0a7
commit
367f69a439
60 files changed
+29
-30
No files matched your search
@@ -0,0 +1,252 @@
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
// Radio config + today's schedule are read live from the DB / settings table.
|
||||
export const revalidate = 300;
|
||||
|
||||
// Canonical weekday order so today's slots come out chronologically and we can
|
||||
// match the current day against whatever casing the `day` column stores.
|
||||
const WEEKDAYS = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
];
|
||||
|
||||
// radio_schedules.start_time / end_time are stored as TIME and surface through
|
||||
// Prisma as a Date with the time component on a 1970 epoch date — render just
|
||||
// the HH:MM portion in UTC to avoid timezone drift.
|
||||
function formatTime(d: Date | null | undefined): string {
|
||||
if (!d) return "";
|
||||
return d.toISOString().slice(11, 16);
|
||||
}
|
||||
|
||||
export default async function RadioPage() {
|
||||
const t = await getTranslations("pages.radio");
|
||||
|
||||
// Sub-pages linked from the radio home hub.
|
||||
const RADIO_LINKS: {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: string;
|
||||
text: string;
|
||||
}[] = [
|
||||
{
|
||||
label: t("linkSchedule"),
|
||||
href: "/radio/schedule",
|
||||
icon: "🗓️",
|
||||
text: t("linkScheduleText"),
|
||||
},
|
||||
{
|
||||
label: t("linkShouts"),
|
||||
href: "/radio/shouts",
|
||||
icon: "📣",
|
||||
text: t("linkShoutsText"),
|
||||
},
|
||||
{
|
||||
label: t("linkRequests"),
|
||||
href: "/radio/requests",
|
||||
icon: "🎵",
|
||||
text: t("linkRequestsText"),
|
||||
},
|
||||
{
|
||||
label: t("linkApply"),
|
||||
href: "/radio/apply",
|
||||
icon: "🎤",
|
||||
text: t("linkApplyText"),
|
||||
},
|
||||
{
|
||||
label: t("linkLeaderboard"),
|
||||
href: "/radio/leaderboard",
|
||||
icon: "🏆",
|
||||
text: t("linkLeaderboardText"),
|
||||
},
|
||||
];
|
||||
|
||||
// ── Radio config from website_settings (graceful fallbacks) ──
|
||||
const [streamUrl, currentDj, currentSong] = await Promise.all([
|
||||
siteSettings.get("radio_stream_url", ""),
|
||||
siteSettings.get("radio_current_dj", ""),
|
||||
siteSettings.get("radio_current_song", ""),
|
||||
]);
|
||||
|
||||
// ── Today's schedule (isolated; a DB hiccup degrades to empty) ──
|
||||
const today = WEEKDAYS[new Date().getUTCDay()] ?? "";
|
||||
const schedules = await prisma.radioSchedules
|
||||
.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: { startTime: "asc" },
|
||||
})
|
||||
.catch(() => []);
|
||||
|
||||
const todaysSlots = schedules.filter(
|
||||
(s) => s.day.toLowerCase() === today.toLowerCase(),
|
||||
);
|
||||
|
||||
// Resolve DJ usernames for today's slots in one query. radio_schedules.user_id
|
||||
// is an UnsignedBigInt while users.id is an Int, so narrow to Number for the
|
||||
// lookup (DJ ids comfortably fit in a JS-safe integer).
|
||||
const djIds = Array.from(new Set(todaysSlots.map((s) => Number(s.userId))));
|
||||
const djs = djIds.length
|
||||
? await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: djIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
.catch(() => [])
|
||||
: [];
|
||||
const djById = new Map(djs.map((d) => [d.id, d.username]));
|
||||
|
||||
const isLive = Boolean(streamUrl);
|
||||
|
||||
return (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
{/* ── Header: live stream + now playing ─────────────────────── */}
|
||||
<ContentCard
|
||||
icon="📻"
|
||||
title={t("title")}
|
||||
subtitle={t("subtitle")}
|
||||
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",
|
||||
}}
|
||||
>
|
||||
{t("nowPlaying")}
|
||||
</p>
|
||||
<p style={{ margin: 0, fontSize: "1.1rem", fontWeight: 700 }}>
|
||||
{currentSong ? (
|
||||
currentSong
|
||||
) : (
|
||||
<span className="muted" style={{ fontWeight: 400 }}>
|
||||
{t("nothingPlaying")}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="muted" style={{ margin: 0 }}>
|
||||
{currentDj ? t("onAir", { dj: currentDj }) : t("noDj")}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="card"
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
{streamUrl ? (
|
||||
<audio
|
||||
controls
|
||||
src={streamUrl}
|
||||
style={{ width: "100%" }}
|
||||
preload="none"
|
||||
>
|
||||
{t("audioUnsupported")}
|
||||
</audio>
|
||||
) : (
|
||||
<p className="muted" style={{ margin: 0 }}>
|
||||
{t("streamOffline")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ContentCard>
|
||||
|
||||
{/* ── Sub-page links ──────────────────────────────────────── */}
|
||||
<ContentCard
|
||||
icon="🎚️"
|
||||
title={t("exploreTitle")}
|
||||
subtitle={t("exploreSubtitle")}
|
||||
>
|
||||
<div className="card-grid sm-2 lg-3">
|
||||
{RADIO_LINKS.map((l) => (
|
||||
<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>
|
||||
</ContentCard>
|
||||
|
||||
{/* ── Today's schedule ────────────────────────────────────── */}
|
||||
<ContentCard
|
||||
icon="🗓️"
|
||||
title={today ? t("todayTitleWithDay", { day: today }) : t("todayTitle")}
|
||||
subtitle={t("todaySubtitle")}
|
||||
action={
|
||||
<Link className="btn btn-outline" href="/radio/schedule">
|
||||
{t("fullWeek")}
|
||||
</Link>
|
||||
}
|
||||
padded={todaysSlots.length === 0}
|
||||
>
|
||||
{todaysSlots.length === 0 ? (
|
||||
<EmptyState icon="📻">{t("noShowsToday")}</EmptyState>
|
||||
) : (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("colTime")}</th>
|
||||
<th>{t("colShow")}</th>
|
||||
<th>{t("colDj")}</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">{t("untitledShow")}</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{djName ? (
|
||||
<Link href={`/u/${djName}`}>{djName}</Link>
|
||||
) : (
|
||||
<span className="muted">
|
||||
{t("userNumber", { id: String(s.userId) })}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</ContentCard>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user