feat: add realtime Discord leaderboards

This commit is contained in:
2026-07-25 00:06:52 +07:00 Unverified
parent 7a9e67e283
commit bb6740fcee
39 changed files with 1917 additions and 119 deletions
+122
View File
@@ -0,0 +1,122 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { ChannelSelect } from "@/components/leaderboard/channel-select";
import { LeaderboardBoard } from "@/components/leaderboard/leaderboard-board";
import { LeaderboardError } from "@/components/leaderboard/leaderboard-error";
import { LeaderboardShell } from "@/components/leaderboard/leaderboard-shell";
import { RealtimeRefresh } from "@/components/realtime-refresh";
import { getVoiceLeaderboardPage } from "@/lib/leaderboard/database";
import { getCurrentDiscordVoiceChannels } from "@/lib/leaderboard/discord";
import { normalizeRequestedPage } from "@/lib/leaderboard/ranking";
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
title: "อันดับเวลาในห้องเสียง",
description: "ดูอันดับเวลารวมในห้องเสียงและห้องเวที Discord ปัจจุบันของ Erika!",
alternates: {
canonical: "/leaderboard/vc",
},
};
type VoiceLeaderboardPageProps = {
searchParams: Promise<{
channel?: string | string[];
page?: string | string[];
}>;
};
function voiceLeaderboardUrl(page: number, channelId?: string) {
const params = new URLSearchParams();
if (channelId) params.set("channel", channelId);
if (page > 1) params.set("page", String(page));
const query = params.toString();
return query ? `/leaderboard/vc?${query}` : "/leaderboard/vc";
}
export default async function VoiceLeaderboardPage({
searchParams,
}: VoiceLeaderboardPageProps) {
const query = await searchParams;
const requestedPage = normalizeRequestedPage(query.page);
let channels;
try {
channels = await getCurrentDiscordVoiceChannels();
} catch (error) {
console.error("Failed to load Discord voice channels", error);
return (
<LeaderboardShell
active="vc"
title="อันดับเวลาในห้องเสียง"
description="รวมเวลาจากห้องเสียงและห้องเวทีที่ยังอยู่ใน Discord ตอนนี้ หรือเลือกดูเฉพาะห้องได้"
>
<LeaderboardError source="discord" />
</LeaderboardShell>
);
}
const requestedChannelId = typeof query.channel === "string"
? query.channel
: undefined;
const selectedChannelId = channels.some(
(channel) => channel.id === requestedChannelId,
)
? requestedChannelId
: undefined;
const invalidChannel = query.channel !== undefined && !selectedChannelId;
let data;
try {
data = await getVoiceLeaderboardPage(
channels.map((channel) => channel.id),
requestedPage,
selectedChannelId,
);
} catch (error) {
console.error("Failed to load voice leaderboard", error);
return (
<LeaderboardShell
active="vc"
title="อันดับเวลาในห้องเสียง"
description="รวมเวลาจากห้องเสียงและห้องเวทีที่ยังอยู่ใน Discord ตอนนี้ หรือเลือกดูเฉพาะห้องได้"
>
<ChannelSelect
channels={channels}
selectedChannelId={selectedChannelId}
/>
<LeaderboardError source="database" />
</LeaderboardShell>
);
}
if (
invalidChannel
|| (query.page !== undefined && query.page !== String(data.page))
) {
redirect(voiceLeaderboardUrl(data.page, selectedChannelId));
}
return (
<LeaderboardShell
active="vc"
title="อันดับเวลาในห้องเสียง"
description="รวมเวลาจากห้องเสียงและห้องเวทีที่ยังอยู่ใน Discord ตอนนี้ หรือเลือกดูเฉพาะห้องได้"
>
<RealtimeRefresh
topic="leaderboards"
leaderboard="vc"
channelId={selectedChannelId}
/>
<ChannelSelect
channels={channels}
selectedChannelId={selectedChannelId}
/>
<LeaderboardBoard
kind="vc"
data={data}
channelId={selectedChannelId}
/>
</LeaderboardShell>
);
}