Reapply "Add missing admin action files and navigation links"
This reverts commit 4d515bc400.
This commit is contained in:
1 parent
96ed768f14
commit
5b4228261a
338 files changed
+28143
-5948
No files matched your search
@@ -25,17 +25,11 @@ export default async function RadioApplyPage() {
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
const submittedDate = existing?.createdAt
|
||||
? existing.createdAt.toISOString().slice(0, 10)
|
||||
: null;
|
||||
const submittedDate = existing?.createdAt ? existing.createdAt.toISOString().slice(0, 10) : null;
|
||||
|
||||
return (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
<ContentCard
|
||||
icon="🎤"
|
||||
title={t("title")}
|
||||
subtitle={t("subtitle")}
|
||||
/>
|
||||
<ContentCard icon="🎤" title={t("title")} subtitle={t("subtitle")} />
|
||||
|
||||
{existing ? (
|
||||
<ContentCard icon="📋" title={t("latestTitle")}>
|
||||
|
||||
@@ -10,11 +10,7 @@ function formatDate(d: Date | null | undefined): string {
|
||||
return d ? d.toISOString().slice(0, 19).replace("T", " ") : "";
|
||||
}
|
||||
|
||||
export default async function RadioContestDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
export default async function RadioContestDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
await getTranslations("pages.radioContests");
|
||||
|
||||
@@ -25,9 +21,7 @@ export default async function RadioContestDetailPage({
|
||||
notFound();
|
||||
}
|
||||
|
||||
const contest = await prisma.radioContests
|
||||
.findUnique({ where: { id: contestId } })
|
||||
.catch(() => null);
|
||||
const contest = await prisma.radioContests.findUnique({ where: { id: contestId } }).catch(() => null);
|
||||
|
||||
if (!contest) notFound();
|
||||
|
||||
|
||||
@@ -23,11 +23,7 @@ export default async function RadioContestsPage() {
|
||||
|
||||
return (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
<ContentCard
|
||||
icon="🎉"
|
||||
title={t("title")}
|
||||
subtitle={t("subtitle")}
|
||||
/>
|
||||
<ContentCard icon="🎉" title={t("title")} subtitle={t("subtitle")} />
|
||||
|
||||
<ContentCard padded={contests.length === 0}>
|
||||
{contests.length === 0 ? (
|
||||
@@ -52,7 +48,9 @@ export default async function RadioContestsPage() {
|
||||
</p>
|
||||
) : null}
|
||||
<p className="muted" style={{ margin: 0, fontSize: "0.85rem" }}>
|
||||
{c.startDate ? `${formatDate(c.startDate)} — ${c.endDate ? formatDate(c.endDate) : "ongoing"}` : t("dateUnknown")}
|
||||
{c.startDate
|
||||
? `${formatDate(c.startDate)} — ${c.endDate ? formatDate(c.endDate) : "ongoing"}`
|
||||
: t("dateUnknown")}
|
||||
</p>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -10,11 +10,7 @@ function formatDate(d: Date | null | undefined): string {
|
||||
return d ? d.toISOString().slice(0, 19).replace("T", " ") : "";
|
||||
}
|
||||
|
||||
export default async function RadioGiveawayDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
export default async function RadioGiveawayDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
await getTranslations("pages.radioGiveaways");
|
||||
|
||||
@@ -25,14 +21,14 @@ export default async function RadioGiveawayDetailPage({
|
||||
notFound();
|
||||
}
|
||||
|
||||
const giveaway = await prisma.radioGiveaways
|
||||
.findUnique({ where: { id: giveawayId } })
|
||||
.catch(() => null);
|
||||
const giveaway = await prisma.radioGiveaways.findUnique({ where: { id: giveawayId } }).catch(() => null);
|
||||
|
||||
if (!giveaway) notFound();
|
||||
|
||||
const active = giveaway.isActive ? "Active" : "Ended";
|
||||
const prizeStr = giveaway.prize || (giveaway.prizeAmount > 0 ? `${giveaway.prizeAmount} ${giveaway.prizeCurrency || "credits"}` : null);
|
||||
const prizeStr =
|
||||
giveaway.prize ||
|
||||
(giveaway.prizeAmount > 0 ? `${giveaway.prizeAmount} ${giveaway.prizeCurrency || "credits"}` : null);
|
||||
|
||||
return (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
|
||||
@@ -17,17 +17,21 @@ export default async function RadioGiveawaysPage() {
|
||||
where: { isActive: true },
|
||||
orderBy: { startDate: "desc" },
|
||||
take: 50,
|
||||
select: { id: true, title: true, prize: true, prizeAmount: true, prizeCurrency: true, startDate: true, endDate: true },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
prize: true,
|
||||
prizeAmount: true,
|
||||
prizeCurrency: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
},
|
||||
})
|
||||
.catch(() => []);
|
||||
|
||||
return (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
<ContentCard
|
||||
icon="🎁"
|
||||
title={t("title")}
|
||||
subtitle={t("subtitle")}
|
||||
/>
|
||||
<ContentCard icon="🎁" title={t("title")} subtitle={t("subtitle")} />
|
||||
|
||||
<ContentCard padded={giveaways.length === 0}>
|
||||
{giveaways.length === 0 ? (
|
||||
@@ -36,7 +40,8 @@ export default async function RadioGiveawaysPage() {
|
||||
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
|
||||
{giveaways.map((g) => {
|
||||
const id = g.id.toString();
|
||||
const prizeStr = g.prize || (g.prizeAmount > 0 ? `${g.prizeAmount} ${g.prizeCurrency || "credits"}` : null);
|
||||
const prizeStr =
|
||||
g.prize || (g.prizeAmount > 0 ? `${g.prizeAmount} ${g.prizeCurrency || "credits"}` : null);
|
||||
return (
|
||||
<Link
|
||||
key={id}
|
||||
@@ -53,7 +58,9 @@ export default async function RadioGiveawaysPage() {
|
||||
</p>
|
||||
) : null}
|
||||
<p className="muted" style={{ margin: 0, fontSize: "0.85rem" }}>
|
||||
{g.startDate ? `${formatDate(g.startDate)} — ${g.endDate ? formatDate(g.endDate) : "ongoing"}` : t("dateUnknown")}
|
||||
{g.startDate
|
||||
? `${formatDate(g.startDate)} — ${g.endDate ? formatDate(g.endDate) : "ongoing"}`
|
||||
: t("dateUnknown")}
|
||||
</p>
|
||||
</Link>
|
||||
);
|
||||
|
||||
+16
-10
@@ -43,7 +43,9 @@ export default async function RadioLayout({ children }: { children: ReactNode })
|
||||
marginBottom: "0.25rem",
|
||||
}}
|
||||
>
|
||||
<span aria-hidden style={{ fontSize: "1.2rem" }}>📻</span>
|
||||
<span aria-hidden style={{ fontSize: "1.2rem" }}>
|
||||
📻
|
||||
</span>
|
||||
Radio
|
||||
</Link>
|
||||
{RADIO_SIDEBAR_LINKS.map((link) => (
|
||||
@@ -63,18 +65,22 @@ export default async function RadioLayout({ children }: { children: ReactNode })
|
||||
transition: "background 0.15s",
|
||||
}}
|
||||
>
|
||||
<span aria-hidden style={{ flex: "none", fontSize: "1rem" }}>{link.icon}</span>
|
||||
{link.key === "schedule" ? t("linkSchedule") :
|
||||
link.key === "shouts" ? t("linkShouts") :
|
||||
link.key === "requests" ? t("linkRequests") :
|
||||
link.key === "apply" ? t("linkApply") :
|
||||
t("linkLeaderboard")}
|
||||
<span aria-hidden style={{ flex: "none", fontSize: "1rem" }}>
|
||||
{link.icon}
|
||||
</span>
|
||||
{link.key === "schedule"
|
||||
? t("linkSchedule")
|
||||
: link.key === "shouts"
|
||||
? t("linkShouts")
|
||||
: link.key === "requests"
|
||||
? t("linkRequests")
|
||||
: link.key === "apply"
|
||||
? t("linkApply")
|
||||
: t("linkLeaderboard")}
|
||||
</Link>
|
||||
))}
|
||||
</aside>
|
||||
<section style={{ flex: 1, minWidth: 0 }}>
|
||||
{children}
|
||||
</section>
|
||||
<section style={{ flex: 1, minWidth: 0 }}>{children}</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -48,11 +48,7 @@ export default async function RadioLeaderboardPage() {
|
||||
|
||||
return (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
<ContentCard
|
||||
icon="🏆"
|
||||
title={t("title")}
|
||||
subtitle={t("subtitle")}
|
||||
/>
|
||||
<ContentCard icon="🏆" title={t("title")} subtitle={t("subtitle")} />
|
||||
|
||||
<ContentCard padded={rows.length === 0}>
|
||||
{rows.length === 0 ? (
|
||||
@@ -89,16 +85,11 @@ export default async function RadioLeaderboardPage() {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Link
|
||||
href={`/u/${encodeURIComponent(row.username)}`}
|
||||
style={{ fontWeight: 700 }}
|
||||
>
|
||||
<Link href={`/u/${encodeURIComponent(row.username)}`} style={{ fontWeight: 700 }}>
|
||||
{row.username}
|
||||
</Link>
|
||||
</td>
|
||||
<td style={{ textAlign: "right", fontWeight: 800 }}>
|
||||
{row.points.toLocaleString()}
|
||||
</td>
|
||||
<td style={{ textAlign: "right", fontWeight: 800 }}>{row.points.toLocaleString()}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
+16
-28
@@ -9,15 +9,7 @@ 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",
|
||||
];
|
||||
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
|
||||
@@ -55,9 +47,7 @@ export default async function RadioPage() {
|
||||
})
|
||||
.catch(() => []);
|
||||
|
||||
const todaysSlots = schedules.filter(
|
||||
(s) => s.day.toLowerCase() === today.toLowerCase(),
|
||||
);
|
||||
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
|
||||
@@ -86,11 +76,20 @@ export default async function RadioPage() {
|
||||
>
|
||||
<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" }}>
|
||||
<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>}
|
||||
{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")}
|
||||
@@ -98,12 +97,7 @@ export default async function RadioPage() {
|
||||
</div>
|
||||
<div className="card" style={{ display: "flex", alignItems: "center" }}>
|
||||
{streamUrl ? (
|
||||
<audio
|
||||
controls
|
||||
src={streamUrl}
|
||||
style={{ width: "100%" }}
|
||||
preload="none"
|
||||
>
|
||||
<audio controls src={streamUrl} style={{ width: "100%" }} preload="none">
|
||||
{t("audioUnsupported")}
|
||||
</audio>
|
||||
) : (
|
||||
@@ -116,11 +110,7 @@ export default async function RadioPage() {
|
||||
</ContentCard>
|
||||
|
||||
{/* ── Sub-page links ──────────────────────────────────────── */}
|
||||
<ContentCard
|
||||
icon="🎚️"
|
||||
title={t("exploreTitle")}
|
||||
subtitle={t("exploreSubtitle")}
|
||||
>
|
||||
<ContentCard icon="🎚️" title={t("exploreTitle")} subtitle={t("exploreSubtitle")}>
|
||||
<div className="card-grid sm-2 lg-3">
|
||||
{RADIO_LINKS.map((l) => (
|
||||
<Link
|
||||
@@ -171,9 +161,7 @@ export default async function RadioPage() {
|
||||
<td>
|
||||
{formatTime(s.startTime)} – {formatTime(s.endTime)}
|
||||
</td>
|
||||
<td>
|
||||
{s.showName || <span className="muted">{t("untitledShow")}</span>}
|
||||
</td>
|
||||
<td>{s.showName || <span className="muted">{t("untitledShow")}</span>}</td>
|
||||
<td>
|
||||
{djName ? (
|
||||
<Link href={`/u/${djName}`}>{djName}</Link>
|
||||
|
||||
@@ -39,18 +39,16 @@ export default async function RadioRequestsPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContentCard
|
||||
icon="🎵"
|
||||
title={genericT("linkRequests")}
|
||||
subtitle={genericT("linkRequestsText")}
|
||||
/>
|
||||
<ContentCard icon="🎵" title={genericT("linkRequests")} subtitle={genericT("linkRequestsText")} />
|
||||
|
||||
{isLoggedIn ? (
|
||||
<ContentCard icon="✍️" title={t("formTitle")} subtitle={t("formSubtitle")}>
|
||||
<form action={submitRequest}>
|
||||
<div className="card-grid sm-2">
|
||||
<div>
|
||||
<label htmlFor="songTitle" className="muted">{t("songLabel")}</label>
|
||||
<label htmlFor="songTitle" className="muted">
|
||||
{t("songLabel")}
|
||||
</label>
|
||||
<input
|
||||
id="songTitle"
|
||||
name="songTitle"
|
||||
@@ -62,7 +60,9 @@ export default async function RadioRequestsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="artist" className="muted">{t("artistLabel")}</label>
|
||||
<label htmlFor="artist" className="muted">
|
||||
{t("artistLabel")}
|
||||
</label>
|
||||
<input
|
||||
id="artist"
|
||||
name="artist"
|
||||
|
||||
@@ -6,15 +6,7 @@ export const revalidate = 300;
|
||||
|
||||
// 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;
|
||||
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).
|
||||
@@ -58,8 +50,7 @@ export default async function RadioSchedulePage() {
|
||||
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 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}`,
|
||||
@@ -77,11 +68,7 @@ export default async function RadioSchedulePage() {
|
||||
|
||||
return (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
<ContentCard
|
||||
icon="🗓️"
|
||||
title={t("title")}
|
||||
subtitle={t("subtitle")}
|
||||
/>
|
||||
<ContentCard icon="🗓️" title={t("title")} subtitle={t("subtitle")} />
|
||||
|
||||
<ContentCard padded={!hasAny}>
|
||||
{!hasAny ? (
|
||||
@@ -121,9 +108,7 @@ export default async function RadioSchedulePage() {
|
||||
<td>{slot.dj}</td>
|
||||
<td>
|
||||
{slot.showName || <span className="muted">—</span>}
|
||||
{slot.description ? (
|
||||
<div className="muted">{slot.description}</div>
|
||||
) : null}
|
||||
{slot.description ? <div className="muted">{slot.description}</div> : null}
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
@@ -22,10 +22,7 @@ export default async function RadioShoutsPage() {
|
||||
take: 50,
|
||||
})
|
||||
.catch(() => []),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
siteSettings.get("habbo_imaging_url", "https://www.habbo.com/habbo-imaging/avatarimage"),
|
||||
auth(),
|
||||
]);
|
||||
|
||||
@@ -45,11 +42,7 @@ export default async function RadioShoutsPage() {
|
||||
|
||||
return (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
<ContentCard
|
||||
icon="📣"
|
||||
title={t("title")}
|
||||
subtitle={t("subtitle")}
|
||||
/>
|
||||
<ContentCard icon="📣" title={t("title")} subtitle={t("subtitle")} />
|
||||
|
||||
{isLoggedIn ? (
|
||||
<ContentCard icon="✍️" title={t("postTitle")} subtitle={t("postSubtitle")}>
|
||||
|
||||
Reference in new issue
Block a user