uploaded
This commit is contained in:
630
public/script.js
Normal file
630
public/script.js
Normal file
@@ -0,0 +1,630 @@
|
||||
const GAMEMODES = [
|
||||
{ id: "overall", label: "Overall", icon: "assets/gamemodes/overall.png" },
|
||||
{ id: "vanilla", label: "Vanilla", icon: "assets/gamemodes/smaller-vanilla-pvp.png" },
|
||||
{ id: "mace", label: "Mace", icon: "assets/gamemodes/mace-pvp.png" },
|
||||
{ id: "axe", label: "Axe", icon: "assets/gamemodes/axe-pvp.png" },
|
||||
{ id: "sword", label: "Sword", icon: "assets/gamemodes/sword-pvp.png" },
|
||||
{ id: "smp", label: "SMP", icon: "assets/gamemodes/smp.png" },
|
||||
{ id: "diamondsmp", label: "Diamond SMP", icon: "assets/gamemodes/diamond-smp.png" },
|
||||
{ id: "uhc", label: "UHC", icon: "assets/gamemodes/uhc.png" },
|
||||
{ id: "pot", label: "Pot", icon: "assets/gamemodes/pot-pvp.png" },
|
||||
{ id: "nethop", label: "Neth OP", icon: "assets/gamemodes/neth-op.png" },
|
||||
{ id: "cart", label: "Cart", icon: "assets/gamemodes/cart-pvp.png" },
|
||||
];
|
||||
|
||||
const MODE_ICON_PATH = {
|
||||
vanilla: "assets/gamemodes/smaller-vanilla-pvp.png",
|
||||
mace: "assets/gamemodes/mace-pvp.png",
|
||||
axe: "assets/gamemodes/axe-pvp.png",
|
||||
sword: "assets/gamemodes/sword-pvp.png",
|
||||
smp: "assets/gamemodes/smp.png",
|
||||
diamondsmp: "assets/gamemodes/diamond-smp.png",
|
||||
uhc: "assets/gamemodes/uhc.png",
|
||||
pot: "assets/gamemodes/pot-pvp.png",
|
||||
nethop: "assets/gamemodes/neth-op.png",
|
||||
cart: "assets/gamemodes/cart-pvp.png",
|
||||
};
|
||||
|
||||
const MODE_LABELS = {
|
||||
vanilla: "Vanilla",
|
||||
mace: "Mace",
|
||||
axe: "Axe",
|
||||
sword: "Sword",
|
||||
smp: "SMP",
|
||||
diamondsmp: "Diamond SMP",
|
||||
uhc: "UHC",
|
||||
pot: "Pot",
|
||||
nethop: "Neth OP",
|
||||
cart: "Cart",
|
||||
};
|
||||
|
||||
const TIER_POINTS = {
|
||||
HT1: 60,
|
||||
LT1: 45,
|
||||
HT2: 30,
|
||||
LT2: 20,
|
||||
HT3: 10,
|
||||
LT3: 6,
|
||||
HT4: 4,
|
||||
LT4: 3,
|
||||
HT5: 2,
|
||||
LT5: 1,
|
||||
};
|
||||
|
||||
function getAvatarUrl(username) {
|
||||
return `https://render.crafty.gg/3d/bust/${encodeURIComponent(username)}`;
|
||||
}
|
||||
|
||||
const state = {
|
||||
gamemode: "overall",
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
totalItems: 0,
|
||||
players: [],
|
||||
};
|
||||
const profileCache = new Map();
|
||||
const searchCache = new Map();
|
||||
let activeSearchController = null;
|
||||
|
||||
const tabsEl = document.getElementById("tabs");
|
||||
const rankingListEl = document.getElementById("rankingList");
|
||||
const listHeadEl = document.querySelector(".list-head");
|
||||
const viewTitleEl = document.getElementById("viewTitle");
|
||||
const statusLabelEl = document.getElementById("statusLabel");
|
||||
const loadingStateEl = document.getElementById("loadingState");
|
||||
const errorStateEl = document.getElementById("errorState");
|
||||
const paginationEl = document.getElementById("pagination");
|
||||
const pageInfoEl = document.getElementById("pageInfo");
|
||||
const prevPageEl = document.getElementById("prevPage");
|
||||
const nextPageEl = document.getElementById("nextPage");
|
||||
const searchInputEl = document.getElementById("searchInput");
|
||||
const searchDropdownEl = document.getElementById("searchDropdown");
|
||||
const profileOverlayEl = document.getElementById("profileOverlay");
|
||||
const profileModalEl = document.getElementById("profileModal");
|
||||
const profileCloseBtnEl = document.getElementById("profileCloseBtn");
|
||||
const profileLoadingEl = document.getElementById("profileLoading");
|
||||
const profileErrorEl = document.getElementById("profileError");
|
||||
const profileContentEl = document.getElementById("profileContent");
|
||||
const profileAvatarEl = document.getElementById("profileAvatar");
|
||||
const profileUsernameEl = document.getElementById("profileUsername");
|
||||
const profileRegionEl = document.getElementById("profileRegion");
|
||||
const profileTopPointsEl = document.getElementById("profileTopPoints");
|
||||
const profilePositionEl = document.getElementById("profilePosition");
|
||||
const profileTiersEl = document.getElementById("profileTiers");
|
||||
|
||||
function debounce(fn, delay = 250) {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
function tierColorClass(tier) {
|
||||
if (!tier) {
|
||||
return "";
|
||||
}
|
||||
return tier.toLowerCase();
|
||||
}
|
||||
|
||||
function buildTabs() {
|
||||
tabsEl.innerHTML = GAMEMODES.map(
|
||||
(mode) => `
|
||||
<button
|
||||
class="tab-btn ${mode.id === state.gamemode ? "active" : ""}"
|
||||
data-mode="${mode.id}"
|
||||
title="${mode.label}"
|
||||
aria-label="${mode.label}"
|
||||
>
|
||||
<img src="${mode.icon}" alt="${mode.label}" class="tab-icon" />
|
||||
<span class="sr-only">${mode.label}</span>
|
||||
</button>
|
||||
`
|
||||
).join("");
|
||||
|
||||
tabsEl.querySelectorAll(".tab-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const mode = btn.dataset.mode;
|
||||
if (mode === state.gamemode) {
|
||||
return;
|
||||
}
|
||||
state.gamemode = mode;
|
||||
state.page = 1;
|
||||
buildTabs();
|
||||
loadRankings();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeProfileModal() {
|
||||
profileOverlayEl.classList.add("hidden");
|
||||
profileOverlayEl.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("modal-open");
|
||||
}
|
||||
|
||||
function renderProfileModal(player) {
|
||||
profileAvatarEl.src = getAvatarUrl(player.username);
|
||||
profileAvatarEl.alt = `${player.username} avatar`;
|
||||
profileUsernameEl.textContent = player.username;
|
||||
profilePositionEl.textContent = `#${player.position}`;
|
||||
const profileTopClass =
|
||||
player.position === 1 ? "position-top-1" : player.position === 2 ? "position-top-2" : player.position === 3 ? "position-top-3" : "";
|
||||
profilePositionEl.className = `position-rank ${profileTopClass}`.trim();
|
||||
const region = String(player.region || "NA").toUpperCase();
|
||||
profileRegionEl.textContent = region;
|
||||
profileRegionEl.className = `profile-region region-${region.toLowerCase()}`;
|
||||
profileTopPointsEl.textContent = `${player.totalPoints} points`;
|
||||
|
||||
const sortedProfileTiers = Object.entries(player.tiers || {})
|
||||
.filter(([, tier]) => tier)
|
||||
.map(([mode, tier]) => ({
|
||||
mode,
|
||||
tier,
|
||||
points: TIER_POINTS[tier] || 0,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (b.points !== a.points) {
|
||||
return b.points - a.points;
|
||||
}
|
||||
return (MODE_LABELS[a.mode] || a.mode).localeCompare(MODE_LABELS[b.mode] || b.mode);
|
||||
});
|
||||
|
||||
const maxTierSlots = 10;
|
||||
const filledSlots = sortedProfileTiers.slice(0, maxTierSlots);
|
||||
const emptySlots = Math.max(maxTierSlots - filledSlots.length, 0);
|
||||
|
||||
profileTiersEl.innerHTML = [
|
||||
...filledSlots.map(
|
||||
({ mode, tier, points }) => `
|
||||
<div class="profile-tier-bubble filled">
|
||||
<span class="profile-tier-icon-wrap ${tierColorClass(tier)}" data-tier="${tier}" data-points="${points}">
|
||||
<img src="${MODE_ICON_PATH[mode]}" alt="${MODE_LABELS[mode] || mode}" />
|
||||
<span class="tier-hover-pop" role="tooltip">
|
||||
<strong>${tier}</strong>
|
||||
<span>${points} points</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="profile-tier-tag ${tierColorClass(tier)}">${tier}</span>
|
||||
</div>
|
||||
`
|
||||
),
|
||||
...Array.from({ length: emptySlots }).map(
|
||||
() => `
|
||||
<div class="profile-tier-bubble empty">
|
||||
<span class="profile-tier-icon-wrap"></span>
|
||||
<span class="profile-tier-tag muted">-</span>
|
||||
</div>
|
||||
`
|
||||
),
|
||||
].join("");
|
||||
|
||||
if (!profileTiersEl.innerHTML) {
|
||||
profileTiersEl.innerHTML = `<p class="profile-tier-empty">No gamemode tiers assigned yet</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function openProfileModal(username) {
|
||||
profileOverlayEl.classList.remove("hidden");
|
||||
profileOverlayEl.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("modal-open");
|
||||
profileLoadingEl.classList.remove("hidden");
|
||||
profileErrorEl.classList.add("hidden");
|
||||
profileErrorEl.textContent = "";
|
||||
profileContentEl.classList.add("hidden");
|
||||
|
||||
try {
|
||||
if (profileCache.has(username)) {
|
||||
renderProfileModal(profileCache.get(username));
|
||||
profileContentEl.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/players/${encodeURIComponent(username)}`, {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed (${response.status})`);
|
||||
}
|
||||
|
||||
const player = await response.json();
|
||||
profileCache.set(username, player);
|
||||
renderProfileModal(player);
|
||||
profileContentEl.classList.remove("hidden");
|
||||
} catch (_error) {
|
||||
profileErrorEl.classList.remove("hidden");
|
||||
profileErrorEl.textContent = "Failed to load player profile.";
|
||||
} finally {
|
||||
profileLoadingEl.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(loading) {
|
||||
loadingStateEl.classList.toggle("hidden", !loading);
|
||||
}
|
||||
|
||||
function setError(message = "") {
|
||||
errorStateEl.classList.toggle("hidden", !message);
|
||||
errorStateEl.textContent = message;
|
||||
}
|
||||
|
||||
function renderPagination() {
|
||||
if (state.gamemode !== "overall" || state.totalItems === 0) {
|
||||
paginationEl.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
paginationEl.classList.remove("hidden");
|
||||
pageInfoEl.textContent = `Page ${state.page} / ${state.totalPages} | ${state.totalItems} players`;
|
||||
prevPageEl.disabled = state.page <= 1;
|
||||
nextPageEl.disabled = state.page >= state.totalPages;
|
||||
}
|
||||
|
||||
function visibleTiers(player) {
|
||||
return Object.entries(player.tiers || {}).filter(([, tier]) => tier);
|
||||
}
|
||||
|
||||
function getTierNumber(tierValue) {
|
||||
const value = String(tierValue || "");
|
||||
const maybeNumber = Number(value.slice(-1));
|
||||
return Number.isInteger(maybeNumber) ? maybeNumber : null;
|
||||
}
|
||||
|
||||
function getTierBand(tierValue) {
|
||||
return String(tierValue || "").toUpperCase().startsWith("HT") ? "HT" : "LT";
|
||||
}
|
||||
|
||||
function tierHeaderIconTemplate(tierNum) {
|
||||
if (tierNum > 3) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `
|
||||
<span class="tier-head-icon trophy-${tierNum}" aria-hidden="true">
|
||||
<img src="assets/gamemodes/overall.png" alt="" />
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
function playerCardTemplate(player, index) {
|
||||
const globalIndex = (state.page - 1) * 20 + index + 1;
|
||||
let topClass = "";
|
||||
|
||||
if (globalIndex === 1) {
|
||||
topClass = "top-1";
|
||||
} else if (globalIndex === 2) {
|
||||
topClass = "top-2";
|
||||
} else if (globalIndex === 3) {
|
||||
topClass = "top-3";
|
||||
}
|
||||
|
||||
const tierEntries = visibleTiers(player)
|
||||
.map(([mode, tier]) => ({
|
||||
mode,
|
||||
tier,
|
||||
points: TIER_POINTS[tier] || 0,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (b.points !== a.points) {
|
||||
return b.points - a.points;
|
||||
}
|
||||
return (MODE_LABELS[a.mode] || a.mode).localeCompare(MODE_LABELS[b.mode] || b.mode);
|
||||
});
|
||||
|
||||
const maxTierSlots = 10;
|
||||
const filledSlots = tierEntries.slice(0, maxTierSlots);
|
||||
const emptySlots = Math.max(maxTierSlots - filledSlots.length, 0);
|
||||
|
||||
const tierSlotsHtml = [
|
||||
...filledSlots.map(
|
||||
(entry) => `
|
||||
<div class="tier-slot filled">
|
||||
<span class="tier-bubble ${tierColorClass(entry.tier)}" data-tier="${entry.tier}" data-points="${entry.points}">
|
||||
<img src="${MODE_ICON_PATH[entry.mode]}" alt="${MODE_LABELS[entry.mode] || entry.mode}" />
|
||||
<span class="tier-hover-pop" role="tooltip">
|
||||
<strong>${entry.tier}</strong>
|
||||
<span>${entry.points} points</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="tier-label ${tierColorClass(entry.tier)}">${entry.tier}</span>
|
||||
</div>
|
||||
`
|
||||
),
|
||||
...Array.from({ length: emptySlots }).map(
|
||||
() => `
|
||||
<div class="tier-slot empty">
|
||||
<span class="tier-bubble"></span>
|
||||
<span class="tier-label muted">-</span>
|
||||
</div>
|
||||
`
|
||||
),
|
||||
].join("");
|
||||
|
||||
const points = state.gamemode === "overall" ? player.totalPoints : player.gamemodePoints || 0;
|
||||
const region = String(player.region || "NA").toUpperCase();
|
||||
const regionClass = `region-${region.toLowerCase()}`;
|
||||
|
||||
return `
|
||||
<article class="rank-card ${topClass}" data-username="${player.username}">
|
||||
<div class="rank-number">#${globalIndex}</div>
|
||||
<div class="rank-main">
|
||||
<div class="player-head">
|
||||
<img class="avatar" src="${getAvatarUrl(player.username)}" alt="${player.username}" loading="lazy" decoding="async" />
|
||||
<strong>${player.username}</strong>
|
||||
</div>
|
||||
<div class="rank-detail-band">
|
||||
<div class="region-spot ${regionClass}">
|
||||
<span>${region}</span>
|
||||
</div>
|
||||
<div class="tier-slot-list">${tierSlotsHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="points-col">
|
||||
<strong>${points}</strong>
|
||||
<span>POINTS</span>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderRankings() {
|
||||
viewTitleEl.textContent =
|
||||
state.gamemode === "overall"
|
||||
? "Overall Rankings"
|
||||
: `${GAMEMODES.find((m) => m.id === state.gamemode)?.label || state.gamemode} Rankings`;
|
||||
|
||||
statusLabelEl.textContent = `${state.totalItems} players ranked`;
|
||||
listHeadEl.classList.add("centered");
|
||||
|
||||
if (state.gamemode !== "overall") {
|
||||
const grouped = {
|
||||
1: [],
|
||||
2: [],
|
||||
3: [],
|
||||
4: [],
|
||||
5: [],
|
||||
};
|
||||
|
||||
for (const player of state.players) {
|
||||
const tierValue = player.tiers?.[state.gamemode];
|
||||
if (!tierValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tierNumber = getTierNumber(tierValue);
|
||||
if (!tierNumber || !grouped[tierNumber]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
grouped[tierNumber].push({
|
||||
username: player.username,
|
||||
tierValue,
|
||||
band: getTierBand(tierValue),
|
||||
});
|
||||
}
|
||||
|
||||
for (const key of Object.keys(grouped)) {
|
||||
grouped[key].sort((a, b) => {
|
||||
if (a.band !== b.band) {
|
||||
return a.band === "HT" ? -1 : 1;
|
||||
}
|
||||
return a.username.localeCompare(b.username);
|
||||
});
|
||||
}
|
||||
|
||||
rankingListEl.innerHTML = `
|
||||
<section class="tier-columns">
|
||||
${[1, 2, 3, 4, 5]
|
||||
.map((tierNum) => {
|
||||
const rows = grouped[tierNum];
|
||||
return `
|
||||
<article class="tier-column glass-panel tier-column-${tierNum}">
|
||||
<header class="tier-column-head">
|
||||
<div class="tier-column-title-row">
|
||||
${tierHeaderIconTemplate(tierNum)}
|
||||
<span class="tier-column-title">Tier ${tierNum}</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="tier-column-body">
|
||||
${
|
||||
rows.length === 0
|
||||
? `<p class="tier-empty">No players currently have this rank yet</p>`
|
||||
: rows
|
||||
.map(
|
||||
(row) => `
|
||||
<button class="tier-player-row" data-username="${row.username}">
|
||||
<img class="tier-player-avatar" src="${getAvatarUrl(row.username)}" alt="${row.username}" loading="lazy" decoding="async" />
|
||||
<span class="tier-player-name">${row.username}</span>
|
||||
<span class="tier-player-arrow ${row.band === "HT" ? "up" : "down"}">${row.band === "HT" ? "↑" : "↓"}</span>
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join("")
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</section>
|
||||
`;
|
||||
|
||||
rankingListEl.querySelectorAll(".tier-player-row").forEach((row) => {
|
||||
row.addEventListener("click", () => {
|
||||
openProfileModal(row.dataset.username);
|
||||
});
|
||||
});
|
||||
|
||||
renderPagination();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.players.length === 0) {
|
||||
rankingListEl.innerHTML = "";
|
||||
renderPagination();
|
||||
return;
|
||||
}
|
||||
|
||||
rankingListEl.innerHTML = state.players.map(playerCardTemplate).join("");
|
||||
|
||||
rankingListEl.querySelectorAll(".rank-card").forEach((card) => {
|
||||
card.addEventListener("click", () => {
|
||||
const username = card.dataset.username;
|
||||
openProfileModal(username);
|
||||
});
|
||||
});
|
||||
|
||||
renderPagination();
|
||||
}
|
||||
|
||||
async function loadRankings() {
|
||||
setError("");
|
||||
setLoading(true);
|
||||
rankingListEl.innerHTML = "";
|
||||
|
||||
const endpoint =
|
||||
state.gamemode === "overall"
|
||||
? `/api/players?page=${state.page}`
|
||||
: `/api/rankings/${state.gamemode}?page=1&perPage=500`;
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed (${response.status})`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
state.players = payload.data || [];
|
||||
state.totalPages = payload.pagination?.totalPages || 1;
|
||||
state.totalItems = payload.pagination?.total || state.players.length;
|
||||
|
||||
renderRankings();
|
||||
} catch (_error) {
|
||||
setError("Failed to load rankings. Check API/database connection.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function runSearch(query) {
|
||||
if (!query) {
|
||||
searchDropdownEl.classList.add("hidden");
|
||||
searchDropdownEl.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchCache.has(query)) {
|
||||
const cached = searchCache.get(query);
|
||||
renderSearchResults(cached);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeSearchController) {
|
||||
activeSearchController.abort();
|
||||
}
|
||||
activeSearchController = new AbortController();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
|
||||
headers: { Accept: "application/json" },
|
||||
signal: activeSearchController.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Search request failed");
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const results = payload.data || [];
|
||||
searchCache.set(query, results);
|
||||
renderSearchResults(results);
|
||||
} catch (_error) {
|
||||
if (_error && _error.name === "AbortError") {
|
||||
return;
|
||||
}
|
||||
searchDropdownEl.classList.remove("hidden");
|
||||
searchDropdownEl.innerHTML = `<div class="search-item"><div></div><div><strong>Search failed</strong></div></div>`;
|
||||
} finally {
|
||||
activeSearchController = null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSearchResults(results) {
|
||||
if (results.length === 0) {
|
||||
searchDropdownEl.classList.remove("hidden");
|
||||
searchDropdownEl.innerHTML = `<div class="search-item"><div></div><div><strong>No players found</strong></div></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
searchDropdownEl.classList.remove("hidden");
|
||||
searchDropdownEl.innerHTML = results
|
||||
.map(
|
||||
(player) => `
|
||||
<div class="search-item" data-user="${player.username}">
|
||||
<img src="${getAvatarUrl(player.username)}" alt="${player.username}" loading="lazy" decoding="async" />
|
||||
<div>
|
||||
<strong>${player.username}</strong>
|
||||
</div>
|
||||
<div class="search-points">
|
||||
<strong>${player.totalPoints || 0}</strong>
|
||||
<span>POINTS</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
searchDropdownEl.querySelectorAll(".search-item[data-user]").forEach((item) => {
|
||||
item.addEventListener("click", () => {
|
||||
const username = item.dataset.user;
|
||||
searchDropdownEl.classList.add("hidden");
|
||||
openProfileModal(username);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const debouncedSearch = debounce((value) => runSearch(value), 220);
|
||||
|
||||
searchInputEl.addEventListener("input", (event) => {
|
||||
debouncedSearch(event.target.value.trim());
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!event.target.closest("#searchShell")) {
|
||||
searchDropdownEl.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
profileCloseBtnEl.addEventListener("click", closeProfileModal);
|
||||
|
||||
profileOverlayEl.addEventListener("click", (event) => {
|
||||
if (!event.target.closest("#profileModal")) {
|
||||
closeProfileModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape" && !profileOverlayEl.classList.contains("hidden")) {
|
||||
closeProfileModal();
|
||||
}
|
||||
});
|
||||
|
||||
prevPageEl.addEventListener("click", () => {
|
||||
if (state.page > 1) {
|
||||
state.page -= 1;
|
||||
loadRankings();
|
||||
}
|
||||
});
|
||||
|
||||
nextPageEl.addEventListener("click", () => {
|
||||
if (state.page < state.totalPages) {
|
||||
state.page += 1;
|
||||
loadRankings();
|
||||
}
|
||||
});
|
||||
|
||||
buildTabs();
|
||||
loadRankings();
|
||||
|
||||
Reference in New Issue
Block a user