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) => ` ` ).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 }) => `
${MODE_LABELS[mode] || mode} ${tier} ${points} points ${tier}
` ), ...Array.from({ length: emptySlots }).map( () => `
-
` ), ].join(""); if (!profileTiersEl.innerHTML) { profileTiersEl.innerHTML = `

No gamemode tiers assigned yet

`; } } 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 ` `; } 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) => `
${MODE_LABELS[entry.mode] || entry.mode} ${entry.tier} ${entry.points} points ${entry.tier}
` ), ...Array.from({ length: emptySlots }).map( () => `
-
` ), ].join(""); const points = state.gamemode === "overall" ? player.totalPoints : player.gamemodePoints || 0; const region = String(player.region || "NA").toUpperCase(); const regionClass = `region-${region.toLowerCase()}`; return `
#${globalIndex}
${player.username} ${player.username}
${region}
${tierSlotsHtml}
${points} POINTS
`; } 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 = `
${[1, 2, 3, 4, 5] .map((tierNum) => { const rows = grouped[tierNum]; return `
${tierHeaderIconTemplate(tierNum)} Tier ${tierNum}
${ rows.length === 0 ? `

No players currently have this rank yet

` : rows .map( (row) => ` ` ) .join("") }
`; }) .join("")}
`; 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 = `
Search failed
`; } finally { activeSearchController = null; } } function renderSearchResults(results) { if (results.length === 0) { searchDropdownEl.classList.remove("hidden"); searchDropdownEl.innerHTML = `
No players found
`; return; } searchDropdownEl.classList.remove("hidden"); searchDropdownEl.innerHTML = results .map( (player) => `
${player.username}
${player.username}
${player.totalPoints || 0} POINTS
` ) .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();