Files
erika/components/leaderboard/leaderboard-board.tsx
T

195 lines
6.3 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 { Trophy } from "lucide-react";
import { LeaderboardPagination } from "@/components/leaderboard/leaderboard-pagination";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
formatInteger,
formatVoiceDuration,
resolveLeaderboardName,
shortenDiscordId,
type LeaderboardPage,
type VoiceLeaderboardRow,
type XpLeaderboardRow,
} from "@/lib/leaderboard/ranking";
type LeaderboardBoardProps =
| {
kind: "xp";
data: LeaderboardPage<XpLeaderboardRow>;
searchQuery: string;
channelId?: never;
}
| {
kind: "vc";
data: LeaderboardPage<VoiceLeaderboardRow>;
searchQuery: string;
channelId?: string;
};
function rankVariant(rank: number) {
if (rank === 1) return "default";
if (rank === 2) return "secondary";
return "outline";
}
function Identity({
displayName,
username,
userId,
}: {
displayName: string | null;
username: string | null;
userId: string;
}) {
const unresolved = !displayName?.trim() && !username?.trim();
return (
<div className="flex min-w-40 flex-col gap-0.5">
<span className="font-medium">
{resolveLeaderboardName({ displayName, username, userId })}
</span>
<span className="text-xs text-muted-foreground">
{unresolved
? `Discord ID: ${shortenDiscordId(userId)}`
: displayName?.trim() && username?.trim()
? `@${username}`
: `Discord ID: ${shortenDiscordId(userId)}`}
</span>
</div>
);
}
function Podium({ data }: LeaderboardBoardProps) {
if (data.podium.length === 0) return null;
return (
<section aria-labelledby="podium-title" className="flex flex-col gap-3">
<h2 id="podium-title" className="text-lg font-medium">สามอันดับนำ</h2>
<div className="grid gap-3 md:grid-cols-3">
{data.podium.map((row) => (
<Card
key={row.userId}
size="sm"
className={row.rank === 1 ? "md:-translate-y-1" : undefined}
>
<CardHeader>
<CardTitle className="flex items-center justify-between gap-3">
<span className="truncate">
{resolveLeaderboardName(row)}
</span>
<Badge variant={rankVariant(row.rank)}>
{row.rank === 1 && <Trophy data-icon="inline-start" />}
#{row.rank}
</Badge>
</CardTitle>
<CardDescription>
{"level" in row
? `เลเวล ${row.level}`
: "เวลารวมในห้องเสียง"}
</CardDescription>
</CardHeader>
<CardContent className="text-xl font-semibold tabular-nums">
{"totalXp" in row
? `${formatInteger(row.totalXp)} XP`
: formatVoiceDuration(row.totalSeconds)}
</CardContent>
</Card>
))}
</div>
</section>
);
}
export function LeaderboardBoard(props: LeaderboardBoardProps) {
const { kind, data, channelId, searchQuery } = props;
const firstRow = data.totalRows === 0
? 0
: (data.page - 1) * data.pageSize + 1;
const lastRow = Math.min(data.page * data.pageSize, data.totalRows);
return (
<>
<Podium {...props} />
<Card>
<CardHeader>
<CardTitle>
{kind === "xp" ? "อันดับ XP ทั้งหมด" : "อันดับเวลาในห้องเสียง"}
</CardTitle>
<CardDescription>
{data.totalRows > 0
? `แสดง ${firstRow}–${lastRow} จาก ${formatInteger(String(data.totalRows))} คน`
: searchQuery
? "ไม่พบสมาชิกที่ตรงกับคำค้นหานี้"
: "ยังไม่มีข้อมูลในขอบเขตนี้"}
</CardDescription>
</CardHeader>
<CardContent className="px-0">
<Table>
<TableCaption>
{kind === "xp"
? "ตารางอันดับจาก XP รวมของสมาชิกเลเวล 1 ขึ้นไป"
: "ตารางอันดับจากเวลารวมในห้องเสียงและห้องเวทีปัจจุบัน"}
</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="pl-6">อันดับ</TableHead>
<TableHead>สมาชิก</TableHead>
{kind === "xp" && <TableHead>เลเวล</TableHead>}
<TableHead className="pr-6 text-right">
{kind === "xp" ? "XP รวม" : "เวลารวม"}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.rows.map((row) => (
<TableRow key={row.userId}>
<TableCell className="pl-6">
<Badge variant={rankVariant(row.rank)}>#{row.rank}</Badge>
</TableCell>
<TableCell>
<Identity {...row} />
</TableCell>
{"level" in row && (
<TableCell className="tabular-nums">{row.level}</TableCell>
)}
<TableCell className="pr-6 text-right font-medium tabular-nums">
{"totalXp" in row
? formatInteger(row.totalXp)
: formatVoiceDuration(row.totalSeconds)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
<CardFooter>
<LeaderboardPagination
path={`/leaderboard/${kind}`}
page={data.page}
totalPages={data.totalPages}
channelId={channelId}
searchQuery={searchQuery}
/>
</CardFooter>
</Card>
</>
);
}