From d0cd0cba2891a47eba97d8f7ab2eee10278c4456 Mon Sep 17 00:00:00 2001 From: Petr Vecera Date: Fri, 14 Jun 2024 15:53:00 +0100 Subject: [PATCH] Add history to the top 10 page (#499) --- .../leaderboards/top-leaderboards-table.tsx | 20 +++++- config.ts | 1 + src/apis/coh3stats-api.ts | 70 +++++++++++++++++++ src/leaderboards/top-leaderboards.ts | 27 ++++++- 4 files changed, 115 insertions(+), 3 deletions(-) diff --git a/components/leaderboards/top-leaderboards-table.tsx b/components/leaderboards/top-leaderboards-table.tsx index 271bfe2f..0effa17e 100644 --- a/components/leaderboards/top-leaderboards-table.tsx +++ b/components/leaderboards/top-leaderboards-table.tsx @@ -34,6 +34,24 @@ const TopLeaderboardsTable = ({ leaderBoardData, loading, error }: Props) => { accessor: "rank", textAlignment: "center", }, + { + title: "RC", + accessor: "change", + textAlignment: "center", + render: ({ change }: { change: number | string }) => { + if (typeof change === "string") { + return change; + } else { + return change > 0 ? ( + +{change} + ) : change < 0 ? ( + {change} + ) : ( + <> + ); + } + }, + }, { title: "ELO", accessor: "rating", @@ -118,7 +136,7 @@ const TopLeaderboardsTable = ({ leaderBoardData, loading, error }: Props) => { accessor: "lastmatchdate", title: "Last Game", textAlignment: "right", - width: 125, + width: 110, // @ts-ignore render: ({ lastmatchdate }) => { return ; diff --git a/config.ts b/config.ts index 6410c6e9..0f7408f5 100644 --- a/config.ts +++ b/config.ts @@ -301,6 +301,7 @@ const config = { DONATION_LINK: "https://ko-fi.com/cohstats", GITHUB_LINK: repository.url, CDN_ASSETS_HOSTING: "https://cdn.coh3stats.com", + STORAGE_LINK: "https://storage.coh3stats.com", BASE_CLOUD_FUNCTIONS_URL: useFirebaseEmulators ? firebaseFunctions.EMULATORS_URL : firebaseFunctions.CLOUD_URL, diff --git a/src/apis/coh3stats-api.ts b/src/apis/coh3stats-api.ts index 82281e6c..f1657a15 100644 --- a/src/apis/coh3stats-api.ts +++ b/src/apis/coh3stats-api.ts @@ -1,7 +1,9 @@ import config from "../../config"; import { GlobalAchievementsData, + leaderBoardType, ProcessedMatch, + raceType, TwitchStream, YouTubeVideo, } from "../coh3/coh3-types"; @@ -293,6 +295,73 @@ const triggerPlayerNemesisAliasesUpdate = async (playerID: string | number) => { return await response.json(); }; +const _fetchFirst10LinesOfLeaderBoards = async (url: string) => { + const response = await fetch(url); + const reader = response.body?.getReader(); + const decoder = new TextDecoder("utf-8"); + + let buffer = ""; + let lines: string[] = []; + + while (lines.length < 10 && reader) { + const { value, done } = await reader.read(); + if (done) break; + + // Decode and append to buffer + buffer += decoder.decode(value || new Uint8Array(), { stream: true }); + + // Split the buffer into lines + const splitBuffer = buffer.split("},"); + + // Save the last part of the current buffer for the next iteration + buffer = splitBuffer.pop() || ""; + + // Add the complete lines to our lines array + lines.push(...splitBuffer); + + // If we have more than 10 lines, truncate the array + if (lines.length >= 10) { + lines = lines.slice(0, 10); + break; + } + } + + // Remove the starts of the file + lines[0] = lines[0].replace('{"leaderboards":[', ""); + + return lines.reduce((acc: Record, line) => { + // Add the missing closing bracket and parse the JSON + const obj = JSON.parse(line + "}"); + + // Use the statgroup_id as the key + acc[obj.statgroup_id] = obj; + + return acc; + }, {}); +}; + +/** + * Fetches only top 10 items for now + * https://storage.coh3stats.com/leaderboards/1718064000/1718064000_1v1_american.json + * @param timeStamp + * @param type + * @param race + */ +const getOldLeaderboardData = async ( + timeStamp: string | number, + type: leaderBoardType, + race: raceType, +) => { + const url = `${config.STORAGE_LINK}/leaderboards/${timeStamp}/${timeStamp}_${type}_${race}.json`; + + try { + return _fetchFirst10LinesOfLeaderBoards(url); + } catch (e) { + console.error(e); + return {}; + } +}; + export { getPlayerCardInfo, getPlayerRecentMatches, @@ -304,4 +373,5 @@ export { getPlayersCardsConfigsHttp, getYouTubeVideosHttp, triggerPlayerNemesisAliasesUpdate, + getOldLeaderboardData, }; diff --git a/src/leaderboards/top-leaderboards.ts b/src/leaderboards/top-leaderboards.ts index f855926a..51267ef5 100644 --- a/src/leaderboards/top-leaderboards.ts +++ b/src/leaderboards/top-leaderboards.ts @@ -1,10 +1,33 @@ import { getLeaderBoardData } from "../coh3/coh3-api"; import { findAndMergeStatGroups } from "../coh3/helpers"; import { raceType, Top1v1LeaderboardsData } from "../coh3/coh3-types"; +import { getOldLeaderboardData } from "../apis/coh3stats-api"; const getTop1v1LeaderBoards = async (race: raceType): Promise => { - const leaderBoardDataRaw = await getLeaderBoardData(race, "1v1", 1, 10, 1, "steam"); - const leaderBoardData = findAndMergeStatGroups(leaderBoardDataRaw, null); + const date = new Date(); + const yesterdayTimeStamp = + Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() - 1, 0, 0, 0) / 1000; + + const promisedLeaderBoardDataRaw = getLeaderBoardData(race, "1v1", 1, 10, 1, "steam"); + const promisedOldData = getOldLeaderboardData(yesterdayTimeStamp, "1v1", race); + + // Old data is already well formatted + const [leaderBoardDataRaw, oldData] = await Promise.all([ + promisedLeaderBoardDataRaw, + promisedOldData, + ]); + // We need to convert the RAW data + let leaderBoardData = findAndMergeStatGroups(leaderBoardDataRaw, null); + + // Add the change there + leaderBoardData = leaderBoardData.map((value) => { + if (oldData[value.statgroup_id]) { + value.change = value.rank - oldData[value.statgroup_id].rank; + } else { + value.change = "new"; + } + return value; + }); return { race: race,