From 6c8f34acb997f0ffe38e8867a8af1aa9562ba620 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 18 Aug 2025 21:52:54 +0200 Subject: [PATCH] fixed some shit ; added pagination --- src/{static => JavaScript}/script.js | 1407 +++++++++++++------------- src/app.py | 7 +- src/templates/index.html | 4 +- 3 files changed, 724 insertions(+), 694 deletions(-) rename src/{static => JavaScript}/script.js (68%) diff --git a/src/static/script.js b/src/JavaScript/script.js similarity index 68% rename from src/static/script.js rename to src/JavaScript/script.js index 47e7c17..104fab4 100644 --- a/src/static/script.js +++ b/src/JavaScript/script.js @@ -1,692 +1,717 @@ -document.addEventListener("DOMContentLoaded", () => { - "use strict"; - - // Utility functions - const utils = { - safeLocalStorage: { - getItem(key) { - try { - return localStorage.getItem(key); - } catch (e) { - console.warn("localStorage.getItem failed:", e); - return null; - } - }, - setItem(key, value) { - try { - localStorage.setItem(key, value); - return true; - } catch (e) { - console.warn("localStorage.setItem failed:", e); - return false; - } - } - }, - - debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func.apply(this, args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - }, - - throttle(func, limit) { - let inThrottle; - return function executedFunction(...args) { - if (!inThrottle) { - func.apply(this, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - }; - } - }; - - // Dark Mode Manager - class DarkModeManager { - constructor() { - this.darkModeToggle = document.getElementById("darkModeToggle"); - this.html = document.documentElement; - this.init(); - } - - init() { - this.loadSavedPreference(); - this.attachEventListeners(); - } - - loadSavedPreference() { - const savedDarkMode = utils.safeLocalStorage.getItem("darkMode"); - if ( - savedDarkMode === "true" || - (savedDarkMode === null && - window.matchMedia("(prefers-color-scheme: dark)").matches) - ) { - this.html.classList.add("dark"); - } - } - - attachEventListeners() { - this.darkModeToggle?.addEventListener("click", () => { - this.html.classList.toggle("dark"); - utils.safeLocalStorage.setItem("darkMode", this.html.classList.contains("dark")); - }); - } - } - - // Problem Manager - class ProblemManager { - constructor() { - this.problemSearch = document.getElementById("problemSearch"); - this.problemsContainer = document.getElementById("problemsContainer"); - this.problemsPagination = document.getElementById("problemsPagination"); - this.problemsPrevBtn = document.getElementById("problemsPrevBtn"); - this.problemsNextBtn = document.getElementById("problemsNextBtn"); - this.problemsPaginationInfo = document.getElementById("problemsPaginationInfo"); - this.difficultyFilter = document.getElementById("difficultyFilter"); - this.sortProblems = document.getElementById("sortProblems"); - - this.allProblemItems = []; - this.filteredProblemItems = []; - this.currentPage = 1; - this.itemsPerPage = 5; - this.problemSort = { column: "alpha", direction: "asc" }; - this.problemDescriptionPopover = null; - this.manifestCache = new Map(); - - this.init(); - } - - init() { - if (!this.problemsContainer) return; - - this.initializeProblemItems(); - this.attachEventListeners(); - this.injectPopoverCSS(); - this.attachProblemHoverEvents(); - } - - initializeProblemItems() { - this.allProblemItems = Array.from( - this.problemsContainer.querySelectorAll(".problem-item") || [] - ); - this.filteredProblemItems = this.allProblemItems.map(this.getProblemData); - this.updatePagination(); - } - - getProblemData = (item) => ({ - element: item, - name: item.dataset.name?.toLowerCase() || "", - desc: item.dataset.desc?.toLowerCase() || "", - difficulty: item.dataset.difficulty || "", - }); - - updatePagination() { - const totalPages = Math.ceil(this.filteredProblemItems.length / this.itemsPerPage); - const startIndex = (this.currentPage - 1) * this.itemsPerPage; - const endIndex = startIndex + this.itemsPerPage; - - // Hide all items first - this.allProblemItems.forEach((item) => { - item.style.display = "none"; - }); - - // Show current page items - this.filteredProblemItems.slice(startIndex, endIndex).forEach((item) => { - item.element.style.display = ""; - }); - - // Update pagination controls - if (this.problemsPrevBtn) this.problemsPrevBtn.disabled = this.currentPage <= 1; - if (this.problemsNextBtn) this.problemsNextBtn.disabled = this.currentPage >= totalPages; - - if (this.problemsPaginationInfo) { - this.problemsPaginationInfo.textContent = - totalPages > 0 - ? `Page ${this.currentPage} of ${totalPages}` - : "No problems found"; - } - - this.setupPaginationLayout(); - } - - setupPaginationLayout() { - if (this.problemsPagination) { - Object.assign(this.problemsPagination.style, { - display: "flex", - justifyContent: "center", - position: "absolute", - left: "0", - right: "0", - bottom: "0", - margin: "0 auto", - width: "100%", - background: "inherit", - borderTop: "1px solid var(--border)", - padding: "12px 0" - }); - this.problemsPagination.classList.remove("hidden"); - } - - if (this.problemsContainer?.parentElement) { - Object.assign(this.problemsContainer.parentElement.style, { - position: "relative", - paddingBottom: "56px" - }); - } - } - - showProblemDescription = async (item) => { - this.hideProblemDescription(); - - const folder = item.querySelector('a')?.getAttribute('href')?.split('/').pop(); - if (!folder) return; - - try { - let manifest = this.manifestCache.get(folder); - - if (!manifest) { - // Try localStorage cache first - const cacheKey = `problem_manifest_${folder}`; - const cached = utils.safeLocalStorage.getItem(cacheKey); - - if (cached) { - manifest = JSON.parse(cached); - this.manifestCache.set(folder, manifest); - } else { - // Fetch from API - const response = await fetch(`/api/problem_manifest/${encodeURIComponent(folder)}`); - manifest = response.ok ? await response.json() : { description: 'No description.' }; - - this.manifestCache.set(folder, manifest); - utils.safeLocalStorage.setItem(cacheKey, JSON.stringify(manifest)); - } - } - - this.createPopover(manifest.description || 'No description.', item); - } catch (error) { - console.warn("Failed to load problem description:", error); - this.createPopover('No description available.', item); - } - }; - - createPopover(description, item) { - this.problemDescriptionPopover = document.createElement("div"); - this.problemDescriptionPopover.className = "problem-desc-popover"; - this.problemDescriptionPopover.textContent = description; - document.body.appendChild(this.problemDescriptionPopover); - - const rect = item.getBoundingClientRect(); - Object.assign(this.problemDescriptionPopover.style, { - position: "fixed", - left: `${rect.left + window.scrollX}px`, - top: `${rect.bottom + window.scrollY + 6}px`, - zIndex: "1000", - minWidth: `${rect.width}px` - }); - } - - hideProblemDescription = () => { - if (this.problemDescriptionPopover) { - this.problemDescriptionPopover.remove(); - this.problemDescriptionPopover = null; - } - }; - - attachProblemHoverEvents() { - this.allProblemItems.forEach((item) => { - item.addEventListener("mouseenter", () => this.showProblemDescription(item)); - item.addEventListener("mouseleave", this.hideProblemDescription); - item.addEventListener("mousemove", this.handleMouseMove); - }); - } - - handleMouseMove = utils.throttle((e) => { - if (this.problemDescriptionPopover) { - this.problemDescriptionPopover.style.left = `${e.clientX + 10}px`; - } - }, 16); // ~60fps - - sortProblemItems(column, direction) { - this.filteredProblemItems.sort((a, b) => { - let valueA, valueB; - - switch (column) { - case "alpha": - valueA = a.name; - valueB = b.name; - break; - case "difficulty": - const difficultyOrder = { easy: 1, medium: 2, hard: 3 }; - valueA = difficultyOrder[a.difficulty] || 0; - valueB = difficultyOrder[b.difficulty] || 0; - break; - default: - return 0; - } - - let comparison = 0; - if (typeof valueA === "number" && typeof valueB === "number") { - comparison = valueA - valueB; - } else { - comparison = valueA < valueB ? -1 : valueA > valueB ? 1 : 0; - } - - return direction === "asc" ? comparison : -comparison; - }); - } - - attachEventListeners() { - this.problemsPrevBtn?.addEventListener("click", () => { - if (this.currentPage > 1) { - this.currentPage--; - this.updatePagination(); - } - }); - - this.problemsNextBtn?.addEventListener("click", () => { - const totalPages = Math.ceil(this.filteredProblemItems.length / this.itemsPerPage); - if (this.currentPage < totalPages) { - this.currentPage++; - this.updatePagination(); - } - }); - - this.problemSearch?.addEventListener("input", utils.debounce(() => { - this.filterProblems(); - this.currentPage = 1; - this.updatePagination(); - }, 300)); - - this.difficultyFilter?.addEventListener("change", () => { - this.filterProblems(); - this.currentPage = 1; - this.updatePagination(); - }); - - this.sortProblems?.addEventListener("change", () => { - const value = this.sortProblems.value; - if (value === "alpha" || value === "difficulty") { - if (this.problemSort.column === value) { - this.problemSort.direction = this.problemSort.direction === "asc" ? "desc" : "asc"; - } else { - this.problemSort.column = value; - this.problemSort.direction = "asc"; - } - this.sortProblemItems(this.problemSort.column, this.problemSort.direction); - this.currentPage = 1; - this.updatePagination(); - } - }); - } - - filterProblems() { - const searchTerm = (this.problemSearch?.value || "").toLowerCase().trim(); - const difficulty = this.difficultyFilter?.value || "all"; - - this.filteredProblemItems = this.allProblemItems - .map(this.getProblemData) - .filter(item => { - const matchesSearch = !searchTerm || - item.name.includes(searchTerm) || - item.desc.includes(searchTerm); - - const matchesDifficulty = difficulty === "all" || - item.difficulty === difficulty; - - return matchesSearch && matchesDifficulty; - }); - } - - injectPopoverCSS() { - if (document.getElementById("problem-desc-popover-style")) return; - - const style = document.createElement("style"); - style.id = "problem-desc-popover-style"; - style.textContent = ` - .problem-desc-popover { - background: var(--card, #fff); - color: var(--text, #222); - border: 1px solid var(--border, #e5e7eb); - border-radius: 8px; - box-shadow: 0 4px 16px rgba(16,24,40,0.13); - padding: 12px 16px; - font-size: 0.98rem; - max-width: 350px; - min-width: 180px; - pointer-events: none; - opacity: 0.97; - transition: opacity 0.2s; - word-break: break-word; - } - html.dark .problem-desc-popover { - background: var(--card, #1e293b); - color: var(--text, #f1f5f9); - border: 1px solid var(--border, #334155); - } - `; - document.head.appendChild(style); - } - - destroy() { - // Clean up event listeners and resources - this.hideProblemDescription(); - this.manifestCache.clear(); - } - } - - // Leaderboard Manager - class LeaderboardManager { - constructor() { - this.problemFilter = document.getElementById("problemFilter"); - this.runtimeFilter = document.getElementById("runtimeFilter"); - this.leaderboardBody = document.getElementById("leaderboardBody"); - this.sortableHeaders = document.querySelectorAll(".sortable"); - this.rankInfoBtn = document.getElementById("rankInfoBtn"); - this.rankingExplanation = document.getElementById("rankingExplanation"); - - this.currentSort = { column: "rank", direction: "asc" }; - this.allRows = []; - - this.init(); - } - - init() { - if (!this.leaderboardBody || this.leaderboardBody.children.length === 0) return; - - this.initializeRows(); - this.attachEventListeners(); - this.calculateOverallRanking(); - this.setInitialSortIndicator(); - } - - initializeRows() { - this.allRows = Array.from(this.leaderboardBody.querySelectorAll("tr")).map((row, index) => ({ - element: row, - user: row.dataset.user || "", - problem: row.dataset.problem || "", - runtime: parseFloat(row.dataset.runtime) || 0, - memory: parseFloat(row.dataset.memory) || 0, - timestamp: new Date(row.dataset.timestamp || Date.now()).getTime(), - language: row.dataset.language || "", - originalIndex: index, - })); - } - - updateRankClasses() { - const visibleRows = this.allRows.filter( - (row) => row.element.style.display !== "none" - ); - - visibleRows.forEach((rowData, index) => { - const rank = index + 1; - const row = rowData.element; - - // Update rank cell - const rankCell = row.cells[0]; - if (rankCell) rankCell.textContent = rank; - - // Update rank classes - row.className = row.className.replace(/\brank-\d+\b/g, ""); - if (rank === 1) row.classList.add("rank-1"); - else if (rank <= 3) row.classList.add("rank-top3"); - }); - } - - calculateOverallRanking() { - const visibleRows = this.allRows.filter( - (row) => row.element.style.display !== "none" - ); - - if (visibleRows.length === 0) return; - - // Group submissions by problem to find the best performance for each - const problemBests = {}; - - visibleRows.forEach((rowData) => { - const problem = rowData.problem; - if (!problemBests[problem]) { - problemBests[problem] = { - bestRuntime: Infinity, - bestMemory: Infinity, - }; - } - - problemBests[problem].bestRuntime = Math.min( - problemBests[problem].bestRuntime, - rowData.runtime - ); - problemBests[problem].bestMemory = Math.min( - problemBests[problem].bestMemory, - rowData.memory - ); - }); - - // Calculate normalized scores for each submission - visibleRows.forEach((rowData) => { - const problemBest = problemBests[rowData.problem]; - - const runtimeScore = problemBest.bestRuntime > 0 - ? rowData.runtime / problemBest.bestRuntime - : 1; - const memoryScore = problemBest.bestMemory > 0 - ? rowData.memory / problemBest.bestMemory - : 1; - - // Weighted overall score (70% runtime, 30% memory) - rowData.overallScore = runtimeScore * 0.7 + memoryScore * 0.3; - }); - - // Sort by overall score (lower is better), then by timestamp (earlier is better for ties) - visibleRows.sort((a, b) => { - const scoreDiff = a.overallScore - b.overallScore; - if (Math.abs(scoreDiff) > 0.000001) return scoreDiff; - return a.timestamp - b.timestamp; - }); - - // Reorder DOM elements and update ranks - const fragment = document.createDocumentFragment(); - visibleRows.forEach((rowData) => { - fragment.appendChild(rowData.element); - }); - this.leaderboardBody.appendChild(fragment); - - this.updateRankClasses(); - } - - filterLeaderboard() { - const problemTerm = (this.problemFilter?.value || "").toLowerCase().trim(); - const runtimeType = this.runtimeFilter?.value || "all"; - - // Reset all rows to visible first - this.allRows.forEach((rowData) => { - rowData.element.style.display = ""; - }); - - // Apply problem filter - if (problemTerm) { - this.allRows.forEach((rowData) => { - const problemMatch = rowData.problem.toLowerCase().includes(problemTerm); - if (!problemMatch) { - rowData.element.style.display = "none"; - } - }); - } - - // Apply runtime filter (best/worst per user per problem) - if (runtimeType === "best" || runtimeType === "worst") { - const userProblemGroups = {}; - - // Group by user + problem combination - this.allRows.forEach((rowData) => { - if (rowData.element.style.display === "none") return; - - const key = `${rowData.user}::${rowData.problem}`; - if (!userProblemGroups[key]) { - userProblemGroups[key] = []; - } - userProblemGroups[key].push(rowData); - }); - - // Hide all except best/worst for each user-problem combination - Object.values(userProblemGroups).forEach((group) => { - if (group.length <= 1) return; - - group.sort((a, b) => a.runtime - b.runtime); - - const keepIndex = runtimeType === "best" ? 0 : group.length - 1; - group.forEach((rowData, index) => { - if (index !== keepIndex) { - rowData.element.style.display = "none"; - } - }); - }); - } - - this.calculateOverallRanking(); - } - - getCellValue(rowData, column) { - switch (column) { - case "rank": - return parseInt(rowData.element.cells[0]?.textContent) || 0; - case "user": - return rowData.user.toLowerCase(); - case "problem": - return rowData.problem.toLowerCase(); - case "runtime": - return rowData.runtime; - case "memory": - return rowData.memory; - case "timestamp": - return rowData.timestamp; - case "language": - return rowData.language.toLowerCase(); - default: - return ""; - } - } - - sortLeaderboard(column, direction) { - if (column === "rank") { - this.calculateOverallRanking(); - return; - } - - const visibleRows = this.allRows.filter( - (row) => row.element.style.display !== "none" - ); - - visibleRows.sort((a, b) => { - const valueA = this.getCellValue(a, column); - const valueB = this.getCellValue(b, column); - - let comparison = 0; - if (typeof valueA === "number" && typeof valueB === "number") { - comparison = valueA - valueB; - } else { - comparison = valueA < valueB ? -1 : valueA > valueB ? 1 : 0; - } - - return direction === "asc" ? comparison : -comparison; - }); - - // Reorder DOM elements using document fragment for better performance - const fragment = document.createDocumentFragment(); - visibleRows.forEach((rowData) => { - fragment.appendChild(rowData.element); - }); - this.leaderboardBody.appendChild(fragment); - - this.updateRankClasses(); - } - - attachEventListeners() { - // Sorting event listeners - this.sortableHeaders.forEach((header) => { - header.addEventListener("click", () => { - const column = header.dataset.sort; - if (!column) return; - - // Remove sorting classes from all headers - this.sortableHeaders.forEach((h) => - h.classList.remove("sort-asc", "sort-desc") - ); - - // Toggle sort direction - if (this.currentSort.column === column) { - this.currentSort.direction = this.currentSort.direction === "asc" ? "desc" : "asc"; - } else { - this.currentSort.column = column; - this.currentSort.direction = "asc"; - } - - // Add sorting class to current header - header.classList.add(`sort-${this.currentSort.direction}`); - - this.sortLeaderboard(column, this.currentSort.direction); - }); - }); - - // Filter event listeners with debouncing - this.problemFilter?.addEventListener("input", - utils.debounce(() => this.filterLeaderboard(), 300) - ); - this.runtimeFilter?.addEventListener("change", () => this.filterLeaderboard()); - - // Rank info popout - this.rankInfoBtn?.addEventListener("click", (e) => { - e.preventDefault(); - this.rankingExplanation?.classList.toggle("active"); - this.rankInfoBtn?.classList.toggle("active"); - }); - - // Close ranking explanation when clicking outside - document.addEventListener("click", (e) => { - if ( - this.rankingExplanation?.classList.contains("active") && - !this.rankingExplanation.contains(e.target) && - !this.rankInfoBtn?.contains(e.target) - ) { - this.rankingExplanation.classList.remove("active"); - this.rankInfoBtn?.classList.remove("active"); - } - }); - } - - setInitialSortIndicator() { - const defaultHeader = document.querySelector('[data-sort="rank"]'); - if (defaultHeader) { - defaultHeader.classList.add("sort-asc"); - } - } - } - - // Initialize all managers - const darkModeManager = new DarkModeManager(); - const problemManager = new ProblemManager(); - const leaderboardManager = new LeaderboardManager(); - - // Apply dark mode to dynamically created elements - const applyDarkModeToElements = () => { - // Any additional dark mode styling for dynamically created elements can go here - }; - - // Watch for dark mode changes - const darkModeObserver = new MutationObserver(applyDarkModeToElements); - darkModeObserver.observe(document.documentElement, { - attributes: true, - attributeFilter: ["class"], - }); - - // Cleanup on page unload - window.addEventListener("beforeunload", () => { - problemManager.destroy(); - darkModeObserver.disconnect(); - }); +/** + * + * This is the stupid fucking JavaScript, i hate this so fucking much + * why the fuck does this need to exsits, idk. + * + * CHANGELOG: + * aug18@21:51-> pagination for leaderboard ; and some shit refactoring. + */ +document.addEventListener("DOMContentLoaded", () => { + "use strict"; + + // Utility functions + const utils = { + safeLocalStorage: { + getItem(key) { + try { + return localStorage.getItem(key); + } catch (e) { + console.warn("localStorage.getItem failed:", e); + return null; + } + }, + setItem(key, value) { + try { + localStorage.setItem(key, value); + return true; + } catch (e) { + console.warn("localStorage.setItem failed:", e); + return false; + } + } + }, + + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func.apply(this, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + throttle(func, limit) { + let inThrottle; + return function executedFunction(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + } + }; + + // Dark Mode Manager + class DarkModeManager { + constructor() { + this.darkModeToggle = document.getElementById("darkModeToggle"); + this.html = document.documentElement; + this.init(); + } + + init() { + this.loadSavedPreference(); + this.attachEventListeners(); + } + + loadSavedPreference() { + const savedDarkMode = utils.safeLocalStorage.getItem("darkMode"); + if ( + savedDarkMode === "true" || + (savedDarkMode === null && + window.matchMedia("(prefers-color-scheme: dark)").matches) + ) { + this.html.classList.add("dark"); + } + } + + attachEventListeners() { + this.darkModeToggle?.addEventListener("click", () => { + this.html.classList.toggle("dark"); + utils.safeLocalStorage.setItem("darkMode", this.html.classList.contains("dark")); + }); + } + } + + // Problem Manager + class ProblemManager { + constructor() { + this.problemSearch = document.getElementById("problemSearch"); + this.problemsContainer = document.getElementById("problemsContainer"); + this.problemsPagination = document.getElementById("problemsPagination"); + this.problemsPrevBtn = document.getElementById("problemsPrevBtn"); + this.problemsNextBtn = document.getElementById("problemsNextBtn"); + this.problemsPaginationInfo = document.getElementById("problemsPaginationInfo"); + this.difficultyFilter = document.getElementById("difficultyFilter"); + this.sortProblems = document.getElementById("sortProblems"); + + this.allProblemItems = []; + this.filteredProblemItems = []; + this.currentPage = 1; + this.itemsPerPage = 5; + this.problemSort = { column: "alpha", direction: "asc" }; + this.problemDescriptionPopover = null; + this.manifestCache = new Map(); + + this.init(); + } + + init() { + if (!this.problemsContainer) return; + + this.initializeProblemItems(); + this.attachEventListeners(); + this.injectPopoverCSS(); + this.attachProblemHoverEvents(); + } + + initializeProblemItems() { + this.allProblemItems = Array.from( + this.problemsContainer.querySelectorAll(".problem-item") || [] + ); + this.filteredProblemItems = this.allProblemItems.map(this.getProblemData); + this.updatePagination(); + } + + getProblemData = (item) => ({ + element: item, + name: item.dataset.name?.toLowerCase() || "", + desc: item.dataset.desc?.toLowerCase() || "", + difficulty: item.dataset.difficulty || "", + }); + + updatePagination() { + const totalPages = Math.ceil(this.filteredProblemItems.length / this.itemsPerPage); + const startIndex = (this.currentPage - 1) * this.itemsPerPage; + const endIndex = startIndex + this.itemsPerPage; + + // Hide all items first + this.allProblemItems.forEach((item) => { + item.style.display = "none"; + }); + + // Show current page items + this.filteredProblemItems.slice(startIndex, endIndex).forEach((item) => { + item.element.style.display = ""; + }); + + // Update pagination controls + if (this.problemsPrevBtn) this.problemsPrevBtn.disabled = this.currentPage <= 1; + if (this.problemsNextBtn) this.problemsNextBtn.disabled = this.currentPage >= totalPages; + + if (this.problemsPaginationInfo) { + this.problemsPaginationInfo.textContent = + totalPages > 0 + ? `Page ${this.currentPage} of ${totalPages}` + : "No problems found"; + } + + this.setupPaginationLayout(); + } + + setupPaginationLayout() { + if (this.problemsPagination) { + Object.assign(this.problemsPagination.style, { + display: "flex", + justifyContent: "center", + position: "absolute", + left: "0", + right: "0", + bottom: "0", + margin: "0 auto", + width: "100%", + background: "inherit", + borderTop: "1px solid var(--border)", + padding: "12px 0" + }); + // Style the pagination buttons and info text + const prevBtn = this.problemsPagination.querySelector('#problemsPrevBtn'); + const nextBtn = this.problemsPagination.querySelector('#problemsNextBtn'); + const infoText = this.problemsPagination.querySelector('#problemsPaginationInfo'); + if (prevBtn) prevBtn.style.marginRight = '10px'; + if (nextBtn) nextBtn.style.marginLeft = '10px'; + if (infoText) infoText.style.marginTop = '2px'; + this.problemsPagination.classList.remove("hidden"); + } + + if (this.problemsContainer?.parentElement) { + Object.assign(this.problemsContainer.parentElement.style, { + position: "relative", + paddingBottom: "56px" + }); + } + } + + showProblemDescription = async (item) => { + this.hideProblemDescription(); + + const folder = item.querySelector('a')?.getAttribute('href')?.split('/').pop(); + if (!folder) return; + + try { + let manifest = this.manifestCache.get(folder); + + if (!manifest) { + // Try localStorage cache first + const cacheKey = `problem_manifest_${folder}`; + const cached = utils.safeLocalStorage.getItem(cacheKey); + + if (cached) { + manifest = JSON.parse(cached); + this.manifestCache.set(folder, manifest); + } else { + // Fetch from API + const response = await fetch(`/api/problem_manifest/${encodeURIComponent(folder)}`); + manifest = response.ok ? await response.json() : { description: 'No description.' }; + + this.manifestCache.set(folder, manifest); + utils.safeLocalStorage.setItem(cacheKey, JSON.stringify(manifest)); + } + } + + this.createPopover(manifest.description || 'No description.', item); + } catch (error) { + console.warn("Failed to load problem description:", error); + this.createPopover('No description available.', item); + } + }; + + createPopover(description, item) { + this.problemDescriptionPopover = document.createElement("div"); + this.problemDescriptionPopover.className = "problem-desc-popover"; + this.problemDescriptionPopover.textContent = description; + document.body.appendChild(this.problemDescriptionPopover); + + const rect = item.getBoundingClientRect(); + Object.assign(this.problemDescriptionPopover.style, { + position: "fixed", + left: `${rect.left + window.scrollX}px`, + top: `${rect.bottom + window.scrollY + 6}px`, + zIndex: "1000", + minWidth: `${rect.width}px` + }); + } + + hideProblemDescription = () => { + if (this.problemDescriptionPopover) { + this.problemDescriptionPopover.remove(); + this.problemDescriptionPopover = null; + } + }; + + attachProblemHoverEvents() { + this.allProblemItems.forEach((item) => { + item.addEventListener("mouseenter", () => this.showProblemDescription(item)); + item.addEventListener("mouseleave", this.hideProblemDescription); + item.addEventListener("mousemove", this.handleMouseMove); + }); + } + + handleMouseMove = utils.throttle((e) => { + if (this.problemDescriptionPopover) { + this.problemDescriptionPopover.style.left = `${e.clientX + 10}px`; + } + }, 16); // ~60fps + + sortProblemItems(column, direction) { + this.filteredProblemItems.sort((a, b) => { + let valueA, valueB; + + switch (column) { + case "alpha": + valueA = a.name; + valueB = b.name; + break; + case "difficulty": + const difficultyOrder = { easy: 1, medium: 2, hard: 3 }; + valueA = difficultyOrder[a.difficulty] || 0; + valueB = difficultyOrder[b.difficulty] || 0; + break; + default: + return 0; + } + + let comparison = 0; + if (typeof valueA === "number" && typeof valueB === "number") { + comparison = valueA - valueB; + } else { + comparison = valueA < valueB ? -1 : valueA > valueB ? 1 : 0; + } + + return direction === "asc" ? comparison : -comparison; + }); + } + + attachEventListeners() { + this.problemsPrevBtn?.addEventListener("click", () => { + if (this.currentPage > 1) { + this.currentPage--; + this.updatePagination(); + } + }); + + this.problemsNextBtn?.addEventListener("click", () => { + const totalPages = Math.ceil(this.filteredProblemItems.length / this.itemsPerPage); + if (this.currentPage < totalPages) { + this.currentPage++; + this.updatePagination(); + } + }); + + this.problemSearch?.addEventListener("input", utils.debounce(() => { + this.filterProblems(); + this.currentPage = 1; + this.updatePagination(); + }, 300)); + + this.difficultyFilter?.addEventListener("change", () => { + this.filterProblems(); + this.currentPage = 1; + this.updatePagination(); + }); + + this.sortProblems?.addEventListener("change", () => { + const value = this.sortProblems.value; + if (value === "alpha" || value === "difficulty") { + if (this.problemSort.column === value) { + this.problemSort.direction = this.problemSort.direction === "asc" ? "desc" : "asc"; + } else { + this.problemSort.column = value; + this.problemSort.direction = "asc"; + } + this.sortProblemItems(this.problemSort.column, this.problemSort.direction); + this.currentPage = 1; + this.updatePagination(); + } + }); + } + + filterProblems() { + const searchTerm = (this.problemSearch?.value || "").toLowerCase().trim(); + const difficulty = this.difficultyFilter?.value || "all"; + + this.filteredProblemItems = this.allProblemItems + .map(this.getProblemData) + .filter(item => { + const matchesSearch = !searchTerm || + item.name.includes(searchTerm) || + item.desc.includes(searchTerm); + + const matchesDifficulty = difficulty === "all" || + item.difficulty === difficulty; + + return matchesSearch && matchesDifficulty; + }); + } + + injectPopoverCSS() { + if (document.getElementById("problem-desc-popover-style")) return; + + const style = document.createElement("style"); + style.id = "problem-desc-popover-style"; + style.textContent = ` + .problem-desc-popover { + background: var(--card, #fff); + color: var(--text, #222); + border: 1px solid var(--border, #e5e7eb); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(16,24,40,0.13); + padding: 12px 16px; + font-size: 0.98rem; + max-width: 350px; + min-width: 180px; + pointer-events: none; + opacity: 0.97; + transition: opacity 0.2s; + word-break: break-word; + } + html.dark .problem-desc-popover { + background: var(--card, #1e293b); + color: var(--text, #f1f5f9); + border: 1px solid var(--border, #334155); + } + `; + document.head.appendChild(style); + } + + destroy() { + // Clean up event listeners and resources + this.hideProblemDescription(); + this.manifestCache.clear(); + } + } + + // Leaderboard Manager + class LeaderboardManager { + constructor() { + this.problemFilter = document.getElementById("problemFilter"); + this.runtimeFilter = document.getElementById("runtimeFilter"); + this.leaderboardBody = document.getElementById("leaderboardBody"); + this.sortableHeaders = document.querySelectorAll(".sortable"); + this.rankInfoBtn = document.getElementById("rankInfoBtn"); + this.rankingExplanation = document.getElementById("rankingExplanation"); + + this.currentSort = { column: "rank", direction: "asc" }; + this.allRows = []; + this.filteredRows = []; + this.currentPage = 1; + this.itemsPerPage = 5; + this.leaderboardPagination = document.createElement("div"); + this.leaderboardPagination.className = "pagination-controls"; + this.leaderboardPagination.style.display = "flex"; + this.leaderboardPagination.style.justifyContent = "center"; + this.leaderboardPagination.style.position = "absolute"; + this.leaderboardPagination.style.left = 0; + this.leaderboardPagination.style.right = 0; + this.leaderboardPagination.style.bottom = 0; + this.leaderboardPagination.style.margin = "0 auto 0 auto"; + this.leaderboardPagination.style.width = "100%"; + this.leaderboardPagination.style.background = "inherit"; + this.leaderboardPagination.style.borderTop = "1px solid var(--border)"; + this.leaderboardPagination.style.padding = "12px 0"; + this.leaderboardPagination.innerHTML = ` + + Page 1 of 1 + + `; + this.leaderboardPrevBtn = this.leaderboardPagination.querySelector("#leaderboardPrevBtn"); + this.leaderboardNextBtn = this.leaderboardPagination.querySelector("#leaderboardNextBtn"); + this.leaderboardPaginationInfo = this.leaderboardPagination.querySelector("#leaderboardPaginationInfo"); + this.init(); + } + + init() { + if (!this.leaderboardBody || this.leaderboardBody.children.length === 0) return; + this.initializeRows(); + this.attachEventListeners(); + this.filterLeaderboard(); + this.setInitialSortIndicator(); + // Insert pagination controls after leaderboard table + const leaderboardContainer = document.getElementById("leaderboardContainer"); + if (leaderboardContainer && !leaderboardContainer.contains(this.leaderboardPagination)) { + leaderboardContainer.appendChild(this.leaderboardPagination); + // Ensure parent card is relatively positioned and has enough bottom padding + const leaderboardCard = leaderboardContainer.closest('.card'); + if (leaderboardCard) { + leaderboardCard.style.position = "relative"; + leaderboardCard.style.paddingBottom = "56px"; + } + } + // Also ensure the parent card (section.card) contains the controls for correct layout + const leaderboardCard = leaderboardContainer?.closest('.card'); + if (leaderboardCard && !leaderboardCard.contains(this.leaderboardPagination)) { + leaderboardCard.appendChild(this.leaderboardPagination); + } + } + + initializeRows() { + this.allRows = Array.from(this.leaderboardBody.querySelectorAll("tr")).map((row, index) => ({ + element: row, + user: row.dataset.user || "", + problem: row.dataset.problem || "", + runtime: parseFloat(row.dataset.runtime) || 0, + memory: parseFloat(row.dataset.memory) || 0, + timestamp: new Date(row.dataset.timestamp || Date.now()).getTime(), + language: row.dataset.language || "", + originalIndex: index, + })); + } + + filterLeaderboard() { + const problemTerm = (this.problemFilter?.value || "").toLowerCase().trim(); + const runtimeType = this.runtimeFilter?.value || "all"; + // Filter rows + this.filteredRows = this.allRows.filter((rowData) => { + let visible = true; + if (problemTerm) { + visible = rowData.problem.toLowerCase().includes(problemTerm); + } + return visible; + }); + // Apply runtime filter (best/worst per user per problem) + if (runtimeType === "best" || runtimeType === "worst") { + const userProblemGroups = {}; + this.filteredRows.forEach((rowData) => { + const key = `${rowData.user}::${rowData.problem}`; + if (!userProblemGroups[key]) userProblemGroups[key] = []; + userProblemGroups[key].push(rowData); + }); + this.filteredRows = Object.values(userProblemGroups).flatMap((group) => { + if (group.length <= 1) return group; + group.sort((a, b) => a.runtime - b.runtime); + const keepIndex = runtimeType === "best" ? 0 : group.length - 1; + return [group[keepIndex]]; + }); + } + this.currentPage = 1; + this.updateLeaderboardPagination(); + } + + updateLeaderboardPagination() { + const totalPages = Math.ceil(this.filteredRows.length / this.itemsPerPage) || 1; + if (this.currentPage > totalPages) this.currentPage = totalPages; + const startIndex = (this.currentPage - 1) * this.itemsPerPage; + const endIndex = startIndex + this.itemsPerPage; + // Hide all rows first + this.allRows.forEach((rowData) => { + rowData.element.style.display = "none"; + }); + // Show only current page rows + this.filteredRows.slice(startIndex, endIndex).forEach((rowData) => { + rowData.element.style.display = ""; + }); + // Update pagination controls + if (this.leaderboardPrevBtn) this.leaderboardPrevBtn.disabled = this.currentPage <= 1; + if (this.leaderboardNextBtn) this.leaderboardNextBtn.disabled = this.currentPage >= totalPages; + if (this.leaderboardPaginationInfo) { + this.leaderboardPaginationInfo.textContent = + totalPages > 0 ? `Page ${this.currentPage} of ${totalPages}` : "No entries found"; + } + // Always show and center pagination at the bottom of the leaderboard card + if (this.leaderboardPagination) { + this.leaderboardPagination.classList.remove("hidden"); + this.leaderboardPagination.style.display = "flex"; + this.leaderboardPagination.style.justifyContent = "center"; + this.leaderboardPagination.style.position = "absolute"; + this.leaderboardPagination.style.left = 0; + this.leaderboardPagination.style.right = 0; + this.leaderboardPagination.style.bottom = 0; + this.leaderboardPagination.style.margin = "0 auto 0 auto"; + this.leaderboardPagination.style.width = "100%"; + this.leaderboardPagination.style.background = "inherit"; + this.leaderboardPagination.style.borderTop = "1px solid var(--border)"; + this.leaderboardPagination.style.padding = "12px 0"; + } + // Make sure the parent leaderboard card is relatively positioned + const leaderboardContainer = document.getElementById("leaderboardContainer"); + if (leaderboardContainer && leaderboardContainer.parentElement) { + leaderboardContainer.parentElement.style.position = "relative"; + leaderboardContainer.parentElement.style.paddingBottom = "56px"; + } + // Recalculate ranks for visible rows + this.calculateOverallRanking(); + } + + calculateOverallRanking() { + // Only consider visible rows (current page) + const visibleRows = this.filteredRows.slice( + (this.currentPage - 1) * this.itemsPerPage, + (this.currentPage - 1) * this.itemsPerPage + this.itemsPerPage + ); + if (visibleRows.length === 0) return; + // Group submissions by problem to find the best performance for each + const problemBests = {}; + visibleRows.forEach((rowData) => { + const problem = rowData.problem; + if (!problemBests[problem]) { + problemBests[problem] = { + bestRuntime: Infinity, + bestMemory: Infinity, + }; + } + problemBests[problem].bestRuntime = Math.min( + problemBests[problem].bestRuntime, + rowData.runtime + ); + problemBests[problem].bestMemory = Math.min( + problemBests[problem].bestMemory, + rowData.memory + ); + }); + // Calculate normalized scores for each submission + visibleRows.forEach((rowData) => { + const problemBest = problemBests[rowData.problem]; + const runtimeScore = + problemBest.bestRuntime > 0 + ? rowData.runtime / problemBest.bestRuntime + : 1; + const memoryScore = + problemBest.bestMemory > 0 + ? rowData.memory / problemBest.bestMemory + : 1; + rowData.overallScore = runtimeScore * 0.7 + memoryScore * 0.3; + }); + // Sort by overall score (lower is better), then by timestamp (earlier is better for ties) + visibleRows.sort((a, b) => { + const scoreDiff = a.overallScore - b.overallScore; + if (Math.abs(scoreDiff) > 0.000001) return scoreDiff; + return a.timestamp - b.timestamp; + }); + // Reorder DOM elements and update ranks + const fragment = document.createDocumentFragment(); + visibleRows.forEach((rowData, index) => { + fragment.appendChild(rowData.element); + // Update rank cell + const rankCell = rowData.element.cells[0]; + if (rankCell) rankCell.textContent = index + 1 + (this.currentPage - 1) * this.itemsPerPage; + // Update rank classes + rowData.element.className = rowData.element.className.replace(/\brank-\d+\b/g, ""); + if (index === 0) rowData.element.classList.add("rank-1"); + else if (index < 3) rowData.element.classList.add("rank-top3"); + }); + this.leaderboardBody.appendChild(fragment); + // this.updateRankClasses(); // Function does not exist, so remove this call + } + + attachEventListeners() { + // Sorting event listeners + this.sortableHeaders.forEach((header) => { + header.addEventListener("click", () => { + const column = header.dataset.sort; + if (!column) return; + // Remove sorting classes from all headers + this.sortableHeaders.forEach((h) => h.classList.remove("sort-asc", "sort-desc")); + // Toggle sort direction + if (this.currentSort.column === column) { + this.currentSort.direction = this.currentSort.direction === "asc" ? "desc" : "asc"; + } else { + this.currentSort.column = column; + this.currentSort.direction = "asc"; + } + // Add sorting class to current header + header.classList.add(`sort-${this.currentSort.direction}`); + // Sort filteredRows + this.filteredRows.sort((a, b) => { + let valueA = a[column]; + let valueB = b[column]; + if (typeof valueA === "string") valueA = valueA.toLowerCase(); + if (typeof valueB === "string") valueB = valueB.toLowerCase(); + let comparison = 0; + if (typeof valueA === "number" && typeof valueB === "number") { + comparison = valueA - valueB; + } else { + comparison = valueA < valueB ? -1 : valueA > valueB ? 1 : 0; + } + return this.currentSort.direction === "asc" ? comparison : -comparison; + }); + this.currentPage = 1; + this.updateLeaderboardPagination(); + }); + }); + + // Filter event listeners with debouncing + this.problemFilter?.addEventListener("input", utils.debounce(() => { + this.filterLeaderboard(); + }, 300)); + this.runtimeFilter?.addEventListener("change", () => this.filterLeaderboard()); + + // Pagination event listeners + this.leaderboardPrevBtn?.addEventListener("click", () => { + if (this.currentPage > 1) { + this.currentPage--; + this.updateLeaderboardPagination(); + } + }); + this.leaderboardNextBtn?.addEventListener("click", () => { + const totalPages = Math.ceil(this.filteredRows.length / this.itemsPerPage) || 1; + if (this.currentPage < totalPages) { + this.currentPage++; + this.updateLeaderboardPagination(); + } + }); + + // Rank info popout + this.rankInfoBtn?.addEventListener("click", (e) => { + e.preventDefault(); + this.rankingExplanation?.classList.toggle("active"); + this.rankInfoBtn?.classList.toggle("active"); + }); + + // Close ranking explanation when clicking outside + document.addEventListener("click", (e) => { + if ( + this.rankingExplanation?.classList.contains("active") && + !this.rankingExplanation.contains(e.target) && + !this.rankInfoBtn?.contains(e.target) + ) { + this.rankingExplanation.classList.remove("active"); + this.rankInfoBtn?.classList.remove("active"); + } + }); + } + + setInitialSortIndicator() { + const defaultHeader = document.querySelector('[data-sort="rank"]'); + if (defaultHeader) { + defaultHeader.classList.add("sort-asc"); + } + } + } + + // Initialize all managers + const darkModeManager = new DarkModeManager(); + const problemManager = new ProblemManager(); + const leaderboardManager = new LeaderboardManager(); + + // Apply dark mode to dynamically created elements + const applyDarkModeToElements = () => { + // Any additional dark mode styling for dynamically created elements can go here + }; + + // Watch for dark mode changes + const darkModeObserver = new MutationObserver(applyDarkModeToElements); + darkModeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + + // Cleanup on page unload + window.addEventListener("beforeunload", () => { + problemManager.destroy(); + darkModeObserver.disconnect(); + }); }); \ No newline at end of file diff --git a/src/app.py b/src/app.py index 2979d2c..2c9a593 100644 --- a/src/app.py +++ b/src/app.py @@ -45,9 +45,14 @@ def api_problem_manifest(folder): except Exception as e: return jsonify({'error': str(e)}), 500 +# I introduce you to the fucking JavaScript shit routes, fuck javascripts +@app.route('/JavaScript/') +def serve_js(filename): + return send_from_directory('JavaScript', filename) + @app.route("/script.js") def script(): - return send_from_directory("templates", "script.js") + return send_from_directory("JavaScript", "script.js") @app.route('/favicon.ico') def favicon(): diff --git a/src/templates/index.html b/src/templates/index.html index 744137b..c71cdf6 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -69,7 +69,7 @@

Leaderboard - ℹ️ +

@@ -141,6 +141,6 @@
- +