- Fix proxy routes to use resolveImagerBase instead of removed resolveUpstreamBase - Fix avatarImageUrl type signature to use AvatarOptions - Run biome formatter
99 lines
2.5 KiB
TypeScript
99 lines
2.5 KiB
TypeScript
import { sql } from "drizzle-orm";
|
|
import Link from "next/link";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
|
|
import { db } from "@/lib/db";
|
|
import { avatarImageUrl } from "@/lib/format";
|
|
|
|
type LeaderRow = {
|
|
username: string;
|
|
look: string;
|
|
points: number | bigint;
|
|
};
|
|
|
|
async function loadRows(): Promise<
|
|
{ username: string; look: string; points: number }[]
|
|
> {
|
|
try {
|
|
const [rows] = (await db.execute(sql`
|
|
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
|
|
`)) as unknown as [LeaderRow[], unknown];
|
|
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 = await loadRows();
|
|
|
|
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(row.look, {
|
|
size: "s",
|
|
headOnly: true,
|
|
});
|
|
return (
|
|
<tr key={row.username}>
|
|
<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>
|
|
);
|
|
}
|