feat: implement real-time leaderboard system with XP and voice channel tracking features
This commit is contained in:
@@ -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)}`
|
||||
|
||||
Reference in New Issue
Block a user