"use client";
import React from "react";
import "../../../../App.css";
import { memo } from "react";
import { notFound } from "next/navigation";
import { useRouter, useParams } from "next/navigation";
import Image from "next/image";
import { createRoot } from "react-dom/client";
import {
getPUUID,
getSummonerStats,
getExtendedMatchList,
matchInfoListDriver,
matchListUpdated,
} from "@/src/App.jsx";
import { useData } from "@/src/context/dataContext";
import { apiCall, apiImageCall } from "@/src/utils/apiService";
import useSummonerStore from "@/src/utils/storeService";
import MatchEntry from "@/src/components/MatchEntry";
import ChampionEntryList from "@/src/components/ChampionEntryList";
import ErrorPage from "@/src/components/ErrorPage.jsx";
import { useEffect, useRef, useState } from "react";
import GridLoader from "react-spinners/GridLoader";
import Button from "@mui/material/Button";
import LoadingButton from "@mui/lab/LoadingButton";
import ButtonGroup from "@mui/material/ButtonGroup";
import Chip from "@mui/material/Chip";
import { LineChart } from "@mui/x-charts";
import { CircularProgressbar, buildStyles } from "react-circular-progressbar";
import CircularProgress from "@mui/material/CircularProgress";
import "react-circular-progressbar/dist/styles.css";
import Header from "@/src/components/Header";
const serverOptions = [
{ value: "EUW1", label: "EUW", region: "europe" },
{ value: "EUN1", label: "EUNE", region: "europe" },
{ value: "NA1", label: "NA", region: "americas" },
{ value: "KR", label: "KR", region: "asia" },
{ value: "JP1", label: "JP", region: "asia" },
{ value: "BR1", label: "BR", region: "americas" },
{ value: "LA1", label: "LAN", region: "americas" },
{ value: "LA2", label: "LAS", region: "americas" },
{ value: "OC1", label: "OC", region: "sea" },
{ value: "TR1", label: "TR", region: "europe" },
{ value: "RU", label: "RU", region: "europe" },
{ value: "PH2", label: "PH", region: "asia" },
{ value: "SG2", label: "SG", region: "sea" },
{ value: "TH2", label: "TH", region: "asia" },
{ value: "TW2", label: "TW", region: "sea" },
{ value: "VN2", label: "VN", region: "asia" },
];
const serverDictionary = serverOptions.reduce((acc, option) => {
acc[option.label] = option.value;
return acc;
}, {});
const spinnerStyles = {
position: "absolute",
top: "40%",
left: "50%",
transform: "translateX(-50%)",
};
var gameQueues;
var ownUsername;
/**
* @module Dashboard
*/
function Dashboard() {
const router = useRouter();
const hasFetched = useRef(false);
const summonerData = useSummonerStore((state) => state.summonerData);
const setSummonerData = useSummonerStore((state) => state.setSummonerData);
const storedData = useSummonerStore.getState().summonerData;
var storedTransformedData = null;
if (storedData) {
storedTransformedData = {
...storedData,
masteryInfo: new Map(storedData.masteryInfo),
champions: new Map(storedData.champions),
};
}
const { data } = useData();
const { server, summonerName } = useParams();
const region = serverOptions.find(
(option) => server === option.label
)?.region;
const [isLoading, setIsLoading] = useState(true);
const [isTempLoading, setIsTempLoading] = useState(false);
const [error, setError] = useState(null);
const [isVisible, setIsVisible] = useState(false);
const [gameName, setGameName] = useState("");
const [tagLine, setTagLine] = useState("");
const [puuid, setPuuid] = useState(null);
const [summonerInfo, setSummonerInfo] = useState(null);
const [summonerRankedInfo, setSummonerRankedInfo] = useState(null);
const [summonerMatchInfo, setSummonerMatchInfo] = useState(null);
const [summonerWinrateInfo, setSummonerWinrateInfo] = useState(null);
const [summonerChampionWinrateInfo, setSummonerChampionWinrateInfo] =
useState(null);
const [championsInfo, setChampions] = useState(null);
const [graphData, setgraphData] = useState(null);
if (!serverOptions.find((option) => option.label === server)) {
setError(`Invalid server "${server}"`);
}
useEffect(() => {
const fetchData = async () => {
if (hasFetched.current) return;
hasFetched.current = true;
try {
gameQueues = await getGameQueues();
setIsLoading(true);
var result;
const newGameName = summonerName.split("-")[0].trim();
const fetchedTagLine = summonerName.split("-")[1].trim();
const fetchedPUUID = await getPUUID(fetchedTagLine, newGameName);
const noDataAvailable = !data && !storedTransformedData;
const matchListNotUpdated = !(await matchListUpdated(
region,
fetchedPUUID
));
const gameNameMismatch =
storedTransformedData?.gameName !== summonerName;
if (noDataAvailable || matchListNotUpdated || gameNameMismatch) {
try {
result = await getSummonerStats(
summonerName.split("-")[1],
newGameName,
serverDictionary[server],
region
);
} catch (error) {
setError(`Cannot find ${summonerName}`);
}
} else {
const resultData = data || storedTransformedData;
result = {
puuid: resultData.puuid,
tagLine: resultData.tagLine,
summonerInfo: resultData.summonerInfo,
rankedInfo: resultData.rankedInfo,
matchInfoList: resultData.matchInfoList,
summonerWinrate: resultData.summonerWinrate,
masteryInfo: resultData.masteryInfo,
champions: resultData.champions,
};
}
//When storing maps in local storage they dont get stored properly
const transformedResult = {
...result,
gameName: summonerName,
masteryInfo: Array.from(result.masteryInfo.entries()),
champions: Array.from(result.champions.entries()),
};
setSummonerData(transformedResult);
setGameName(newGameName);
setTagLine(result.tagLine);
setPuuid(result.puuid);
setSummonerInfo(result.summonerInfo);
setSummonerRankedInfo(result.rankedInfo);
setSummonerMatchInfo(result.matchInfoList);
setSummonerWinrateInfo(result.summonerWinrate);
setSummonerChampionWinrateInfo(result.masteryInfo);
setChampions(result.champions);
setIsLoading(false);
getGraphDates(result.matchInfoList, setgraphData);
} catch (error) {
console.error("Error fetching data:", error);
router.push("/");
}
};
const handleScroll = () => {
if (window.scrollY > 100) {
setIsVisible(true);
} else {
setIsVisible(false);
}
};
fetchData();
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [
data,
region,
router,
setSummonerData,
summonerData,
summonerName,
server,
]);
useEffect(() => {
if (!isLoading) {
makeSummonerProfile(
summonerInfo,
summonerRankedInfo,
summonerMatchInfo,
summonerChampionWinrateInfo,
championsInfo
);
makeMatchHistory(summonerMatchInfo, setIsTempLoading);
document.documentElement.style.overflowY = "scroll";
document.getElementById("footer").style.position = "relative";
document.getElementById("homeBody").style.animation =
"fade-in 1s forwards";
}
}, [
summonerInfo,
summonerRankedInfo,
summonerWinrateInfo,
summonerChampionWinrateInfo,
isLoading,
]);
if (error) {
return notFound();
}
ownUsername = gameName;
if (isLoading) {
return (
<GridLoader
color={"#9b792f"}
loading={isLoading}
cssOverride={spinnerStyles}
margin={6}
size={26}
speedMultiplier={0.8}
aria-label="Loading Spinner"
data-testid="loader"
/>
);
}
return (
<>
<Header />
<div className="dashboard">
<div id="homeBody">
<div id="summonerContainer">
<div id="winrateBlockContainer">
<div id="winrateBlock">
<ButtonGroup
variant="outlined"
sx={{
".MuiButtonGroup-grouped": {
"&:hover": {
color: "#C89B3C",
backgroundColor: "#262c33",
borderColor: "#C89B3C",
},
color: "#A09B8C",
backgroundColor: "262c33",
borderColor: "#C89B3C",
},
}}
size="Large"
aria-label="Basic button group"
fullWidth
>
{/* <Button
onClick={() => {
loadWinrate(
summonerRankedInfo[1],
summonerWinrateInfo.normalWinrate
);
}}
>
Normal
</Button> */}
<Button
onClick={() => {
loadWinrate(
summonerRankedInfo[1],
summonerWinrateInfo.rankedSoloWinrate
);
}}
>
Solo/Duo
</Button>
<Button
onClick={() => {
loadWinrate(
summonerRankedInfo[0],
summonerWinrateInfo.rankedFlexWinrate
);
}}
>
Flex
</Button>
</ButtonGroup>
<div className="winrateContainer">
<div id="games">
<CircularProgressbar
strokeWidth={5}
value={
summonerRankedInfo[1].rankedGames != undefined
? summonerRankedInfo[1].rankedGames
: 0
}
maxValue={1}
text={
summonerRankedInfo[1].rankedGames != undefined
? summonerRankedInfo[1].rankedGames
: "0"
}
styles={buildStyles({
strokeLinecap: "butt",
strokeDashoffset:
summonerRankedInfo[1].rankedGames > 0 ? 0 : 298.451,
textSize: "18px",
pathTransitionDuration: 0.4,
pathColor: `rgb(197 134 0)`,
textColor: "#E3E4E4",
trailColor: "#65645E",
})}
/>
Games Played
</div>
<div id="winrate">
<CircularProgressbar
strokeWidth={5}
value={
!isNaN(summonerWinrateInfo.rankedSoloWinrate)
? summonerWinrateInfo.rankedSoloWinrate
: 0
}
text={
summonerWinrateInfo.rankedSoloWinrate
? `${summonerWinrateInfo.rankedSoloWinrate}%`
: `0%`
}
styles={buildStyles({
strokeLinecap: "butt",
textSize: "18px",
pathTransitionDuration: 0.4,
pathColor: `rgb(197 134 0)`,
textColor: "#E3E4E4",
trailColor: "#65645E",
})}
/>
Winrate
</div>
</div>
</div>
<div className="winrateGraph">
<LineChart
xAxis={[
{
data: graphData.map((item) => item.date),
scaleType: "band",
},
]}
series={[
{
data: graphData.map((item) => item.value),
color: "rgb(197, 134, 0)",
label: "Net Win Count",
valueFormatter: (v, { dataIndex }) => {
if (dataIndex >= 0 && dataIndex < graphData.length) {
const { date, label } = graphData[dataIndex];
return label;
}
return "";
},
},
]}
width={450}
height={320}
grid={{ vertical: true, horizontal: true }}
slotProps={{
popper: {
sx: {
"& .MuiChartsTooltip-paper": {
backgroundColor: "#1b1f24 !important",
border: "1px solid #C89B3C !important",
},
"& .MuiChartsTooltip-cell": {
color: "#C89B3C !important",
},
"& .MuiChartsTooltip-mark": {
border: "0px !important",
},
"& .MuiChartsTooltip-table thead tr td": {
color: "#C89B3C !important",
},
"& .MuiChartsTooltip-table tbody tr td": {
borderTop: "1px solid #383838 !important",
},
},
},
}}
sx={{
"& .MuiMarkElement-root": {
// Marks in the grid
fill: "rgb(27, 31, 36) !important",
},
"& .MuiChartsAxisHighlight-root": {
// Inside of the mark color
stroke: "rgb(101, 100, 94) !important",
},
"& .MuiChartsAxis-line": {
// Axis lines color
stroke: "rgb(101, 100, 94) !important",
},
"& .MuiChartsGrid-line": {
// Grid lines color
stroke: "rgb(48, 48, 48) !important",
},
"& .MuiChartsAxis-tick": {
// Little line next to the marks
stroke: "rgb(101, 100, 94) !important",
},
"& .MuiChartsAxis-tickLabel": {
// Text on axis labels
fill: "rgb(101, 100, 94) !important",
},
"& .MuiChartsLegend-root text": {
// Text on axis labels
fill: "rgb(101, 100, 94) !important",
},
}}
/>
</div>
</div>
<div id="summonerBlock">
<div className="profileGroup">
<div id="profileBg"> </div>
<div id="profileIconGroupContainer"></div>
<div id="name">
<div id="gameName"> {decodeURIComponent(gameName)} </div>
<div id="tagLine"> #{decodeURIComponent(tagLine)} </div>
<div
id="iconContainer"
onClick={() => {
copyToClipBoard(gameName, tagLine);
}}
onMouseEnter={resetCopyButton}
>
<span id="copyToClipboardIconText" className="tooltip-text">
Copy to clipboard
</span>
<svg
id="copyToClipboardIcon"
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 -1 21 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m6 18h-3c-.48 0-1-.379-1-1v-14c0-.481.38-1 1-1h14c.621 0 1 .522 1 1v3h3c.621 0 1 .522
1 1v14c0 .621-.522 1-1 1h-14c-.48
0-1-.379-1-1zm1.5-10.5v13h13v-13zm9-1.5v-2.5h-13v13h2.5v-9.5c0-.481.38-1 1-1z"
fillRule="nonzero"
/>
</svg>
</div>
</div>
<div className="summonerChips"></div>
</div>
{(summonerRankedInfo[0] !== "Unranked" ||
summonerRankedInfo[1] !== "Unranked") && (
<div id="lineDivider"></div>
)}
<div className="rankedInfo">
{summonerRankedInfo[1] !== "Unranked" && (
<div id="rankedSolo">
<div className="rankedRank">
<div>{`${summonerRankedInfo[1].rankedTier} ${summonerRankedInfo[1].rankedDivision} --`}</div>
<div>{`${summonerRankedInfo[1].rankedPoints} LP`}</div>
</div>
<div className="rankedTitle">Ranked Solo/Duo</div>
<div className="rankedWinrate">
<div>{`${summonerWinrateInfo.rankedSoloWinrate}% Winrate`}</div>
<div>{`(${summonerRankedInfo[1].rankedWins}W ${summonerRankedInfo[1].rankedLosses}L)`}</div>
</div>
</div>
)}
{summonerRankedInfo[0] !== "Unranked" && (
<div id="rankedFlex">
<div className="rankedRank">
<div>{`${summonerRankedInfo[0].rankedTier} ${summonerRankedInfo[0].rankedDivision} --`}</div>
<div>{`${summonerRankedInfo[0].rankedPoints} LP`}</div>
</div>
<div className="rankedTitle">Ranked Flex</div>
<div className="rankedWinrate">
<div>{`${summonerWinrateInfo.rankedFlexWinrate}% Winrate`}</div>
<div>{`(${summonerRankedInfo[0].rankedWins}W ${summonerRankedInfo[0].rankedLosses}L)`}</div>
</div>
</div>
)}
</div>
</div>
<div id="championBlock">
<ButtonGroup
id="championButtonGroup"
variant="outlined"
sx={{
".MuiButtonGroup-grouped": {
"&:hover": {
color: "#C89B3C",
backgroundColor: "#262c33",
borderColor: "#C89B3C",
},
color: "#A09B8C",
backgroundColor: "262c33",
borderColor: "#C89B3C",
},
}}
size="Large"
aria-label="Basic button group"
fullWidth
>
{/* <Button
onClick={() => {
makeChampionWinrate(
summonerChampionWinrateInfo,
championsInfo,
490
);
}}
>
Normal
</Button> */}
<Button
onClick={() => {
makeChampionWinrate(
summonerChampionWinrateInfo,
championsInfo,
420
);
}}
>
Solo/Duo
</Button>
<Button
onClick={() => {
makeChampionWinrate(
summonerChampionWinrateInfo,
championsInfo,
440
);
}}
>
Flex
</Button>
</ButtonGroup>
<div id="statNames">
<div>Name</div>
<div>Winrate</div>
<div>Games</div>
</div>
<ChampionEntryList
summonerChampionWinrateInfo={summonerChampionWinrateInfo}
championsInfo={championsInfo}
queueId={420}
></ChampionEntryList>
</div>
</div>
<div id="matchHistoryBlock">
<div id="matchHistoryHeader"> MATCH HISTORY </div>
<div id="matchList" />
<ButtonGroup
id="loadingButtonGroup"
variant="outlined"
sx={{
".MuiButtonGroup-grouped": {
"&:hover": {
color: "#C89B3C",
backgroundColor: "#262c33",
borderColor: "#C89B3C",
},
color: "#A09B8C",
backgroundColor: "262c33",
borderColor: "#C89B3C",
},
}}
size="Large"
aria-label="Basic button group"
width="300px"
>
<LoadingButton
id="loadingButton"
loading={isTempLoading}
loadingIndicator={
<CircularProgress sx={{ color: "#d8a841" }} size={35} />
}
loadingPosition="center"
onClick={() => {
extendMatchHistory(
summonerMatchInfo,
region,
puuid,
setSummonerMatchInfo,
setIsTempLoading
);
}}
>
Load More
</LoadingButton>
</ButtonGroup>
</div>
</div>
{isVisible ? (
<div
onClick={() =>
window.scrollTo({
top: 0,
behavior: "smooth",
})
}
>
<svg
id="scrollUpIcon"
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m2.019 11.993c0 5.518 4.48 9.998 9.998 9.998 5.517 0 9.997-4.48 9.997-9.998s-4.48-9.998-9.997-9.998c-5.518 0-9.998 4.48-9.998
9.998zm1.5 0c0-4.69 3.808-8.498 8.498-8.498s8.497 3.808 8.497 8.498-3.807 8.498-8.497 8.498-8.498-3.808-8.498-8.498zm4.715-1.528s1.505-1.502
3.259-3.255c.147-.146.338-.219.53-.219s.384.073.53.219c1.754 1.753 3.259 3.254 3.259 3.254.145.145.217.336.216.527 0
.191-.074.383-.22.53-.293.293-.766.294-1.057.004l-1.978-1.978v6.694c0
.413-.336.75-.75.75s-.75-.337-.75-.75v-6.694l-1.978 1.979c-.29.289-.762.286-1.055-.007-.147-.146-.22-.338-.221-.53-.001-.19.071-.38.215-.524z"
/>
</svg>{" "}
</div>
) : null}
</div>
</>
);
}
/**
* API call to retrieve queue ids and their
* respective names and return a mapping between them
*
* @returns {Map<number, string>}
*/
async function getGameQueues() {
const gameQueueURL =
"https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/queues.json";
const gameQueueData = await apiCall(gameQueueURL);
var queueMapping = new Map();
for (var queue in gameQueueData) {
queueMapping.set(gameQueueData[queue].id, gameQueueData[queue].name);
}
return queueMapping;
}
/**
* Clipboard copy method for summoner name
*
* @param {string} gameName
* @param {string} tagLine
*/
function copyToClipBoard(gameName, tagLine) {
var toolTip = document.getElementById("copyToClipboardIconText");
navigator.clipboard.writeText(decodeURIComponent(gameName) + "#" + tagLine);
toolTip.innerHTML = "Copied!";
}
/**
* Resets tooltip text when hovering over
* copy to clipboard button
*/
function resetCopyButton() {
var toolTip = document.getElementById("copyToClipboardIconText");
toolTip.innerHTML = "Copy to clipboard";
}
/**
* Searches for the most played champion and fetches
* its splash art through API call for use in the
* profile background
*
* @param {Object<number, Object>} summonerChampionWinrateInfo
* @param {Object<number, string>} championsInfo
*/
async function getProfileBackground(
summonerChampionWinrateInfo,
championsInfo
) {
const [championId] = summonerChampionWinrateInfo.keys();
var championName = championsInfo.get(championId).replace(/[\s\W]/g, "");
const specialCases = {
NunuWillump: "nunu",
RenataGlasc: "renata",
Wukong: "monkeyking",
};
const specialCases2 = ["LeeSin", "Teemo"];
championName = specialCases[championName] || championName;
const container = document.getElementById("profileBg");
const baseImageURL = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/`;
var champImageURL = `/lol-game-data/assets/ASSETS/Characters/${championName}/Skins/Base/Images/${championName}_splash_centered_0.jpg`;
if (specialCases2.includes(championName)) {
champImageURL = `/lol-game-data/assets/ASSETS/Characters/${championName}/Skins/Base/Images/${championName}_splash_centered_0.asu_${championName}.jpg`;
} else if (championName == "Aurora")
champImageURL = `/lol-game-data/assets/ASSETS/Characters/${championName}/Skins/Base/Images/${championName}_splash_centered_0.${championName}.jpg`;
const extractedPath = champImageURL
.replace("/lol-game-data/assets/", "")
.toLowerCase();
const finalURL = baseImageURL + extractedPath;
const championImage = await apiImageCall(finalURL);
const img = document.createElement("img");
img.src = championImage;
container.appendChild(img);
}
/**
* Function that displays champion winrate stats
* for a specific game queue based on champion mastery info
*
* @param {Object<number, Object>} summonerChampionWinrateInfo
* @param {Object<number, string>} championsInfo
* @param {number} queueId
*/
async function makeChampionWinrate(
summonerChampionWinrateInfo,
championsInfo,
queueId
) {
const container = document.getElementById("championEntryList");
if (container._reactRoot) container._reactRoot.unmount();
container._reactRoot = createRoot(container);
var root = container._reactRoot;
const matchComponent = (
<ChampionEntryList
summonerChampionWinrateInfo={summonerChampionWinrateInfo}
championsInfo={championsInfo}
queueId={queueId}
></ChampionEntryList>
);
root.render(matchComponent);
}
/**
* Driver method to enable spinner in match history
* block and to then make match history entry components
* to render in page
*
* @param {string[][]} summonerMatchInfo
* @param {React.Dispatch<React.SetStateAction<boolean>>} setIsTempLoading
*/
async function makeMatchHistory(summonerMatchInfo, setIsTempLoading) {
if (summonerMatchInfo.length == 0) {
document.getElementById("loadingButtonGroup").style.display = "none";
const container = document.getElementById("matchList");
const gamesNotFoundElement = document.createElement("div");
gamesNotFoundElement.setAttribute("id", "notFoundText");
gamesNotFoundElement.innerHTML = `No matches found`;
container.appendChild(gamesNotFoundElement);
return;
}
setIsTempLoading(true);
await makeMatchEntries(summonerMatchInfo, 15);
setIsTempLoading(false);
}
/**
* Extends match history by fetching the next 10
* match entries that come after the last match rendered on the page
*
* @param {string[]} summonerMatchInfo
* @param {string} region
* @param {string} puuid
* @param {React.Dispatch<React.SetStateAction<null>>} setSummonerMatchInfo
* @param {React.Dispatch<React.SetStateAction<null>>} setIsTempLoading
*/
async function extendMatchHistory(
summonerMatchInfo,
region,
puuid,
setSummonerMatchInfo,
setIsTempLoading
) {
setIsTempLoading(true);
const lastMatchDate = summonerMatchInfo.find(
(matchObject) =>
matchObject[0].gameID ===
document.getElementById("matchList").lastChild.id
)[0].gameDateSQLFormat;
const newMatchList = await getExtendedMatchList(region, puuid, lastMatchDate);
const newMatchInfoList = await matchInfoListDriver(
region,
newMatchList,
puuid
);
await makeMatchEntries(newMatchInfoList, 10);
setSummonerMatchInfo(summonerMatchInfo.concat(newMatchInfoList));
setIsTempLoading(false);
}
/**
* Goes through the list of match info stored
* and create components with all the relevant
* match information displayed
*
* @param {string[][]} summonerMatchInfo
* @param {number} minLimit
*/
async function makeMatchEntries(summonerMatchInfo, minLimit) {
const container = document.getElementById("matchList");
const components = [];
const promises = [];
for (
let counter = 0;
counter < Math.min(minLimit, summonerMatchInfo.length);
counter++
) {
const matchComponent = document.createElement("div");
matchComponent.setAttribute("class", "matchHistoryContainer");
matchComponent.setAttribute("id", summonerMatchInfo[counter][0].gameID);
matchComponent.innerHTML = ``;
container.append(matchComponent);
const root = createRoot(matchComponent);
const entryComponent = (
<MatchEntry
summonerMatchInfo={summonerMatchInfo}
counter={counter}
gameQueues={gameQueues}
></MatchEntry>
);
root.render(entryComponent);
matchComponent.style.display = "none";
components.push(matchComponent);
promises.push(getAllAssets(summonerMatchInfo, counter, matchComponent));
}
await Promise.all(promises);
components.forEach((component) => {
component.style.display = "flex";
});
}
/**
* Driver method that sends API calls to fetch
* all image assets related to the match and
* the participants
*
* @param {string[][]} summonerMatchInfo
* @param {number} counter
* @param {HTMLDivElement} component
*/
async function getAllAssets(summonerMatchInfo, counter, component) {
await getSummonerSpellAssets(
summonerMatchInfo[counter][1].summoner1Id,
summonerMatchInfo[counter][1].summoner2Id,
".spellsImages",
component
);
await getSummonerRuneAssets(
summonerMatchInfo[counter][1].perks.styles[0].selections[0].perk,
summonerMatchInfo[counter][1].perks.styles[1].style,
".runeImages",
component
);
await getItemAssets(summonerMatchInfo[counter][1], ".itemImages", component);
await getChampionAssets(
summonerMatchInfo[counter][1].championId,
".championImage",
component
);
await getOtherPlayerAssets(
summonerMatchInfo[counter][2],
".otherPlayers",
component
);
}
/**
* Driver method to build the main summoner profile
* block with info regarding ranked mode and their gameplay
*
* @param {string[]} summonerInfo
* @param {string[][]} summonerRankedInfo
* @param {string[][]} summonerMatchInfo
* @param {Object<number, Object[]>} summonerChampionWinrateInfo
* @param {Object<number, string>} championsInfo
*/
async function makeSummonerProfile(
summonerInfo,
summonerRankedInfo,
summonerMatchInfo,
summonerChampionWinrateInfo,
championsInfo
) {
makeSummonerBadges(
summonerMatchInfo,
summonerChampionWinrateInfo,
championsInfo
);
await getProfileBackground(summonerChampionWinrateInfo, championsInfo);
await makeProfileIcon(summonerInfo);
await makeRankedEmblems(summonerRankedInfo);
}
/**
* Driver method to display badges on profile block
* that represent key information about the player
* and his performance
*
* @param {string[][]} summonerMatchInfo
* @param {Object<number, Object[]>} summonerChampionWinrateInfo
* @param {Object<number, string>} championsInfo
*/
function makeSummonerBadges(
summonerMatchInfo,
summonerChampionWinrateInfo,
championsInfo
) {
if (summonerMatchInfo.length == 0) return;
const summComponent = document.getElementById("summonerBlock");
const root = createRoot(summComponent.querySelector(".summonerChips"));
const allSummonerChips = [];
allSummonerChips.push(
makeMainRoleBadge(summonerMatchInfo),
makeMillionBadge(summonerChampionWinrateInfo, championsInfo),
//makeMostSkilledBadge(championsInfo, summonerChampionWinrateInfo),
makeMostPlayedBadge(summonerChampionWinrateInfo, championsInfo),
makeStreakBadge(summonerMatchInfo)
);
root.render(<>{allSummonerChips}</>);
}
/**
* Gathers most played position in ranked from
* summoner match info and displays it on badge
*
* @param {string[][]} summonerMatchInfo
* @returns {React.ComponentType}
*/
function makeMainRoleBadge(summonerMatchInfo) {
const rolesValues = new Map();
var mostPlayedRole = "SUPPORT";
rolesValues.set(mostPlayedRole, 0);
for (let i = 0; i < summonerMatchInfo.length; i++) {
var role = summonerMatchInfo[i][1].teamPosition;
if (role == "UTILITY") role = "SUPPORT";
else role = summonerMatchInfo[i][1].teamPosition;
const count = rolesValues.get(role) || 0;
rolesValues.set(role, count + 1);
}
for (const [role, count] of rolesValues) {
if (rolesValues.get(role) > rolesValues.get(mostPlayedRole))
mostPlayedRole = role;
}
mostPlayedRole = mostPlayedRole
.toLowerCase()
.replace(/^\w/, (c) => c.toUpperCase());
return (
<Chip
key={mostPlayedRole}
label={`Main ${mostPlayedRole}`}
variant="outlined"
sx={{
borderRadius: "8px",
borderColor: "#c89b3c",
backgroundColor: "#1b1f24",
color: "#c89b3c",
}}
/>
);
}
/**
* Iterates first couple champions on their mastery info
* to determine which ones have a million plus mastery points
* and then displays it on badge
*
* @param {Object<number, Object[]>} summonerChampionWinrateInfo
* @param {Object<number, string>} championsInfo
* @returns {React.ComponentType}
*/
function makeMillionBadge(summonerChampionWinrateInfo, championsInfo) {
const summonerChips = [];
for (const [id, value] of summonerChampionWinrateInfo.entries()) {
if (value.championPoints < 1000000) break;
summonerChips.push(
<Chip
key={id}
label={`${championsInfo.get(id)} Million`}
variant="outlined"
sx={{
borderRadius: "8px",
borderColor: "#c89b3c",
backgroundColor: "#1b1f24",
color: "#c89b3c",
}}
/>
);
}
return summonerChips;
}
function makeMostSkilledBadge(championsInfo, summonerChampionWinrateInfo) {
var [bestChampName, bestChampWinrate] = ["null", 0];
for (const [id, value] of summonerChampionWinrateInfo.entries()) {
const normalWinrate = value.winrateMapping.get(490)[2];
const soloWinrate = value.winrateMapping.get(420)[2];
const flexWinrate = value.winrateMapping.get(440)[2];
const sum = (normalWinrate + soloWinrate + flexWinrate) / 3;
if (sum > bestChampWinrate) {
bestChampName = championsInfo.get(id);
bestChampWinrate = sum;
}
}
if (bestChampName == "null") return;
return (
<Chip
key={`Best ${bestChampName}`}
label={`Skilled ${bestChampName}`}
variant="outlined"
sx={{
borderRadius: "8px",
borderColor: "#c89b3c",
backgroundColor: "#1b1f24",
color: "#c89b3c",
}}
/>
);
}
/**
* Scans games played on all champs in all modes
* to determine the top champion with the most games played
* and then displays it on a badge
*
* @param {Object<number, Object[]>} summonerChampionWinrateInfo
* @param {Object<number, string>} championsInfo
* @returns {React.ComponentType}
*/
function makeMostPlayedBadge(summonerChampionWinrateInfo, championsInfo) {
var [bestChampName, bestChampGames] = [null, 0];
for (const [id, value] of summonerChampionWinrateInfo.entries()) {
const normalGames = value.normalGames;
const soloGames = value.rankedSoloGames;
const flexGames = value.rankedFlexGames;
const sum = normalGames + soloGames + flexGames;
if (sum > bestChampGames) {
bestChampName = championsInfo.get(id);
bestChampGames = sum;
}
}
if (bestChampName == null) return;
return (
<Chip
key={`OTP ${bestChampName}`}
label={`OTP ${bestChampName}`}
variant="outlined"
sx={{
borderRadius: "8px",
borderColor: "#c89b3c",
backgroundColor: "#1b1f24",
color: "#c89b3c",
}}
/>
);
}
/**
* Scans games until the win outcome changes to
* determine if a player is on a winning streak or
* a losing streak and displays it on a badge
*
* @param {string[][]} summonerMatchInfo
* @returns {React.ComponentType}
*/
function makeStreakBadge(summonerMatchInfo) {
const streakType = summonerMatchInfo[0][1].win;
var streakAmount = 0;
for (let i = 0; i < summonerMatchInfo.length; i++) {
if (summonerMatchInfo[i][1].win != streakType) {
streakAmount = i;
break;
}
}
if (streakAmount < 3) return;
return (
<Chip
key={streakType}
label={streakType ? `Winning streak` : `Losing streak`}
variant="outlined"
sx={{
borderRadius: "8px",
borderColor: "#c89b3c",
backgroundColor: "#1b1f24",
color: "#c89b3c",
}}
/>
);
}
/**
* API call to retrieve the profile icon image
* and then display it on a profile component
*
* @param {string[]} summonerInfo
*/
async function makeProfileIcon(summonerInfo) {
const container = document.getElementById("profileIconGroupContainer");
const component = document.createElement("div");
component.setAttribute("class", "profileIconGroup");
component.innerHTML = `
<div id="summonerIcon"></div>
<div id="level">${summonerInfo[0]}</div>
`;
const imgURL = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/profile-icons/${summonerInfo[1]}.jpg`;
const profileIconImage = await apiImageCall(imgURL);
const img = (
<Image src={profileIconImage} width={120} height={120} alt="Profile Icon" />
);
const profileIconImageComponent = component.querySelector("#summonerIcon");
const root = createRoot(profileIconImageComponent);
root.render(img);
container.appendChild(component);
}
/**
* Driver method to retrieve ranked emblem image assets
* and append them to profile components
*
* @param {string[]} summonerRankedInfo
*/
async function makeRankedEmblems(summonerRankedInfo) {
if (summonerRankedInfo[0] !== "Unranked")
await makeRankedEmblem(summonerRankedInfo[0], "rankedFlex");
if (summonerRankedInfo[1] !== "Unranked")
await makeRankedEmblem(summonerRankedInfo[1], "rankedSolo");
}
/**
* Fetches ranked emblem image asset through API call to
* server to send the static image or to a third party API
* to retrieve the image and append it to a component
*
* @param {JSON} summonerRankedInfo
* @param {string} containerName
*/
async function makeRankedEmblem(summonerRankedInfo, containerName) {
const container = document.getElementById(containerName);
const component = document.createElement("div");
component.setAttribute("class", "rankedEmblem");
const imgURL = `https://raw.communitydragon.org/latest/plugins/rcp-fe-lol-static-assets/global/default/ranked-emblem/emblem-${summonerRankedInfo.rankedTier.toLowerCase()}.png`;
const rankedIconImage = await apiImageCall(imgURL);
const img = document.createElement("img");
img.src = rankedIconImage;
component.appendChild(img);
container.appendChild(component);
}
/**
* Retrieves champion image assets through API call to
* server to send the static image or to a third party API
* to retrieve the image and append it to a component
*
* @param {number} championId
* @param {string} insideClass
* @param {HTMLDivElement} parentComponent
*/
async function getChampionAssets(championId, insideClass, parentComponent) {
const championDataURL = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champions/${championId}.json`;
const championData = await apiCall(championDataURL);
const baseImageURL = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/`;
const champImageURL = championData.squarePortraitPath;
const extractedPath = champImageURL
.replace("/lol-game-data/assets/", "")
.toLowerCase();
const finalURL = baseImageURL + extractedPath;
const championImage = await apiImageCall(finalURL);
const img = (
<Image src={championImage} width={30} height={30} alt={championData.name} />
);
const championImageComponent = parentComponent.querySelector(insideClass);
const root = createRoot(championImageComponent);
root.render(img);
}
/**
* Driver method that gets JSON data from a URL
* and uses it to get summoner image assets
*
* @param {string} summoner1Id
* @param {string} summoner2Id
* @returns {HTMLImageElement[]}
*/
async function getSummonerSpellAssets(
summoner1Id,
summoner2Id,
divClass,
component
) {
const summonerSpellsURL = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/summoner-spells.json`;
const summonerSpellsData = await apiCall(summonerSpellsURL);
const baseImageURL = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/`;
const img1 = await getSummonerSpellImage(
summonerSpellsData,
summoner1Id,
baseImageURL
);
const img2 = await getSummonerSpellImage(
summonerSpellsData,
summoner2Id,
baseImageURL
);
const spellsImagesComponent = component.querySelector(divClass);
const root = createRoot(spellsImagesComponent);
root.render([img1, img2]);
}
/**
* Executes API call to get image data
* and return image component from it
*
* @param {JSON} summonerSpellsData
* @param {string} spellID
* @param {string} baseImageURL
* @returns {HTMLImageElement}
*/
async function getSummonerSpellImage(
summonerSpellsData,
spellID,
baseImageURL
) {
const spellObject = summonerSpellsData.find((spell) => spell.id === spellID);
const parts = spellObject.iconPath.split("/");
const summonerSpellImageURL = spellObject.iconPath;
const extractedPath = summonerSpellImageURL
.replace("/lol-game-data/assets/", "")
.toLowerCase();
const finalURL = baseImageURL + extractedPath;
const summonerSpellImage = await apiImageCall(finalURL);
const img = (
<Image
src={summonerSpellImage}
width={35}
height={35}
alt="Summoner Spell Icon"
/>
);
return img;
}
/**
* Driver method to get rune image files and render
* HTMLImageElement components
*
* @param {number} mainRuneID
* @param {number} secondaryRuneID
* @param {string} divClass
* @param {HTMLDivElement} component
*/
async function getSummonerRuneAssets(
mainRuneID,
secondaryRuneID,
divClass,
component
) {
const runeImages = await Promise.all([
getRuneImage(mainRuneID, false),
getRuneImage(secondaryRuneID, true),
]);
const summonerRunesImagesComponent = component.querySelector(divClass);
const root = createRoot(summonerRunesImagesComponent);
root.render(<>{runeImages}</>);
}
/**
* Sends API call to gather JSON data for runes based on it's type
* (primary or secondary), sends API call to retrieve image from the iconpath
* in the JSON data and then creates a HTMLImageElement to append the file image
*
* @param {number} runeID
* @param {boolean} isSecondary
*/
async function getRuneImage(runeID, isSecondary) {
const runeDataURL = isSecondary
? `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json`
: `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/perks.json`;
const summonerRuneData = await apiCall(runeDataURL);
const runeObject = isSecondary
? summonerRuneData.styles.find((rune) => rune.id === runeID)
: summonerRuneData.find((rune) => rune.id === runeID);
const baseImageURL = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/`;
const runeImageURL = runeObject.iconPath;
const extractedPath = runeImageURL
.replace("/lol-game-data/assets/", "")
.toLowerCase();
const finalURL = baseImageURL + extractedPath;
const summonerRuneImage = await apiImageCall(finalURL);
var img;
if (!isSecondary) {
img = (
<Image
src={summonerRuneImage}
width={40}
height={40}
alt={"Primary Rune Icon"}
id={"primaryRune"}
key={"Primary Rune"}
/>
);
} else {
img = (
<Image
src={summonerRuneImage}
width={22}
height={22}
alt={"Secondary Rune Icon"}
id={"secondaryRune"}
key={"Secondary Rune"}
/>
);
}
return img;
}
/**
* Driver method that sends API call to retrieve item JSON data
* in order to get icon paths based on item ID
*
* @param {string[]} summonerInfo
* @param {string} divClass
* @param {HTMLDivElement} component
*/
async function getItemAssets(summonerInfo, divClass, component) {
const itemDataURL = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/items.json`;
const summonerItemData = await apiCall(itemDataURL);
const baseImageURL = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/`;
var itemIds = [
summonerInfo.item0,
summonerInfo.item1,
summonerInfo.item2,
summonerInfo.item3,
summonerInfo.item4,
summonerInfo.item5,
summonerInfo.item6,
];
var emptyImagesCounter = 0;
const imagesArray = await Promise.all(
itemIds.map(async (itemId, index) => {
if (itemId !== 0) {
const image = await getSummonerItemImage(
summonerItemData,
itemId,
baseImageURL
);
if (!image) {
emptyImagesCounter++;
return null;
}
return (
<Image
src={image}
width={45}
height={45}
alt={`Summoner Item ${itemId}`}
key={`${itemId}-${index}`}
/>
);
} else {
emptyImagesCounter++;
return null;
}
})
);
const itemsImagesComponent = component.querySelector(divClass);
const root = createRoot(itemsImagesComponent);
root.render(
<>
{imagesArray}
{[...Array(emptyImagesCounter)].map((_, index) => (
<div key={`empty-${index}`} className="emptyItem" />
))}
</>
);
}
/**
* Queries JSON data to get URL for the icon path of
* a specific item. API call is sent to that URL and
* a HTMLImageElement is returned with that image on it
*
* @param {JSON} summonerItemData
* @param {number} itemID
* @param {string} baseImageURL
* @returns {HTMLImageElement}
*/
async function getSummonerItemImage(summonerItemData, itemID, baseImageURL) {
const itemObject = summonerItemData.find((item) => item.id === itemID);
if (!itemObject) return null;
const summonerItemImageURL = itemObject.iconPath;
const extractedPath = summonerItemImageURL
.replace("/lol-game-data/assets/", "")
.toLowerCase();
const finalURL = baseImageURL + extractedPath;
const summonerItemImage = await apiImageCall(finalURL);
return summonerItemImage;
}
/**
* Creates HTMLDivElement entry to display player name and champion picture
* retrieved from API call
*
* @param {string[][]} participantsInfo
* @param {string} divClass
* @param {HTMLDivElement} component
*/
async function getOtherPlayerAssets(participantsInfo, divClass, component) {
for (const participantInfo of participantsInfo) {
const playerName = participantInfo.riotIdGameName;
const playerTagLine = participantInfo.riotIdTagline;
const playerComponent = document.createElement("div");
playerComponent.setAttribute("class", "player");
checkIfOwnPlayer(playerName, playerTagLine, playerComponent);
const playerParentComponent = component.querySelector(divClass);
playerParentComponent.append(playerComponent);
await getChampionAssets(
participantInfo.championId,
".playerImage",
playerComponent
);
}
}
/**
* Check to see if the player is the one being displayed on profile
* in order to highlight it with a different class name
*
* @param {string} playerName
* @param {string} playerTagLine
* @param {HTMLDivElement} playerComponent
*/
function checkIfOwnPlayer(playerName, playerTagLine, playerComponent) {
if (playerName == ownUsername) {
playerComponent.innerHTML = `
<div class="playerImage" id="ownImage"></div>
<a class="ownUsername" href="${playerName}-${playerTagLine}" target="_blank">${playerName}</a>
<span class="tooltip-text">${playerName} #${playerTagLine}</span>
`;
} else {
playerComponent.innerHTML = `
<div class="playerImage"></div>
<a class="playerUsername" href="${playerName}-${playerTagLine}">${playerName}</a>
<span class="tooltip-text">${playerName} #${playerTagLine}</span>
`;
}
}
/**
* Driver method to update games played and winrate percentage
* based on ranked game mode (solo, flex)
*
* @param {number} gameQueue
* @param {number} winrateNumber
*/
function loadWinrate(gameQueue, winrateNumber) {
var totalGames = 0;
var winratePercentage = 0;
if (gameQueue.rankedGames != undefined) {
totalGames = gameQueue.rankedGames;
winratePercentage = winrateNumber;
}
updateProgressBar("games", totalGames, `${totalGames}`);
updateProgressBar("winrate", winratePercentage, `${winratePercentage}%`);
}
/**
* Button method to change winrate and games played
* value on the circular progress bars
*
* @param {string} elementId
* @param {number} value
* @param {string} text
*/
function updateProgressBar(elementId, value, text) {
const progressbarTextElement = document.querySelector(
`#${elementId} .CircularProgressbar-text`
);
const progressbarElement = document.querySelector(
`#${elementId} .CircularProgressbar-path`
);
progressbarTextElement.textContent = text;
if (elementId == "games")
progressbarElement.style.strokeDashoffset = value > 0 ? 0 : 298.451;
else progressbarElement.style.strokeDashoffset = 298.451 * (1 - value / 100);
}
/**
* Formats match info list into an array of value points
* with labels to display on the LineChart component
*
* @param {string[][]} summonerMatchInfo
* @param {React.Dispatch<React.SetStateAction<null>>} setgraphData
*/
function getGraphDates(summonerMatchInfo, setgraphData) {
const dateMap = new Map();
const reversedMatchInfo = [...summonerMatchInfo].reverse();
for (const matchInfo of reversedMatchInfo) {
if (matchInfo[0].gameDuration < 300) continue;
const formattedDate = matchInfo[0].gameDateSQLFormat.split(" ")[0];
const gamePoint = matchInfo[1].win ? 1 : -1;
const dateData = dateMap.get(formattedDate);
const wins =
gamePoint > 0 ? (dateData?.[0] ?? 0) + gamePoint : dateData?.[0] ?? 0;
const losses =
gamePoint < 0 ? (dateData?.[1] ?? 0) + gamePoint : dateData?.[1] ?? 0;
const total = wins + losses;
dateMap.set(formattedDate, [wins, losses, total, `W:${wins} L:${losses}`]);
}
const dates = Array.from(dateMap, ([date, [wins, losses, value, label]]) => ({
date,
value,
label,
}));
setgraphData(dates);
}
export default Dashboard;