new problem and i added better support for sorting and a overall better script
This commit is contained in:
17
src/app.py
17
src/app.py
@@ -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")
|
||||||
|
|||||||
90
src/problems/PrimeNumber/description.md
Normal file
90
src/problems/PrimeNumber/description.md
Normal 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 it’s **not prime**.
|
||||||
7
src/problems/PrimeNumber/manifest.json
Normal file
7
src/problems/PrimeNumber/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
33
src/problems/PrimeNumber/test.py
Normal file
33
src/problems/PrimeNumber/test.py
Normal 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)
|
||||||
@@ -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 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:
|
### Function Signature:
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.4 KiB |
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user