From f3baec17e4850d7f5849f540853dcb7d6ee5ec7c Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Sun, 17 Aug 2025 18:58:12 +0200 Subject: [PATCH] new problem and i added better support for sorting and a overall better script --- src/app.py | 17 +- src/problems/PrimeNumber/description.md | 90 ++ src/problems/PrimeNumber/manifest.json | 7 + src/problems/PrimeNumber/test.py | 33 + src/problems/ReversedList/description.md | 4 +- .../sortlist/{manifets.json => manifest.json} | 0 src/static/favicon.ico | Bin 15406 -> 9662 bytes src/static/script.js | 971 ++++++++++++------ 8 files changed, 778 insertions(+), 344 deletions(-) create mode 100644 src/problems/PrimeNumber/description.md create mode 100644 src/problems/PrimeNumber/manifest.json create mode 100644 src/problems/PrimeNumber/test.py rename src/problems/sortlist/{manifets.json => manifest.json} (100%) diff --git a/src/app.py b/src/app.py index 4983e22..2979d2c 100644 --- a/src/app.py +++ b/src/app.py @@ -1,5 +1,6 @@ +# API endpoint to get problem manifest (description) by folder from markupsafe import Markup -from flask import Flask, render_template, request, redirect, url_for, send_from_directory +from flask import Flask, render_template, request, redirect, url_for, send_from_directory, jsonify import markdown as md import ast from src.models import db, Problem, Solution @@ -30,6 +31,20 @@ def setup(): # Start the background thread to scan problems start_problem_scanner() +@app.route('/api/problem_manifest/') +def api_problem_manifest(folder): + # Try to load manifest.json from the problem folder + import json + manifest_path = BASE_DIR / 'problems' / folder / 'manifest.json' + if not manifest_path.exists(): + return jsonify({'error': 'Manifest not found'}), 404 + try: + with open(manifest_path, 'r', encoding='utf-8') as f: + manifest = json.load(f) + return jsonify(manifest) + except Exception as e: + return jsonify({'error': str(e)}), 500 + @app.route("/script.js") def script(): return send_from_directory("templates", "script.js") diff --git a/src/problems/PrimeNumber/description.md b/src/problems/PrimeNumber/description.md new file mode 100644 index 0000000..d0e46ea --- /dev/null +++ b/src/problems/PrimeNumber/description.md @@ -0,0 +1,90 @@ +# Prime Number Function Checker + +You are asked to **write a function** that checks if a number is a **prime number**. + +### What is a Prime Number? + +* A **prime number** is a whole number greater than `1`. +* It has only **two divisors**: `1` and the number itself. +* Example: + + * `7` → Prime (divisible only by `1` and `7`) + * `8` → Not Prime (divisible by `1, 2, 4, 8`) + +Numbers less than or equal to `1` are **not prime**. + +📖 More info: [Wikipedia](https://en.wikipedia.org/wiki/Prime_number) + +--- + +### Function Signature + +```python +def check_prime(number: int) -> bool: +``` + +* **Input**: + + * `number` → an integer + +* **Output**: + + * `True` → if the number is prime + * `False` → if the number is not prime + +--- + +### Example 1 + +**Input:** + +```python +check_prime(2) +``` + +**Output:** + +``` +True +``` + +--- + +### Example 2 + +**Input:** + +```python +check_prime(4) +``` + +**Output:** + +``` +False +``` + +--- + +### Example 3 + +**Input:** + +```python +check_prime(13) +``` + +**Output:** + +``` +True +``` + +--- + +**_Dont worry you do NOT need to write these Function Calls into your solution. QPP checks automatically_** + +### Hint + +Try using the **modulo operator `%`** to check if one number divides evenly into another. +If any number between `2` and `n-1` divides your number evenly, then it’s **not prime**. diff --git a/src/problems/PrimeNumber/manifest.json b/src/problems/PrimeNumber/manifest.json new file mode 100644 index 0000000..1e0d434 --- /dev/null +++ b/src/problems/PrimeNumber/manifest.json @@ -0,0 +1,7 @@ +{ + "title": "Prime Number Checker", + "description": "Determine if a given number is a prime number", + "description_md": "problems/PrimeNumber/description.md", + "test_code": "problems/PrimeNumber/test.py", + "difficulty": "medium" +} \ No newline at end of file diff --git a/src/problems/PrimeNumber/test.py b/src/problems/PrimeNumber/test.py new file mode 100644 index 0000000..a59adde --- /dev/null +++ b/src/problems/PrimeNumber/test.py @@ -0,0 +1,33 @@ +import unittest + + +# +# def check_prime(number : int) -> bool: + # for i in range(2, int(number)): + # if int(number) % i == 0: + # return False + # return True + +class TestPrimeNumber(unittest.TestCase): + def test_prime_function(self): + test_cases = [ + (2,True), + (3,True), + (4,False), + (6,False), + (1,False) + ] + print("\nFUNCTION OUTPUT TEST RESULTS") + + for input_val, expected in test_cases: + try: + actual = check_prime(input_val) + status = "✓ PASS" if actual == expected else "✗ FAIL" + print(f"{status} | Input: '{input_val}' -> Got: {actual} | Expected: {expected}") + self.assertEqual(actual, expected) + except Exception as e: + print(f"✗ ERROR | Input: '{input_val}' -> Exception: {e}") + raise + +if __name__ == "__main__": + unittest.main(verbosity=2) \ No newline at end of file diff --git a/src/problems/ReversedList/description.md b/src/problems/ReversedList/description.md index e701145..3f8a55b 100644 --- a/src/problems/ReversedList/description.md +++ b/src/problems/ReversedList/description.md @@ -1,13 +1,13 @@ ## Reverse a List Write a function called `reverse_list` that takes a list as input and returns the list in reverse order. -You are **not allowed** to just use Python’s built-in `.reverse()` method or slicing (`[::-1]`) — try to reverse it manually for practice. +You are **allowed** to just use Python’s built-in `.reverse()` method or slicing (`[::-1]`), try to reverse it manually for practice. ### Function Signature: ```python def reverse_list(lst): - # your code here + # your code here ``` ### Requirements diff --git a/src/problems/sortlist/manifets.json b/src/problems/sortlist/manifest.json similarity index 100% rename from src/problems/sortlist/manifets.json rename to src/problems/sortlist/manifest.json diff --git a/src/static/favicon.ico b/src/static/favicon.ico index 1e4fe17ea0af76ab93ce1006529573e20b2fd08c..c554e2de0dc896f48909899cbdc43c9e22f91d12 100644 GIT binary patch literal 9662 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 MJ<#oe|3we{3+%%LO#lD@ diff --git a/src/static/script.js b/src/static/script.js index 38d271f..47e7c17 100644 --- a/src/static/script.js +++ b/src/static/script.js @@ -1,126 +1,412 @@ document.addEventListener("DOMContentLoaded", () => { - // Dark mode functionality - const darkModeToggle = document.getElementById("darkModeToggle"); - const html = document.documentElement; + "use strict"; - // Load saved dark mode preference - const savedDarkMode = localStorage.getItem("darkMode"); - if ( - savedDarkMode === "true" || - (savedDarkMode === null && - // detect if the user already has a dark mode enabled in the system settings ( works for all systems ) - window.matchMedia("(prefers-color-scheme: dark)").matches) - ) { - html.classList.add("dark"); + // 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")); + }); + } } - darkModeToggle?.addEventListener("click", () => { - html.classList.toggle("dark"); - localStorage.setItem("darkMode", 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"); - // Problem search and pagination - const problemSearch = document.getElementById("problemSearch"); - const problemsContainer = document.getElementById("problemsContainer"); - const problemsPagination = document.getElementById("problemsPagination"); - const problemsPrevBtn = document.getElementById("problemsPrevBtn"); - const problemsNextBtn = document.getElementById("problemsNextBtn"); - const problemsPaginationInfo = document.getElementById( - "problemsPaginationInfo", - ); + this.allProblemItems = []; + this.filteredProblemItems = []; + this.currentPage = 1; + this.itemsPerPage = 5; + this.problemSort = { column: "alpha", direction: "asc" }; + this.problemDescriptionPopover = null; + this.manifestCache = new Map(); - let allProblemItems = []; - let filteredProblemItems = []; - let currentPage = 1; - const itemsPerPage = 5; + this.init(); + } - // Initialize problem items - function initializeProblemItems() { - allProblemItems = Array.from( - problemsContainer?.querySelectorAll(".problem-item") || [], - ); - filteredProblemItems = [...allProblemItems]; - updatePagination(); - } + init() { + if (!this.problemsContainer) return; + + this.initializeProblemItems(); + this.attachEventListeners(); + this.injectPopoverCSS(); + this.attachProblemHoverEvents(); + } - function updatePagination() { - const totalPages = Math.ceil(filteredProblemItems.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; + initializeProblemItems() { + this.allProblemItems = Array.from( + this.problemsContainer.querySelectorAll(".problem-item") || [] + ); + this.filteredProblemItems = this.allProblemItems.map(this.getProblemData); + this.updatePagination(); + } - // Hide all items first - allProblemItems.forEach((item) => { - item.style.display = "none"; + getProblemData = (item) => ({ + element: item, + name: item.dataset.name?.toLowerCase() || "", + desc: item.dataset.desc?.toLowerCase() || "", + difficulty: item.dataset.difficulty || "", }); - // Show current page items - filteredProblemItems.slice(startIndex, endIndex).forEach((item) => { - item.style.display = ""; - }); + updatePagination() { + const totalPages = Math.ceil(this.filteredProblemItems.length / this.itemsPerPage); + const startIndex = (this.currentPage - 1) * this.itemsPerPage; + const endIndex = startIndex + this.itemsPerPage; - // Update pagination controls - if (problemsPrevBtn) problemsPrevBtn.disabled = currentPage <= 1; - if (problemsNextBtn) problemsNextBtn.disabled = currentPage >= totalPages; - if (problemsPaginationInfo) { - problemsPaginationInfo.textContent = - totalPages > 0 - ? `Page ${currentPage} of ${totalPages}` - : "No problems found"; + // 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(); } - // Hide pagination if not needed - if (problemsPagination) { - problemsPagination.classList.toggle("hidden", totalPages <= 1); + 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(); } } - function filterProblems() { - const term = problemSearch?.value.toLowerCase().trim() || ""; - filteredProblemItems = allProblemItems.filter((item) => { - const name = item.dataset.name?.toLowerCase() || ""; - const desc = item.dataset.desc?.toLowerCase() || ""; - return !term || name.includes(term) || desc.includes(term); - }); - currentPage = 1; - updatePagination(); - } + // 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"); - // Event listeners for pagination - problemsPrevBtn?.addEventListener("click", () => { - if (currentPage > 1) { - currentPage--; - updatePagination(); + this.currentSort = { column: "rank", direction: "asc" }; + this.allRows = []; + + this.init(); } - }); - problemsNextBtn?.addEventListener("click", () => { - const totalPages = Math.ceil(filteredProblemItems.length / itemsPerPage); - if (currentPage < totalPages) { - currentPage++; - updatePagination(); + init() { + if (!this.leaderboardBody || this.leaderboardBody.children.length === 0) return; + + this.initializeRows(); + this.attachEventListeners(); + this.calculateOverallRanking(); + this.setInitialSortIndicator(); } - }); - problemSearch?.addEventListener("input", filterProblems); - - // Initialize problems pagination - if (problemsContainer) { - initializeProblemItems(); - } - - // Leaderboard functionality - const problemFilter = document.getElementById("problemFilter"); - const runtimeFilter = document.getElementById("runtimeFilter"); - const leaderboardBody = document.getElementById("leaderboardBody"); - const sortableHeaders = document.querySelectorAll(".sortable"); - - let currentSort = { column: "rank", direction: "asc" }; - let allRows = []; - - // Initialize rows array - function initializeRows() { - allRows = Array.from(leaderboardBody.querySelectorAll("tr")).map((row) => { - return { + initializeRows() { + this.allRows = Array.from(this.leaderboardBody.querySelectorAll("tr")).map((row, index) => ({ element: row, user: row.dataset.user || "", problem: row.dataset.problem || "", @@ -128,276 +414,279 @@ document.addEventListener("DOMContentLoaded", () => { memory: parseFloat(row.dataset.memory) || 0, timestamp: new Date(row.dataset.timestamp || Date.now()).getTime(), language: row.dataset.language || "", - originalIndex: Array.from(leaderboardBody.children).indexOf(row), - }; - }); - } + originalIndex: index, + })); + } - function updateRankClasses() { - const visibleRows = 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"); - }); - } - - function calculateOverallRanking() { - const visibleRows = 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, + updateRankClasses() { + const visibleRows = this.allRows.filter( + (row) => row.element.style.display !== "none" ); - problemBests[problem].bestMemory = Math.min( - problemBests[problem].bestMemory, - rowData.memory, + + 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" ); - }); - // Calculate normalized scores for each submission - visibleRows.forEach((rowData) => { - const problemBest = problemBests[rowData.problem]; + if (visibleRows.length === 0) return; - // Prevent division by zero - const runtimeScore = - problemBest.bestRuntime > 0 + // 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 + 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; // Use small epsilon for float comparison - - // If scores are essentially equal, prefer earlier submission - return a.timestamp - b.timestamp; - }); - - // Reorder DOM elements and update ranks - visibleRows.forEach((rowData, index) => { - leaderboardBody.appendChild(rowData.element); - }); - - updateRankClasses(); - } - - function filterLeaderboard() { - const problemTerm = (problemFilter?.value || "").toLowerCase().trim(); - const runtimeType = runtimeFilter?.value || "all"; - - // Reset all rows to visible first - allRows.forEach((rowData) => { - rowData.element.style.display = ""; - }); - - // Apply problem filter - if (problemTerm) { - allRows.forEach((rowData) => { - const problemMatch = rowData.problem - .toLowerCase() - .includes(problemTerm); - if (!problemMatch) { - rowData.element.style.display = "none"; - } + // 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(); } - // Apply runtime filter (best/worst per user per problem) - if (runtimeType === "best" || runtimeType === "worst") { - const userProblemGroups = {}; + filterLeaderboard() { + const problemTerm = (this.problemFilter?.value || "").toLowerCase().trim(); + const runtimeType = this.runtimeFilter?.value || "all"; - // Group by user + problem combination - 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); + // Reset all rows to visible first + this.allRows.forEach((rowData) => { + rowData.element.style.display = ""; }); - // Hide all except best/worst for each user-problem combination - Object.values(userProblemGroups).forEach((group) => { - if (group.length <= 1) return; - - // Sort by runtime - group.sort((a, b) => a.runtime - b.runtime); - - const keepIndex = runtimeType === "best" ? 0 : group.length - 1; - group.forEach((rowData, index) => { - if (index !== keepIndex) { + // 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"); + } }); } - calculateOverallRanking(); - } - - function 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 ""; - } - } - - function sortLeaderboard(column, direction) { - if (column === "rank") { - calculateOverallRanking(); - return; - } - - const visibleRows = allRows.filter( - (row) => row.element.style.display !== "none", - ); - - visibleRows.sort((a, b) => { - const valueA = getCellValue(a, column); - const valueB = 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; + setInitialSortIndicator() { + const defaultHeader = document.querySelector('[data-sort="rank"]'); + if (defaultHeader) { + defaultHeader.classList.add("sort-asc"); } - - return direction === "asc" ? comparison : -comparison; - }); - - // Reorder DOM elements - visibleRows.forEach((rowData) => { - leaderboardBody.appendChild(rowData.element); - }); - - updateRankClasses(); - } - - // Event listeners for sorting - sortableHeaders.forEach((header) => { - header.addEventListener("click", () => { - const column = header.dataset.sort; - if (!column) return; - - // Remove sorting classes from all headers - sortableHeaders.forEach((h) => - h.classList.remove("sort-asc", "sort-desc"), - ); - - // Toggle sort direction - if (currentSort.column === column) { - currentSort.direction = - currentSort.direction === "asc" ? "desc" : "asc"; - } else { - currentSort.column = column; - currentSort.direction = "asc"; - } - - // Add sorting class to current header - header.classList.add(`sort-${currentSort.direction}`); - - sortLeaderboard(column, currentSort.direction); - }); - }); - - // Filter event listeners - problemFilter?.addEventListener("input", filterLeaderboard); - runtimeFilter?.addEventListener("change", filterLeaderboard); - - // Rank info popout - const rankInfoBtn = document.getElementById("rankInfoBtn"); - const rankingExplanation = document.getElementById("rankingExplanation"); - - rankInfoBtn?.addEventListener("click", (e) => { - e.preventDefault(); - rankingExplanation?.classList.toggle("active"); - rankInfoBtn?.classList.toggle("active"); - }); - - // Close ranking explanation when clicking outside - document.addEventListener("click", (e) => { - if ( - rankingExplanation?.classList.contains("active") && - !rankingExplanation.contains(e.target) && - !rankInfoBtn?.contains(e.target) - ) { - rankingExplanation.classList.remove("active"); - rankInfoBtn?.classList.remove("active"); - } - }); - - // Initialize everything - if (leaderboardBody && leaderboardBody.children.length > 0) { - initializeRows(); - calculateOverallRanking(); - - // Set initial sort indicator - 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 - function applyDarkModeToElements() { - const isDark = html.classList.contains("dark"); + const applyDarkModeToElements = () => { // Any additional dark mode styling for dynamically created elements can go here - } + }; // Watch for dark mode changes - new MutationObserver(applyDarkModeToElements).observe(html, { + 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