import { asc, eq, inArray } from "drizzle-orm"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui"; import { db, RadioSchedules, User } from "@/lib/db"; import { siteSettings } from "@/lib/services/site-settings"; // TODO: Cache Components adoption. Refactor this route so this opt-out can be removed. // See: https://nextjs.org/docs/app/guides/migrating-to-cache-components export const instant = false; // Radio config + today's schedule are read live from the DB / settings table. // 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 // 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 db .select() .from(RadioSchedules) .where(eq(RadioSchedules.isActive, true)) .orderBy(asc(RadioSchedules.startTime)) .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 db .select({ id: User.id, username: User.username }) .from(User) .where(inArray(User.id, djIds)) .catch(() => []) : []; const djById = new Map(djs.map((d) => [d.id, d.username])); const isLive = Boolean(streamUrl); return (
{/* ── Header: live stream + now playing ─────────────────────── */} } >

{t("nowPlaying")}

{currentSong ? ( currentSong ) : ( {t("nothingPlaying")} )}

{currentDj ? t("onAir", { dj: currentDj }) : t("noDj")}

{streamUrl ? ( ) : (

{t("streamOffline")}

)}
{/* ── Sub-page links ──────────────────────────────────────── */}
{RADIO_LINKS.map((l) => (

{l.icon} {l.label}

{l.text}

))}
{/* ── Today's schedule ────────────────────────────────────── */} {t("fullWeek")} } padded={todaysSlots.length === 0} > {todaysSlots.length === 0 ? ( {t("noShowsToday")} ) : ( {todaysSlots.map((s) => { const djName = djById.get(Number(s.userId)); return ( ); })}
{t("colTime")} {t("colShow")} {t("colDj")}
{formatTime(s.startTime)} – {formatTime(s.endTime)} {s.showName || ( {t("untitledShow")} )} {djName ? ( {djName} ) : ( {t("userNumber", { id: String(s.userId) })} )}
)}
); }