Decouple admin from public max-w-7xl via (site) route group.
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:
SimoandCursor committed 2026-07-15 21:12:52 +02:00
1 parent a1dc7ff0a7
commit 367f69a439
60 files changed
+29 -30

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
import { avatarImageUrl } from "@/lib/format";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300;
// The radio_listener_points table in the live AtomCMS DB carries user_id +
// points columns, but the generated Prisma model is a stub (id/timestamps only),
// so it has no `points`/`userId` delegate fields. We therefore read it via
// $queryRaw, joining users for the username/look — the same approach used for
// the keyless/@@ignore'd emulator tables (see src/app/admin/logs). MySQL may
// return BigInt columns as bigint, so values are coerced defensively.
type LeaderRow = {
username: string;
look: string;
points: number | bigint;
};
async function loadRows(): Promise<
{ username: string; look: string; points: number }[]
> {
try {
const rows = await prisma.$queryRaw<LeaderRow[]>`
SELECT u.username AS username, u.look AS look, p.points AS points
FROM radio_listener_points p
INNER JOIN users u ON u.id = p.user_id
ORDER BY p.points DESC
LIMIT 50
`;
return rows.map((r) => ({
username: r.username,
look: r.look,
points: Number(r.points),
}));
} catch {
return [];
}
}
export default async function RadioLeaderboardPage() {
const t = await getTranslations("pages.radioLeaderboard");
const [rows, imagerBase] = await Promise.all([
loadRows(),
siteSettings.get(
"habbo_imaging_url",
"https://www.habbo.com/habbo-imaging/avatarimage",
),
]);
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard icon="🏆" title={t("title")} subtitle={t("subtitle")} />
<ContentCard padded={rows.length === 0}>
{rows.length === 0 ? (
<EmptyState icon="🏆">{t("empty")}</EmptyState>
) : (
<table>
<thead>
<tr>
<th style={{ width: "3.5rem" }}>{t("colRank")}</th>
<th style={{ width: "3.5rem" }}>{t("colAvatar")}</th>
<th>{t("colListener")}</th>
<th style={{ textAlign: "right" }}>{t("colPoints")}</th>
</tr>
</thead>
<tbody>
{rows.map((row, i) => {
const avatar = avatarImageUrl(imagerBase ?? "", row.look, {
size: "s",
headOnly: true,
});
return (
<tr key={`${row.username}-${i}`}>
<td>
<RankBadge position={i + 1} />
</td>
<td>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="avatar"
src={avatar}
alt={`${row.username} avatar`}
width={40}
height={40}
/>
</td>
<td>
<Link
href={`/u/${encodeURIComponent(row.username)}`}
style={{ fontWeight: 700 }}
>
{row.username}
</Link>
</td>
<td style={{ textAlign: "right", fontWeight: 800 }}>
{row.points.toLocaleString()}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</ContentCard>
</main>
);
}