feat: implement real-time leaderboard system with XP and voice channel tracking features

This commit is contained in:
2026-07-25 21:26:53 +07:00 Unverified
parent bb6740fcee
commit 3f4b670170
18 changed files with 531 additions and 178 deletions
+31 -3
View File
@@ -66,9 +66,23 @@ function compareIdentities(a: LeaderboardIdentity, b: LeaderboardIdentity) {
return byName || a.userId.localeCompare(b.userId);
}
function normalizeSearchQuery(value: string | undefined) {
return value?.trim().toLocaleLowerCase("th-TH") ?? "";
}
function matchesSearch(identity: LeaderboardIdentity, searchQuery: string) {
if (!searchQuery) return true;
return [
identity.displayName,
identity.username,
identity.userId,
].some((value) => value?.toLocaleLowerCase("th-TH").includes(searchQuery));
}
function rankAndPaginate<T extends LeaderboardIdentity & { score: string }>(
sourceRows: T[],
requestedPage: number,
searchQuery?: string,
): LeaderboardPage<T & { rank: number }> {
const sorted = [...sourceRows].sort((a, b) => {
const scoreA = asBigInt(a.score);
@@ -87,14 +101,19 @@ function rankAndPaginate<T extends LeaderboardIdentity & { score: string }>(
return { ...row, rank: sharedRank };
});
const totalRows = ranked.length;
const normalizedSearchQuery = normalizeSearchQuery(searchQuery);
const filtered = normalizedSearchQuery
? ranked.filter((row) => matchesSearch(row, normalizedSearchQuery))
: ranked;
const totalRows = filtered.length;
const totalPages = Math.max(1, Math.ceil(totalRows / LEADERBOARD_PAGE_SIZE));
const page = Math.min(Math.max(1, requestedPage), totalPages);
const start = (page - 1) * LEADERBOARD_PAGE_SIZE;
return {
rows: ranked.slice(start, start + LEADERBOARD_PAGE_SIZE),
podium: ranked.slice(0, 3),
rows: filtered.slice(start, start + LEADERBOARD_PAGE_SIZE),
podium: filtered.slice(0, 3),
page,
pageSize: LEADERBOARD_PAGE_SIZE,
totalRows,
@@ -105,6 +124,7 @@ function rankAndPaginate<T extends LeaderboardIdentity & { score: string }>(
export function buildXpLeaderboard(
rows: XpSourceRow[],
requestedPage: number,
searchQuery?: string,
): LeaderboardPage<XpLeaderboardRow> {
return rankAndPaginate(
rows
@@ -115,6 +135,7 @@ export function buildXpLeaderboard(
score: row.totalXp,
})),
requestedPage,
searchQuery,
);
}
@@ -123,6 +144,7 @@ export function buildVoiceLeaderboard(
currentChannelIds: string[],
requestedPage: number,
selectedChannelId?: string,
searchQuery?: string,
): LeaderboardPage<VoiceLeaderboardRow> {
const allowedChannels = new Set(
selectedChannelId ? [selectedChannelId] : currentChannelIds,
@@ -152,6 +174,7 @@ export function buildVoiceLeaderboard(
score: row.totalSeconds.toString(),
})),
requestedPage,
searchQuery,
);
}
@@ -161,6 +184,11 @@ export function normalizeRequestedPage(value: string | string[] | undefined) {
return Number.isSafeInteger(page) ? page : 1;
}
export function normalizeLeaderboardSearch(value: string | string[] | undefined) {
if (typeof value !== "string") return "";
return value.trim().slice(0, 80);
}
export function shortenDiscordId(userId: string) {
return userId.length > 10
? `${userId.slice(0, 5)}…${userId.slice(-4)}`