Files
Epicnabbo-Catalogus-Updated…/src/app/radio/schedule/page.tsx
T
Simo e004dfedaf i18n: translate all public page bodies (EN + IT)
Internationalised the ~44 public pages with next-intl (the shell was
already translated). Each page now pulls its copy from a "pages.<slug>"
namespace via getTranslations (server) / useTranslations (client); the
EN + IT catalogs were authored by parallel agents and merged centrally,
with natural Italian (ICU plurals) and it backfilled from en for any
gap. request.ts gained getMessageFallback/onError so a missing key
degrades to the English value, never a raw key.

Behaviour unchanged (only display text moved to t() calls; queries,
actions, fields, ContentCard structure preserved). Verified on the prod
server: with NEXT_LOCALE=it, home/community/staff/news/shop/rankings all
render Italian copy, no raw-key leakage; English unchanged. The nav
language switcher toggles EN/IT live. tsc 0, vitest 49/49, next build 0.

Admin pages intentionally left in English (staff tooling).
2026-06-28 20:40:06 +02:00

138 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
// Weekly order so the table reads Monday → Sunday regardless of insert order.
// The `day` column stores a free-form string; we key case-insensitively.
const DAYS = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
] as const;
// `start_time` / `end_time` are MySQL TIME columns; Prisma hydrates them as a
// Date on the 1970-01-01 epoch. Pull just HH:MM in UTC (Time has no zone).
function formatTime(d: Date | null | undefined): string {
if (!d) return "";
return d.toISOString().slice(11, 16);
}
type Slot = {
id: string;
dj: string;
showName: string | null;
description: string | null;
start: string;
end: string;
};
export default async function RadioSchedulePage() {
const t = await getTranslations("pages.radioSchedule");
const schedules = await prisma.radioSchedules
.findMany({
where: { isActive: true },
orderBy: { startTime: "asc" },
})
.catch(() => []);
// Resolve DJ usernames. schedule.userId is BigInt; User.id is Int — convert.
const djIds = Array.from(new Set(schedules.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((u) => [u.id, u.username]));
// Bucket slots by normalised day name.
const byDay = new Map<string, Slot[]>();
for (const day of DAYS) byDay.set(day, []);
for (const s of schedules) {
const key =
DAYS.find((d) => d.toLowerCase() === s.day.trim().toLowerCase()) ?? s.day;
const slot: Slot = {
id: String(s.id),
dj: djById.get(Number(s.userId)) ?? `User #${s.userId}`,
showName: s.showName,
description: s.description,
start: formatTime(s.startTime),
end: formatTime(s.endTime),
};
const bucket = byDay.get(key);
if (bucket) bucket.push(slot);
else byDay.set(key, [slot]);
}
const hasAny = schedules.length > 0;
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard
icon="🗓️"
title={t("title")}
subtitle={t("subtitle")}
/>
<ContentCard padded={!hasAny}>
{!hasAny ? (
<EmptyState icon="🗓️">{t("empty")}</EmptyState>
) : (
<table>
<thead>
<tr>
<th>{t("colDay")}</th>
<th>{t("colTime")}</th>
<th>{t("colDj")}</th>
<th>{t("colShow")}</th>
</tr>
</thead>
<tbody>
{DAYS.map((day) => {
const slots = byDay.get(day) ?? [];
if (slots.length === 0) {
return (
<tr key={day}>
<td>
<strong>{day}</strong>
</td>
<td colSpan={3} className="muted">
{t("noShows")}
</td>
</tr>
);
}
return slots.map((slot, i) => (
<tr key={slot.id}>
<td>{i === 0 ? <strong>{day}</strong> : ""}</td>
<td>
{slot.start}
{slot.end ? ` – ${slot.end}` : ""}
</td>
<td>{slot.dj}</td>
<td>
{slot.showName || <span className="muted">—</span>}
{slot.description ? (
<div className="muted">{slot.description}</div>
) : null}
</td>
</tr>
));
})}
</tbody>
</table>
)}
</ContentCard>
</main>
);
}