new problem and i added better support for sorting and a overall better script

This commit is contained in:
2025-08-17 18:58:12 +02:00
parent 996cfd2821
commit f3baec17e4
8 changed files with 778 additions and 344 deletions

View File

@@ -1,5 +1,6 @@
# API endpoint to get problem manifest (description) by folder
from markupsafe import Markup 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 markdown as md
import ast import ast
from src.models import db, Problem, Solution from src.models import db, Problem, Solution
@@ -30,6 +31,20 @@ def setup():
# Start the background thread to scan problems # Start the background thread to scan problems
start_problem_scanner() start_problem_scanner()
@app.route('/api/problem_manifest/<folder>')
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") @app.route("/script.js")
def script(): def script():
return send_from_directory("templates", "script.js") return send_from_directory("templates", "script.js")

View File

@@ -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 its **not prime**.

View File

@@ -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"
}

View File

@@ -0,0 +1,33 @@
import unittest
# <!-- Function to check -->
# 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)

View File

@@ -1,7 +1,7 @@
## Reverse a List ## Reverse a List
Write a function called `reverse_list` that takes a list as input and returns the list in reverse order. 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 Pythons built-in `.reverse()` method or slicing (`[::-1]`) try to reverse it manually for practice. You are **allowed** to just use Pythons built-in `.reverse()` method or slicing (`[::-1]`), try to reverse it manually for practice.
### Function Signature: ### Function Signature:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,126 +1,412 @@
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
// Dark mode functionality "use strict";
const darkModeToggle = document.getElementById("darkModeToggle");
const html = document.documentElement;
// Load saved dark mode preference // Utility functions
const savedDarkMode = localStorage.getItem("darkMode"); 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 ( if (
savedDarkMode === "true" || savedDarkMode === "true" ||
(savedDarkMode === null && (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) window.matchMedia("(prefers-color-scheme: dark)").matches)
) { ) {
html.classList.add("dark"); this.html.classList.add("dark");
}
} }
darkModeToggle?.addEventListener("click", () => { attachEventListeners() {
html.classList.toggle("dark"); this.darkModeToggle?.addEventListener("click", () => {
localStorage.setItem("darkMode", html.classList.contains("dark")); 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 || "",
}); });
// Problem search and pagination updatePagination() {
const problemSearch = document.getElementById("problemSearch"); const totalPages = Math.ceil(this.filteredProblemItems.length / this.itemsPerPage);
const problemsContainer = document.getElementById("problemsContainer"); const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const problemsPagination = document.getElementById("problemsPagination"); const endIndex = startIndex + this.itemsPerPage;
const problemsPrevBtn = document.getElementById("problemsPrevBtn");
const problemsNextBtn = document.getElementById("problemsNextBtn");
const problemsPaginationInfo = document.getElementById(
"problemsPaginationInfo",
);
let allProblemItems = [];
let filteredProblemItems = [];
let currentPage = 1;
const itemsPerPage = 5;
// Initialize problem items
function initializeProblemItems() {
allProblemItems = Array.from(
problemsContainer?.querySelectorAll(".problem-item") || [],
);
filteredProblemItems = [...allProblemItems];
updatePagination();
}
function updatePagination() {
const totalPages = Math.ceil(filteredProblemItems.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
// Hide all items first // Hide all items first
allProblemItems.forEach((item) => { this.allProblemItems.forEach((item) => {
item.style.display = "none"; item.style.display = "none";
}); });
// Show current page items // Show current page items
filteredProblemItems.slice(startIndex, endIndex).forEach((item) => { this.filteredProblemItems.slice(startIndex, endIndex).forEach((item) => {
item.style.display = ""; item.element.style.display = "";
}); });
// Update pagination controls // Update pagination controls
if (problemsPrevBtn) problemsPrevBtn.disabled = currentPage <= 1; if (this.problemsPrevBtn) this.problemsPrevBtn.disabled = this.currentPage <= 1;
if (problemsNextBtn) problemsNextBtn.disabled = currentPage >= totalPages; if (this.problemsNextBtn) this.problemsNextBtn.disabled = this.currentPage >= totalPages;
if (problemsPaginationInfo) {
problemsPaginationInfo.textContent = if (this.problemsPaginationInfo) {
this.problemsPaginationInfo.textContent =
totalPages > 0 totalPages > 0
? `Page ${currentPage} of ${totalPages}` ? `Page ${this.currentPage} of ${totalPages}`
: "No problems found"; : "No problems found";
} }
// Hide pagination if not needed this.setupPaginationLayout();
if (problemsPagination) {
problemsPagination.classList.toggle("hidden", totalPages <= 1);
}
} }
function filterProblems() { setupPaginationLayout() {
const term = problemSearch?.value.toLowerCase().trim() || ""; if (this.problemsPagination) {
filteredProblemItems = allProblemItems.filter((item) => { Object.assign(this.problemsPagination.style, {
const name = item.dataset.name?.toLowerCase() || ""; display: "flex",
const desc = item.dataset.desc?.toLowerCase() || ""; justifyContent: "center",
return !term || name.includes(term) || desc.includes(term); position: "absolute",
left: "0",
right: "0",
bottom: "0",
margin: "0 auto",
width: "100%",
background: "inherit",
borderTop: "1px solid var(--border)",
padding: "12px 0"
}); });
currentPage = 1; this.problemsPagination.classList.remove("hidden");
updatePagination();
} }
// Event listeners for pagination if (this.problemsContainer?.parentElement) {
problemsPrevBtn?.addEventListener("click", () => { Object.assign(this.problemsContainer.parentElement.style, {
if (currentPage > 1) { position: "relative",
currentPage--; paddingBottom: "56px"
updatePagination(); });
}
}
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();
} }
}); });
problemsNextBtn?.addEventListener("click", () => { this.problemsNextBtn?.addEventListener("click", () => {
const totalPages = Math.ceil(filteredProblemItems.length / itemsPerPage); const totalPages = Math.ceil(this.filteredProblemItems.length / this.itemsPerPage);
if (currentPage < totalPages) { if (this.currentPage < totalPages) {
currentPage++; this.currentPage++;
updatePagination(); this.updatePagination();
} }
}); });
problemSearch?.addEventListener("input", filterProblems); this.problemSearch?.addEventListener("input", utils.debounce(() => {
this.filterProblems();
this.currentPage = 1;
this.updatePagination();
}, 300));
// Initialize problems pagination this.difficultyFilter?.addEventListener("change", () => {
if (problemsContainer) { this.filterProblems();
initializeProblemItems(); 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();
}
});
} }
// Leaderboard functionality filterProblems() {
const problemFilter = document.getElementById("problemFilter"); const searchTerm = (this.problemSearch?.value || "").toLowerCase().trim();
const runtimeFilter = document.getElementById("runtimeFilter"); const difficulty = this.difficultyFilter?.value || "all";
const leaderboardBody = document.getElementById("leaderboardBody");
const sortableHeaders = document.querySelectorAll(".sortable");
let currentSort = { column: "rank", direction: "asc" }; this.filteredProblemItems = this.allProblemItems
let allRows = []; .map(this.getProblemData)
.filter(item => {
const matchesSearch = !searchTerm ||
item.name.includes(searchTerm) ||
item.desc.includes(searchTerm);
// Initialize rows array const matchesDifficulty = difficulty === "all" ||
function initializeRows() { item.difficulty === difficulty;
allRows = Array.from(leaderboardBody.querySelectorAll("tr")).map((row) => {
return { 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, element: row,
user: row.dataset.user || "", user: row.dataset.user || "",
problem: row.dataset.problem || "", problem: row.dataset.problem || "",
@@ -128,15 +414,15 @@ document.addEventListener("DOMContentLoaded", () => {
memory: parseFloat(row.dataset.memory) || 0, memory: parseFloat(row.dataset.memory) || 0,
timestamp: new Date(row.dataset.timestamp || Date.now()).getTime(), timestamp: new Date(row.dataset.timestamp || Date.now()).getTime(),
language: row.dataset.language || "", language: row.dataset.language || "",
originalIndex: Array.from(leaderboardBody.children).indexOf(row), originalIndex: index,
}; }));
});
} }
function updateRankClasses() { updateRankClasses() {
const visibleRows = allRows.filter( const visibleRows = this.allRows.filter(
(row) => row.element.style.display !== "none", (row) => row.element.style.display !== "none"
); );
visibleRows.forEach((rowData, index) => { visibleRows.forEach((rowData, index) => {
const rank = index + 1; const rank = index + 1;
const row = rowData.element; const row = rowData.element;
@@ -152,9 +438,9 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
} }
function calculateOverallRanking() { calculateOverallRanking() {
const visibleRows = allRows.filter( const visibleRows = this.allRows.filter(
(row) => row.element.style.display !== "none", (row) => row.element.style.display !== "none"
); );
if (visibleRows.length === 0) return; if (visibleRows.length === 0) return;
@@ -173,11 +459,11 @@ document.addEventListener("DOMContentLoaded", () => {
problemBests[problem].bestRuntime = Math.min( problemBests[problem].bestRuntime = Math.min(
problemBests[problem].bestRuntime, problemBests[problem].bestRuntime,
rowData.runtime, rowData.runtime
); );
problemBests[problem].bestMemory = Math.min( problemBests[problem].bestMemory = Math.min(
problemBests[problem].bestMemory, problemBests[problem].bestMemory,
rowData.memory, rowData.memory
); );
}); });
@@ -185,13 +471,10 @@ document.addEventListener("DOMContentLoaded", () => {
visibleRows.forEach((rowData) => { visibleRows.forEach((rowData) => {
const problemBest = problemBests[rowData.problem]; const problemBest = problemBests[rowData.problem];
// Prevent division by zero const runtimeScore = problemBest.bestRuntime > 0
const runtimeScore =
problemBest.bestRuntime > 0
? rowData.runtime / problemBest.bestRuntime ? rowData.runtime / problemBest.bestRuntime
: 1; : 1;
const memoryScore = const memoryScore = problemBest.bestMemory > 0
problemBest.bestMemory > 0
? rowData.memory / problemBest.bestMemory ? rowData.memory / problemBest.bestMemory
: 1; : 1;
@@ -202,35 +485,33 @@ document.addEventListener("DOMContentLoaded", () => {
// Sort by overall score (lower is better), then by timestamp (earlier is better for ties) // Sort by overall score (lower is better), then by timestamp (earlier is better for ties)
visibleRows.sort((a, b) => { visibleRows.sort((a, b) => {
const scoreDiff = a.overallScore - b.overallScore; const scoreDiff = a.overallScore - b.overallScore;
if (Math.abs(scoreDiff) > 0.000001) return scoreDiff; // Use small epsilon for float comparison if (Math.abs(scoreDiff) > 0.000001) return scoreDiff;
// If scores are essentially equal, prefer earlier submission
return a.timestamp - b.timestamp; return a.timestamp - b.timestamp;
}); });
// Reorder DOM elements and update ranks // Reorder DOM elements and update ranks
visibleRows.forEach((rowData, index) => { const fragment = document.createDocumentFragment();
leaderboardBody.appendChild(rowData.element); visibleRows.forEach((rowData) => {
fragment.appendChild(rowData.element);
}); });
this.leaderboardBody.appendChild(fragment);
updateRankClasses(); this.updateRankClasses();
} }
function filterLeaderboard() { filterLeaderboard() {
const problemTerm = (problemFilter?.value || "").toLowerCase().trim(); const problemTerm = (this.problemFilter?.value || "").toLowerCase().trim();
const runtimeType = runtimeFilter?.value || "all"; const runtimeType = this.runtimeFilter?.value || "all";
// Reset all rows to visible first // Reset all rows to visible first
allRows.forEach((rowData) => { this.allRows.forEach((rowData) => {
rowData.element.style.display = ""; rowData.element.style.display = "";
}); });
// Apply problem filter // Apply problem filter
if (problemTerm) { if (problemTerm) {
allRows.forEach((rowData) => { this.allRows.forEach((rowData) => {
const problemMatch = rowData.problem const problemMatch = rowData.problem.toLowerCase().includes(problemTerm);
.toLowerCase()
.includes(problemTerm);
if (!problemMatch) { if (!problemMatch) {
rowData.element.style.display = "none"; rowData.element.style.display = "none";
} }
@@ -242,7 +523,7 @@ document.addEventListener("DOMContentLoaded", () => {
const userProblemGroups = {}; const userProblemGroups = {};
// Group by user + problem combination // Group by user + problem combination
allRows.forEach((rowData) => { this.allRows.forEach((rowData) => {
if (rowData.element.style.display === "none") return; if (rowData.element.style.display === "none") return;
const key = `${rowData.user}::${rowData.problem}`; const key = `${rowData.user}::${rowData.problem}`;
@@ -256,7 +537,6 @@ document.addEventListener("DOMContentLoaded", () => {
Object.values(userProblemGroups).forEach((group) => { Object.values(userProblemGroups).forEach((group) => {
if (group.length <= 1) return; if (group.length <= 1) return;
// Sort by runtime
group.sort((a, b) => a.runtime - b.runtime); group.sort((a, b) => a.runtime - b.runtime);
const keepIndex = runtimeType === "best" ? 0 : group.length - 1; const keepIndex = runtimeType === "best" ? 0 : group.length - 1;
@@ -268,10 +548,10 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
} }
calculateOverallRanking(); this.calculateOverallRanking();
} }
function getCellValue(rowData, column) { getCellValue(rowData, column) {
switch (column) { switch (column) {
case "rank": case "rank":
return parseInt(rowData.element.cells[0]?.textContent) || 0; return parseInt(rowData.element.cells[0]?.textContent) || 0;
@@ -292,19 +572,19 @@ document.addEventListener("DOMContentLoaded", () => {
} }
} }
function sortLeaderboard(column, direction) { sortLeaderboard(column, direction) {
if (column === "rank") { if (column === "rank") {
calculateOverallRanking(); this.calculateOverallRanking();
return; return;
} }
const visibleRows = allRows.filter( const visibleRows = this.allRows.filter(
(row) => row.element.style.display !== "none", (row) => row.element.style.display !== "none"
); );
visibleRows.sort((a, b) => { visibleRows.sort((a, b) => {
const valueA = getCellValue(a, column); const valueA = this.getCellValue(a, column);
const valueB = getCellValue(b, column); const valueB = this.getCellValue(b, column);
let comparison = 0; let comparison = 0;
if (typeof valueA === "number" && typeof valueB === "number") { if (typeof valueA === "number" && typeof valueB === "number") {
@@ -316,88 +596,97 @@ document.addEventListener("DOMContentLoaded", () => {
return direction === "asc" ? comparison : -comparison; return direction === "asc" ? comparison : -comparison;
}); });
// Reorder DOM elements // Reorder DOM elements using document fragment for better performance
const fragment = document.createDocumentFragment();
visibleRows.forEach((rowData) => { visibleRows.forEach((rowData) => {
leaderboardBody.appendChild(rowData.element); fragment.appendChild(rowData.element);
}); });
this.leaderboardBody.appendChild(fragment);
updateRankClasses(); this.updateRankClasses();
} }
// Event listeners for sorting attachEventListeners() {
sortableHeaders.forEach((header) => { // Sorting event listeners
this.sortableHeaders.forEach((header) => {
header.addEventListener("click", () => { header.addEventListener("click", () => {
const column = header.dataset.sort; const column = header.dataset.sort;
if (!column) return; if (!column) return;
// Remove sorting classes from all headers // Remove sorting classes from all headers
sortableHeaders.forEach((h) => this.sortableHeaders.forEach((h) =>
h.classList.remove("sort-asc", "sort-desc"), h.classList.remove("sort-asc", "sort-desc")
); );
// Toggle sort direction // Toggle sort direction
if (currentSort.column === column) { if (this.currentSort.column === column) {
currentSort.direction = this.currentSort.direction = this.currentSort.direction === "asc" ? "desc" : "asc";
currentSort.direction === "asc" ? "desc" : "asc";
} else { } else {
currentSort.column = column; this.currentSort.column = column;
currentSort.direction = "asc"; this.currentSort.direction = "asc";
} }
// Add sorting class to current header // Add sorting class to current header
header.classList.add(`sort-${currentSort.direction}`); header.classList.add(`sort-${this.currentSort.direction}`);
sortLeaderboard(column, currentSort.direction); this.sortLeaderboard(column, this.currentSort.direction);
}); });
}); });
// Filter event listeners // Filter event listeners with debouncing
problemFilter?.addEventListener("input", filterLeaderboard); this.problemFilter?.addEventListener("input",
runtimeFilter?.addEventListener("change", filterLeaderboard); utils.debounce(() => this.filterLeaderboard(), 300)
);
this.runtimeFilter?.addEventListener("change", () => this.filterLeaderboard());
// Rank info popout // Rank info popout
const rankInfoBtn = document.getElementById("rankInfoBtn"); this.rankInfoBtn?.addEventListener("click", (e) => {
const rankingExplanation = document.getElementById("rankingExplanation");
rankInfoBtn?.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
rankingExplanation?.classList.toggle("active"); this.rankingExplanation?.classList.toggle("active");
rankInfoBtn?.classList.toggle("active"); this.rankInfoBtn?.classList.toggle("active");
}); });
// Close ranking explanation when clicking outside // Close ranking explanation when clicking outside
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
if ( if (
rankingExplanation?.classList.contains("active") && this.rankingExplanation?.classList.contains("active") &&
!rankingExplanation.contains(e.target) && !this.rankingExplanation.contains(e.target) &&
!rankInfoBtn?.contains(e.target) !this.rankInfoBtn?.contains(e.target)
) { ) {
rankingExplanation.classList.remove("active"); this.rankingExplanation.classList.remove("active");
rankInfoBtn?.classList.remove("active"); this.rankInfoBtn?.classList.remove("active");
} }
}); });
}
// Initialize everything setInitialSortIndicator() {
if (leaderboardBody && leaderboardBody.children.length > 0) {
initializeRows();
calculateOverallRanking();
// Set initial sort indicator
const defaultHeader = document.querySelector('[data-sort="rank"]'); const defaultHeader = document.querySelector('[data-sort="rank"]');
if (defaultHeader) { if (defaultHeader) {
defaultHeader.classList.add("sort-asc"); defaultHeader.classList.add("sort-asc");
} }
} }
// Apply dark mode to dynamically created elements
function applyDarkModeToElements() {
const isDark = html.classList.contains("dark");
// Any additional dark mode styling for dynamically created elements can go here
} }
// 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 // Watch for dark mode changes
new MutationObserver(applyDarkModeToElements).observe(html, { const darkModeObserver = new MutationObserver(applyDarkModeToElements);
darkModeObserver.observe(document.documentElement, {
attributes: true, attributes: true,
attributeFilter: ["class"], attributeFilter: ["class"],
}); });
// Cleanup on page unload
window.addEventListener("beforeunload", () => {
problemManager.destroy();
darkModeObserver.disconnect();
});
}); });