darkmode #2

Merged
rattatwinko merged 2 commits from darkmode into main 2025-08-17 09:50:58 +00:00
9 changed files with 1664 additions and 1191 deletions

View File

@@ -1,17 +1,17 @@
#!/bin/bash u!/bin/bash
set -e # exit if any command fails set -e # exit if any command fails
# Ensure QPP/database directory exists # Ensure QPP/database directory exists
mkdir -p src/database mkdir -p src/database
python -m venv venv python -m venv venv
source venv/bin/activate source venv/bin/activate
pip install --upgrade pip pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
export FLASK_APP=src.app export FLASK_APP=src.app
export FLASK_ENV=production export FLASK_ENV=production
flask run --host=0.0.0.0 --port=5000 flask run --host=0.0.0.0 --port=5000

1
run.bat Normal file
View File

@@ -0,0 +1 @@
python -m flask --app .\src\app.py run --host=0.0.0.0 --port=5000

View File

@@ -1,207 +1,326 @@
:root { :root {
--bg: #f6f8fb; --bg: #f6f8fb;
--card: #fff; --card: #fff;
--muted: #6b7280; --text: #0f172a;
--accent: #2563eb; --muted: #6b7280;
--shadow: 0 4px 12px rgba(16, 24, 40, 0.06); --accent: #2563eb;
--radius: 8px; --border: #e5e7eb;
--mono: 'JetBrains Mono', monospace; --hover: #f3f4f6;
} --shadow: 0 4px 12px rgba(16, 24, 40, 0.06);
* { box-sizing: border-box; margin: 0; padding: 0; } --radius: 8px;
html, body { --mono: "JetBrains Mono", monospace;
height: 100%; }
}
body { /* Dark mode variables */
font-family: Inter, sans-serif; html.dark {
background: var(--bg); --bg: #0f172a;
color: #0f172a; --card: #1e293b;
padding: 16px; --text: #f1f5f9;
display: flex; --muted: #94a3b8;
justify-content: center; --accent: #3b82f6;
align-items: center; --border: #334155;
} --hover: #334155;
.wrap { --shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
width: 100%; }
max-width: 1100px; * {
} box-sizing: border-box;
header { margin: 0;
margin-bottom: 14px; padding: 0;
} }
header h1 { html,
text-align: center; body {
font-size: 1.6rem; height: 100%;
color: #111827; }
} body {
header p { font-family: Inter, sans-serif;
color: var(--muted); background: var(--bg);
font-size: 0.9rem; color: var(--text);
} padding: 16px;
.content { display: flex;
display: grid; justify-content: center;
grid-template-columns: 1fr 1fr; align-items: center;
gap: 12px; transition:
} background-color 0.3s ease,
.content.single-column { color 0.3s ease;
grid-template-columns: 1fr; }
} .wrap {
.card { width: 100%;
background: var(--card); max-width: 1100px;
border-radius: var(--radius); }
box-shadow: var(--shadow); header {
padding: 12px; margin-bottom: 14px;
} }
/* Search/filter controls */ .header-content {
.search-controls { display: flex;
margin-bottom: 12px; justify-content: center;
display: flex; align-items: center;
gap: 8px; position: relative;
} }
.search-input { header h1 {
flex: 1; text-align: center;
padding: 6px 10px; font-size: 1.6rem;
border: 1px solid #e5e7eb; color: var(--text);
border-radius: 4px; }
font-size: 0.9rem; header p {
} color: var(--muted);
.filter-select { font-size: 0.9rem;
padding: 6px 8px; }
border: 1px solid #e5e7eb; .dark-mode-toggle {
border-radius: 4px; position: absolute;
font-size: 0.9rem; right: 0;
background: white; background: none;
} border: 1px solid var(--border);
/* Problems list */ border-radius: var(--radius);
.problems-list .problem-item { padding: 6px 10px;
padding: 8px; cursor: pointer;
border-bottom: 1px solid #e5e7eb; color: var(--text);
display: flex; font-size: 1.2rem;
justify-content: space-between; transition: all 0.3s ease;
align-items: center; }
} .dark-mode-toggle:hover {
.problem-item:last-child { background: var(--hover);
border-bottom: none; transform: scale(1.05);
} }
.problem-item a { html.dark .dark-mode-icon::before {
text-decoration: none; content: "☀︎️";
color: #0077ff; }
font-weight: 600; html:not(.dark) .dark-mode-icon::before {
} content: "⏾";
/* Difficulty badge */ }
.difficulty { .dark-mode-icon {
display: inline-flex; display: inline-block;
align-items: center; }
padding: 0.25em 0.6em; .dark-mode-icon::before {
border-radius: 10px; font-size: 1em;
font-size: 0.85em; }
font-weight: bold; .content {
text-transform: uppercase; display: grid;
color: white; grid-template-columns: 1fr 1fr;
white-space: nowrap; gap: 12px;
} }
.difficulty[data-difficulty="easy"] { .content.single-column {
background-color: #4CAF50; /* Green */ grid-template-columns: 1fr;
} }
.difficulty[data-difficulty="medium"] { .card {
background-color: #FFC107; /* Amber */ background: var(--card);
color: #333; border-radius: var(--radius);
} box-shadow: var(--shadow);
.difficulty[data-difficulty="hard"] { padding: 12px;
background-color: #F44336; /* Red */ }
} /* Search/filter controls */
/* Leaderboard */ .search-controls {
.leaderboard-head { margin-bottom: 12px;
display: flex; display: flex;
justify-content: space-between; gap: 8px;
align-items: center; }
margin-bottom: 6px; .search-input {
} flex: 1;
.leaderboard-controls { padding: 6px 10px;
display: flex; border: 1px solid var(--border);
gap: 8px; border-radius: 4px;
margin-bottom: 12px; font-size: 0.9rem;
} background: var(--card);
.leaderboard-table { color: var(--text);
width: 100%; transition: border-color 0.3s ease;
border-collapse: collapse; }
font-size: 0.9rem; .search-input:focus {
} outline: none;
.leaderboard-table th, border-color: var(--accent);
.leaderboard-table td { }
padding: 6px 8px; .filter-select {
border-bottom: 1px solid #e5e7eb; padding: 6px 8px;
text-align: left; border: 1px solid var(--border);
} border-radius: 4px;
.leaderboard-table th { font-size: 0.9rem;
background: #f9fafb; background: var(--card);
font-weight: 600; color: var(--text);
color: var(--muted); transition: border-color 0.3s ease;
} }
.leaderboard-table tr:hover { .filter-select:focus {
background: #f3f4f6; outline: none;
} border-color: var(--accent);
/* Sort indicators */ }
.sortable { /* Problems list */
cursor: pointer; .problems-list .problem-item {
position: relative; padding: 8px;
padding-right: 16px; border-bottom: 1px solid var(--border);
} display: flex;
.sortable::after { justify-content: space-between;
content: "↕"; align-items: center;
position: absolute; transition: background-color 0.3s ease;
right: 4px; }
top: 50%; .problem-item:hover {
transform: translateY(-50%); background: var(--hover);
font-size: 0.8em; }
opacity: 0.5; .problem-item:last-child {
} border-bottom: none;
.sort-asc::after { }
content: "↑"; .problem-item a {
opacity: 1; text-decoration: none;
} color: var(--accent);
.sort-desc::after { font-weight: 600;
content: "↓"; transition: color 0.3s ease;
opacity: 1; }
} .problem-item a:hover {
/* Toggle button */ text-decoration: underline;
.btn { }
border: none; /* Difficulty badge */
background: transparent; .difficulty {
cursor: pointer; display: inline-flex;
color: var(--accent); align-items: center;
font-size: 0.85rem; padding: 0.25em 0.6em;
padding: 4px 6px; border-radius: 10px;
border-radius: 4px; font-size: 0.85em;
} font-weight: bold;
.btn:hover { text-transform: uppercase;
background: rgba(37, 99, 235, 0.08); color: white;
} white-space: nowrap;
.btn.active { }
background: rgba(37, 99, 235, 0.15); .difficulty[data-difficulty="easy"] {
} background-color: #4caf50; /* Green */
@media (max-width: 800px) { }
.content { grid-template-columns: 1fr; } .difficulty[data-difficulty="medium"] {
.leaderboard-controls { background-color: #ffc107; /* Amber */
flex-direction: column; color: #333;
} }
} .difficulty[data-difficulty="hard"] {
background-color: #f44336; /* Red */
/* Leaderboard horizontal collapse */ }
#leaderboardSection { /* Leaderboard */
transition: max-width 0.35s ease, opacity 0.25s ease; .leaderboard-head {
overflow: hidden; display: flex;
max-width: 100%; justify-content: space-between;
} align-items: center;
margin-bottom: 6px;
#leaderboardSection.hidden { }
max-width: 0; .leaderboard-controls {
opacity: 0; display: flex;
pointer-events: none; gap: 8px;
} margin-bottom: 12px;
}
#leaderboardSection.visible { .leaderboard-table {
max-width: 100%; /* take full available space in grid column */ width: 100%;
opacity: 1; border-collapse: collapse;
} font-size: 0.9rem;
#rankingExplanation { }
transition: all 0.35s ease; .leaderboard-table th,
} .leaderboard-table td {
padding: 6px 8px;
border-bottom: 1px solid var(--border);
text-align: left;
}
.leaderboard-table th {
background: var(--hover);
font-weight: 600;
color: var(--muted);
}
.leaderboard-table tr:hover {
background: var(--hover);
}
/* Sort indicators */
.sortable {
cursor: pointer;
position: relative;
padding-right: 16px;
}
.sortable::after {
content: "↕";
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
font-size: 0.8em;
opacity: 0.5;
}
.sort-asc::after {
content: "↑";
opacity: 1;
}
.sort-desc::after {
content: "↓";
opacity: 1;
}
/* Toggle button */
.btn {
border: none;
background: transparent;
cursor: pointer;
color: var(--accent);
font-size: 0.85rem;
padding: 4px 6px;
border-radius: 4px;
}
.btn:hover {
background: rgba(37, 99, 235, 0.08);
}
.btn.active {
background: rgba(37, 99, 235, 0.15);
}
@media (max-width: 800px) {
.content {
grid-template-columns: 1fr;
}
.leaderboard-controls {
flex-direction: column;
}
}
/* Leaderboard horizontal collapse */
#leaderboardSection {
transition:
max-width 0.35s ease,
opacity 0.25s ease;
overflow: hidden;
max-width: 100%;
}
#leaderboardSection.hidden {
max-width: 0;
opacity: 0;
pointer-events: none;
}
#leaderboardSection.visible {
max-width: 100%; /* take full available space in grid column */
opacity: 1;
}
#rankingExplanation {
transition: all 0.35s ease;
}
/* Pagination Controls */
.pagination-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.pagination-btn {
background: var(--card);
border: 1px solid var(--border);
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
color: var(--text);
font-size: 0.9rem;
transition: all 0.3s ease;
}
.pagination-btn:hover:not(:disabled) {
background: var(--hover);
border-color: var(--accent);
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-info {
color: var(--muted);
font-size: 0.9rem;
}
/* Hide pagination when not needed */
.pagination-controls.hidden {
display: none;
}

View File

@@ -1,212 +1,296 @@
body { :root {
font-family: 'Inter', sans-serif; --bg: #f9f9f9;
margin: 0; --card: #fff;
padding: 0; --text: #333;
background-color: #f9f9f9; --muted: #666;
color: #333; --accent: #007bff;
min-height: 100vh; /* allow content to grow */ --accent-hover: #0069d9;
overflow-y: auto; /* allow vertical scroll */ --border: #eaeaea;
box-sizing: border-box; --hover: #f8f9fa;
} --code-bg: #f6f8fa;
--editor-border: #ddd;
*, *::before, *::after { }
box-sizing: inherit;
} html.dark {
--bg: #0f172a;
.main-container { --card: #1e293b;
display: flex; --text: #f1f5f9;
flex-wrap: wrap; /* wrap on small screens */ --muted: #94a3b8;
min-height: 100vh; --accent: #3b82f6;
width: 100vw; --accent-hover: #2563eb;
} --border: #334155;
--hover: #334155;
.problem-panel { --code-bg: #1e293b;
flex: 1 1 400px; /* grow/shrink with base 400px */ --editor-border: #475569;
min-width: 300px; }
background: white;
overflow-y: auto; body {
padding: 20px; font-family: "Inter", sans-serif;
border-right: 1px solid #eaeaea; margin: 0;
max-height: 100vh; padding: 0;
} background-color: var(--bg);
color: var(--text);
.editor-container { min-height: 100vh; /* allow content to grow */
flex: 1 1 400px; overflow-y: auto; /* allow vertical scroll */
min-width: 300px; box-sizing: border-box;
display: flex; transition:
flex-direction: column; background-color 0.3s ease,
background: white; color 0.3s ease;
max-height: 100vh; }
overflow: hidden; /* internal scroll handling */
} *,
*::before,
.editor-header { *::after {
padding: 15px 20px; box-sizing: inherit;
border-bottom: 1px solid #eaeaea; }
flex-shrink: 0;
} .main-container {
display: flex;
.editor-wrapper { flex-wrap: wrap; /* wrap on small screens */
flex: 1 1 auto; min-height: 100vh;
display: flex; width: 100vw;
flex-direction: column; }
min-height: 0;
padding: 0 20px; .problem-panel {
overflow-y: auto; flex: 1 1 400px; /* grow/shrink with base 400px */
} min-width: 300px;
background: var(--card);
.problem-header { overflow-y: auto;
display: flex; padding: 20px;
align-items: center; border-right: 1px solid var(--border);
margin-bottom: 20px; max-height: 100vh;
} transition: background-color 0.3s ease;
}
.back-btn {
background: none; .editor-container {
border: none; flex: 1 1 400px;
cursor: pointer; min-width: 300px;
font-size: 16px; display: flex;
color: #666; flex-direction: column;
margin-right: 15px; background: var(--card);
padding: 5px; max-height: 100vh;
} overflow: hidden; /* internal scroll handling */
transition: background-color 0.3s ease;
.back-btn:hover { }
color: #000;
} .editor-header {
padding: 15px 20px;
h1 { border-bottom: 1px solid var(--border);
font-size: 22px; flex-shrink: 0;
font-weight: 600; }
margin: 0;
color: #1a1a1a; .editor-wrapper {
} flex: 1 1 auto;
display: flex;
.problem-desc { flex-direction: column;
line-height: 1.6; min-height: 0;
font-size: 15px; padding: 0 20px;
overflow-wrap: break-word; overflow-y: auto;
} }
.problem-desc pre { .problem-header {
background: #f6f8fa; display: flex;
padding: 12px; align-items: center;
border-radius: 4px; justify-content: space-between;
overflow-x: auto; margin-bottom: 20px;
font-family: 'JetBrains Mono', monospace; }
font-size: 14px;
} .back-btn {
background: none;
.problem-desc code { border: 1px solid var(--border);
background: #f6f8fa; border-radius: 4px;
padding: 2px 4px; cursor: pointer;
border-radius: 3px; font-size: 16px;
font-family: 'JetBrains Mono', monospace; color: var(--muted);
font-size: 14px; margin-right: 15px;
} padding: 6px 10px;
transition: all 0.3s ease;
.editor-actions { }
padding: 15px 0;
display: flex; .back-btn:hover {
justify-content: flex-end; color: var(--text);
flex-shrink: 0; background: var(--hover);
} }
.editor-actions button { .dark-mode-toggle {
background-color: #007bff; background: none;
color: white; border: 1px solid var(--border);
border: none; border-radius: 4px;
padding: 8px 16px; padding: 6px 10px;
border-radius: 4px; cursor: pointer;
cursor: pointer; color: var(--text);
font-weight: 500; font-size: 1.2rem;
font-size: 14px; transition: all 0.3s ease;
} }
.editor-actions button:hover { .dark-mode-toggle:hover {
background-color: #0069d9; background: var(--hover);
} transform: scale(1.05);
}
#editor {
flex: 1 1 auto; html.dark .dark-mode-icon::before {
min-height: 300px; content: "☀";
border: 1px solid #ddd; }
border-radius: 4px;
overflow: auto; html:not(.dark) .dark-mode-icon::before {
max-height: 60vh; content: "⏾";
} }
.result-panel { .dark-mode-icon {
margin-top: 20px; display: inline-block;
padding: 15px; }
background: #f8f9fa;
border-radius: 4px; .dark-mode-icon::before {
margin-bottom: 20px; font-size: 1em;
min-height: 120px; }
overflow-y: auto;
max-height: 30vh; h1 {
} font-size: 22px;
font-weight: 600;
.result-panel h3 { margin: 0;
margin-top: 0; color: var(--text);
font-size: 16px; flex: 1;
margin-bottom: 10px; }
}
.problem-desc {
.result-panel pre { line-height: 1.6;
background: #f6f8fa; font-size: 15px;
padding: 12px; overflow-wrap: break-word;
border-radius: 4px; }
overflow-x: auto;
white-space: pre-wrap; .problem-desc pre {
font-family: 'JetBrains Mono', monospace; background: var(--code-bg);
font-size: 14px; padding: 12px;
margin: 5px 0; border-radius: 4px;
} overflow-x: auto;
font-family: "JetBrains Mono", monospace;
.placeholder { font-size: 14px;
color: #999; border: 1px solid var(--border);
font-style: italic; }
text-align: center;
padding: 20px; .problem-desc code {
} background: var(--code-bg);
padding: 2px 4px;
label { border-radius: 3px;
display: block; font-family: "JetBrains Mono", monospace;
margin-bottom: 5px; font-size: 14px;
font-size: 14px; }
color: #666;
} .editor-actions {
padding: 15px 0;
input[type="text"] { display: flex;
width: 100%; justify-content: flex-end;
padding: 8px; flex-shrink: 0;
border: 1px solid #ddd; }
border-radius: 4px;
margin-bottom: 15px; .editor-actions button {
font-family: 'Inter', sans-serif; background-color: var(--accent);
} color: white;
border: none;
/* Responsive adjustments */ padding: 8px 16px;
@media (max-width: 768px) { border-radius: 4px;
.main-container { cursor: pointer;
flex-direction: column; font-weight: 500;
height: auto; font-size: 14px;
overflow-y: visible; transition: background-color 0.3s ease;
} }
.problem-panel, .editor-container {
flex: none; .editor-actions button:hover {
width: 100%; background-color: var(--accent-hover);
min-width: auto; }
max-height: none;
border-right: none; #editor {
border-bottom: 1px solid #eaeaea; flex: 1 1 auto;
} min-height: 300px;
#editor { border: 1px solid var(--editor-border);
min-height: 400px; border-radius: 4px;
max-height: none; overflow: auto;
} max-height: 60vh;
.result-panel { }
max-height: none;
} .result-panel {
} margin-top: 20px;
padding: 15px;
background: var(--hover);
border-radius: 4px;
margin-bottom: 20px;
min-height: 120px;
overflow-y: auto;
max-height: 30vh;
border: 1px solid var(--border);
transition: background-color 0.3s ease;
}
.result-panel h3 {
margin-top: 0;
font-size: 16px;
margin-bottom: 10px;
}
.result-panel pre {
background: var(--code-bg);
padding: 12px;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
font-family: "JetBrains Mono", monospace;
font-size: 14px;
margin: 5px 0;
border: 1px solid var(--border);
}
.placeholder {
color: var(--muted);
font-style: italic;
text-align: center;
padding: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-size: 14px;
color: var(--muted);
}
input[type="text"] {
width: 100%;
padding: 8px;
border: 1px solid var(--border);
border-radius: 4px;
margin-bottom: 15px;
font-family: "Inter", sans-serif;
background: var(--card);
color: var(--text);
transition: border-color 0.3s ease;
}
input[type="text"]:focus {
outline: none;
border-color: var(--accent);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.main-container {
flex-direction: column;
height: auto;
overflow-y: visible;
}
.problem-panel,
.editor-container {
flex: none;
width: 100%;
min-width: auto;
max-height: none;
border-right: none;
border-bottom: 1px solid var(--border);
}
#editor {
min-height: 400px;
max-height: none;
}
.result-panel {
max-height: none;
}
}

403
src/static/script.js Normal file
View File

@@ -0,0 +1,403 @@
document.addEventListener("DOMContentLoaded", () => {
// Dark mode functionality
const darkModeToggle = document.getElementById("darkModeToggle");
const html = document.documentElement;
// 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");
}
darkModeToggle?.addEventListener("click", () => {
html.classList.toggle("dark");
localStorage.setItem("darkMode", html.classList.contains("dark"));
});
// 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",
);
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
allProblemItems.forEach((item) => {
item.style.display = "none";
});
// Show current page items
filteredProblemItems.slice(startIndex, endIndex).forEach((item) => {
item.style.display = "";
});
// 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 pagination if not needed
if (problemsPagination) {
problemsPagination.classList.toggle("hidden", totalPages <= 1);
}
}
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();
}
// Event listeners for pagination
problemsPrevBtn?.addEventListener("click", () => {
if (currentPage > 1) {
currentPage--;
updatePagination();
}
});
problemsNextBtn?.addEventListener("click", () => {
const totalPages = Math.ceil(filteredProblemItems.length / itemsPerPage);
if (currentPage < totalPages) {
currentPage++;
updatePagination();
}
});
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 {
element: row,
user: row.dataset.user || "",
problem: row.dataset.problem || "",
runtime: parseFloat(row.dataset.runtime) || 0,
memory: parseFloat(row.dataset.memory) || 0,
timestamp: new Date(row.dataset.timestamp || Date.now()).getTime(),
language: row.dataset.language || "",
originalIndex: Array.from(leaderboardBody.children).indexOf(row),
};
});
}
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,
);
problemBests[problem].bestMemory = Math.min(
problemBests[problem].bestMemory,
rowData.memory,
);
});
// Calculate normalized scores for each submission
visibleRows.forEach((rowData) => {
const problemBest = problemBests[rowData.problem];
// Prevent division by zero
const runtimeScore =
problemBest.bestRuntime > 0
? rowData.runtime / problemBest.bestRuntime
: 1;
const memoryScore =
problemBest.bestMemory > 0
? rowData.memory / problemBest.bestMemory
: 1;
// Weighted overall score (70% runtime, 30% memory)
rowData.overallScore = runtimeScore * 0.7 + memoryScore * 0.3;
});
// Sort by overall score (lower is better), then by timestamp (earlier is better for ties)
visibleRows.sort((a, b) => {
const scoreDiff = a.overallScore - b.overallScore;
if (Math.abs(scoreDiff) > 0.000001) return scoreDiff; // 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";
}
});
}
// Apply runtime filter (best/worst per user per problem)
if (runtimeType === "best" || runtimeType === "worst") {
const userProblemGroups = {};
// 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);
});
// 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) {
rowData.element.style.display = "none";
}
});
});
}
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;
}
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");
}
}
// 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
}
// Watch for dark mode changes
new MutationObserver(applyDarkModeToElements).observe(html, {
attributes: true,
attributeFilter: ["class"],
});
});

View File

@@ -1,260 +1,317 @@
/* Reset and base styles */ :root {
* { --bg: #f8f9fa;
margin: 0; --card: #fff;
padding: 0; --text: #333;
box-sizing: border-box; --heading: #2c3e50;
} --heading-secondary: #34495e;
--accent: #3498db;
body { --accent-hover: #2980b9;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; --success: #27ae60;
line-height: 1.6; --success-hover: #229954;
color: #333; --error: #e74c3c;
background-color: #f8f9fa; --muted: #6c757d;
padding: 20px; --muted-hover: #5a6268;
max-width: 1200px; --border: #ddd;
margin: 0 auto; --code-bg: #f4f4f4;
} --success-bg: #d4edda;
--success-text: #155724;
/* Main heading */ --error-bg: #f8d7da;
h1 { --error-text: #721c24;
color: #2c3e50; --hover-bg: #e3f2fd;
margin-bottom: -10px; --shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding-bottom: 3px; }
border-bottom: 3px solid #3498db;
font-size: 2.2em; html.dark {
} --bg: #0f172a;
--card: #1e293b;
h2 { --text: #f1f5f9;
color: #34495e; --heading: #3b82f6;
margin: 30px 0 20px 0; --heading-secondary: #94a3b8;
font-size: 1.5em; --accent: #3b82f6;
} --accent-hover: #2563eb;
--success: #22c55e;
h3 { --success-hover: #16a34a;
color: #34495e; --error: #ef4444;
margin: 25px 0 15px 0; --muted: #64748b;
font-size: 1.3em; --muted-hover: #475569;
} --border: #334155;
--code-bg: #1e293b;
/* Links and buttons */ --success-bg: #065f46;
a { --success-text: #d1fae5;
color: #3498db; --error-bg: #7f1d1d;
text-decoration: none; --error-text: #fecaca;
padding: 8px 16px; --hover-bg: #1e40af;
border-radius: 5px; --shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
transition: background-color 0.3s ease; }
}
/* Reset and base styles */
a:hover { * {
background-color: #e3f2fd; margin: 0;
text-decoration: none; padding: 0;
} box-sizing: border-box;
}
/* Primary action link (Submit New Problem) */
a[href="/problem/new"] { body {
background-color: #3498db; font-family:
color: white; -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 600; line-height: 1.6;
margin-bottom: 30px; color: var(--text);
display: inline-block; background-color: var(--bg);
padding: 12px 24px; padding: 20px;
border-radius: 8px; max-width: 1200px;
} margin: 0 auto;
transition:
a[href="/problem/new"]:hover { background-color 0.3s ease,
background-color: #2980b9; color 0.3s ease;
} }
/* Problem list */ /* Main heading */
ul { h1 {
list-style: none; color: var(--heading);
background: white; margin-bottom: -10px;
border-radius: 8px; padding-bottom: 3px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); border-bottom: 3px solid var(--accent);
padding: 25px; font-size: 2.2em;
margin: 20px 0; }
}
h2 {
li { color: var(--heading-secondary);
padding: 15px 0; margin: 30px 0 20px 0;
border-bottom: 1px solid #eee; font-size: 1.5em;
} }
li:last-child { h3 {
border-bottom: none; color: var(--heading-secondary);
} margin: 25px 0 15px 0;
font-size: 1.3em;
li a { }
display: block;
padding: 12px 20px; /* Links and buttons */
margin: -12px -20px; a {
border-radius: 6px; color: var(--accent);
font-size: 1.1em; text-decoration: none;
} padding: 8px 16px;
border-radius: 5px;
li a:hover { transition: background-color 0.3s ease;
background-color: #f8f9fa; }
transform: translateX(5px);
transition: all 0.2s ease; a:hover {
} background-color: var(--hover-bg);
text-decoration: none;
/* Problem page specific styles */ }
.problem-header {
display: flex; /* Primary action link (Submit New Problem) */
align-items: center; a[href="/problem/new"] {
margin-bottom: 30px; background-color: var(--accent);
gap: 20px; color: white;
} font-weight: 600;
margin-bottom: 30px;
.back-btn { display: inline-block;
background-color: #95a5a6; padding: 12px 24px;
color: white; border-radius: 8px;
border: none; }
padding: 10px 20px;
border-radius: 6px; a[href="/problem/new"]:hover {
cursor: pointer; background-color: var(--accent-hover);
font-size: 14px; }
font-weight: 500;
transition: background-color 0.3s ease; /* Problem list */
} ul {
list-style: none;
.back-btn:hover { background: var(--card);
background-color: #7f8c8d; border-radius: 8px;
} box-shadow: var(--shadow);
padding: 25px;
.problem-desc { margin: 20px 0;
background: white; transition: background-color 0.3s ease;
padding: 30px; }
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); li {
margin-bottom: 30px; padding: 15px 0;
font-size: 1.1em; border-bottom: 1px solid var(--border);
line-height: 1.7; }
}
li:last-child {
/* Editor section */ border-bottom: none;
.editor-section { }
background: white;
padding: 30px; li a {
border-radius: 8px; display: block;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 12px 20px;
margin-bottom: 30px; margin: -12px -20px;
} border-radius: 6px;
font-size: 1.1em;
#editor { }
border: 2px solid #ddd;
border-radius: 8px; li a:hover {
margin: 20px 0; background-color: var(--hover-bg);
height: 400px; transform: translateX(5px);
overflow: hidden; transition: all 0.2s ease;
} }
.editor-actions { /* Problem page specific styles */
margin-top: 20px; .problem-header {
text-align: right; display: flex;
} align-items: center;
margin-bottom: 30px;
form button[type="submit"] { gap: 20px;
background-color: #27ae60; }
color: white;
border: none; .back-btn {
padding: 12px 30px; background-color: var(--muted);
border-radius: 8px; color: white;
cursor: pointer; border: none;
font-size: 16px; padding: 10px 20px;
font-weight: 600; border-radius: 6px;
transition: background-color 0.3s ease; cursor: pointer;
} font-size: 14px;
font-weight: 500;
form button[type="submit"]:hover { transition: background-color 0.3s ease;
background-color: #229954; }
}
.back-btn:hover {
/* Results section */ background-color: var(--muted-hover);
b { }
color: #2c3e50;
display: inline-block; .problem-desc {
margin: 10px 0 5px 0; background: var(--card);
} padding: 30px;
border-radius: 8px;
pre { box-shadow: var(--shadow);
background-color: #f4f4f4; margin-bottom: 30px;
padding: 20px; font-size: 1.1em;
border-radius: 6px; line-height: 1.7;
border-left: 4px solid #3498db; transition: background-color 0.3s ease;
margin: 10px 0 20px 0; }
overflow-x: auto;
font-family: 'JetBrains Mono', 'Courier New', monospace; /* Editor section */
font-size: 14px; .editor-section {
line-height: 1.4; background: var(--card);
} padding: 30px;
border-radius: 8px;
pre[style*="color:red"] { box-shadow: var(--shadow);
border-left-color: #e74c3c; margin-bottom: 30px;
background-color: #fdf2f2; transition: background-color 0.3s ease;
} }
/* Status messages */ #editor {
p[style*="color:green"] { border: 2px solid var(--border);
background-color: #d4edda; border-radius: 8px;
color: #155724; margin: 20px 0;
padding: 15px 20px; height: 400px;
border-radius: 6px; overflow: hidden;
border-left: 4px solid #27ae60; }
margin: 20px 0;
font-weight: 600; .editor-actions {
} margin-top: 20px;
text-align: right;
p[style*="color:red"] { }
background-color: #f8d7da;
color: #721c24; form button[type="submit"] {
padding: 15px 20px; background-color: var(--success);
border-radius: 6px; color: white;
border-left: 4px solid #e74c3c; border: none;
margin: 20px 0; padding: 12px 30px;
font-weight: 600; border-radius: 8px;
} cursor: pointer;
font-size: 16px;
/* Back to Problems link */ font-weight: 600;
a[href="/"] { transition: background-color 0.3s ease;
display: inline-block; }
margin-top: 30px;
background-color: #6c757d; form button[type="submit"]:hover {
color: white; background-color: var(--success-hover);
padding: 10px 20px; }
border-radius: 6px;
font-weight: 500; /* Results section */
} b {
color: var(--heading);
a[href="/"]:hover { display: inline-block;
background-color: #5a6268; margin: 10px 0 5px 0;
} }
/* Responsive design */ pre {
@media (max-width: 768px) { background-color: var(--code-bg);
body { padding: 20px;
padding: 15px; border-radius: 6px;
} border-left: 4px solid var(--accent);
margin: 10px 0 20px 0;
.problem-header { overflow-x: auto;
flex-direction: column; font-family: "JetBrains Mono", "Courier New", monospace;
align-items: flex-start; font-size: 14px;
gap: 15px; line-height: 1.4;
} border: 1px solid var(--border);
transition: background-color 0.3s ease;
h1 { }
font-size: 1.8em;
} pre[style*="color:red"] {
border-left-color: var(--error);
.problem-desc, .editor-section, ul { background-color: var(--error-bg);
padding: 20px; }
}
/* Status messages */
#editor { p[style*="color:green"] {
height: 300px; background-color: var(--success-bg);
} color: var(--success-text);
padding: 15px 20px;
.editor-actions { border-radius: 6px;
text-align: center; border-left: 4px solid var(--success);
} margin: 20px 0;
} font-weight: 600;
}
p[style*="color:red"] {
background-color: var(--error-bg);
color: var(--error-text);
padding: 15px 20px;
border-radius: 6px;
border-left: 4px solid var(--error);
margin: 20px 0;
font-weight: 600;
}
/* Back to Problems link */
a[href="/"] {
display: inline-block;
margin-top: 30px;
background-color: var(--muted);
color: white;
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
}
a[href="/"]:hover {
background-color: var(--muted-hover);
}
/* Responsive design */
@media (max-width: 768px) {
body {
padding: 15px;
}
.problem-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
h1 {
font-size: 1.8em;
}
.problem-desc,
.editor-section,
ul {
padding: 20px;
}
#editor {
height: 300px;
}
.editor-actions {
text-align: center;
}
}

View File

@@ -1,136 +1,146 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en" class="">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Quick Problem Platform</title> <title>Quick Problem Platform</title>
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='index.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='index.css') }}">
<style> </head>
/* Popout explanation */ <style>
#rankingExplanation { /* Popout explanation */
grid-column: 1 / span 2; #rankingExplanation {
max-height: 0; grid-column: 1 / span 2;
opacity: 0; max-height: 0;
overflow: hidden; opacity: 0;
transition: max-height 0.5s ease, opacity 0.4s ease, padding 0.4s ease; overflow: hidden;
padding: 0 12px; transition: max-height 0.5s ease, opacity 0.4s ease, padding 0.4s ease;
} padding: 0 12px;
#rankingExplanation.active { }
max-height: 800px; #rankingExplanation.active {
opacity: 1; max-height: 800px;
padding: 12px; opacity: 1;
} padding: 12px;
#rankInfoBtn.active { color: #2563eb; cursor:pointer; transition: transform 0.3s ease; } }
#rankInfoBtn.active { transform: rotate(90deg); } #rankInfoBtn.active { color: #2563eb; cursor:pointer; transition: transform 0.3s ease; }
#rankInfoBtn.active { transform: rotate(90deg); }
/* Highlight top rank */
.rank-1 { background-color: #f0fff0; } /* Highlight top rank */
.rank-1 td:first-child { font-weight: bold; color: #2e7d32; } .rank-1 td:first-child { font-weight: bold; }
.sort-asc::after { content: " ↑"; } .sort-asc::after { content: " ↑"; }
.sort-desc::after { content: " ↓"; } .sort-desc::after { content: " ↓"; }
</style> </style>
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
<header> <header>
<h1>Quick Problem Platform</h1> <div class="header-content">
</header> <h1>Quick Problem Platform</h1>
<div class="content" id="contentContainer"> <button id="darkModeToggle" class="dark-mode-toggle" title="Toggle dark mode">
<!-- Problems --> <span class="dark-mode-icon"></span>
<section class="card problems-list"> </button>
<div class="search-controls"> </div>
<input type="text" class="search-input" id="problemSearch" placeholder="Search problems..." /> </header>
</div> <div class="content" id="contentContainer">
<h2 style="margin-bottom:6px;font-size:1.1rem">Problems</h2> <!-- Problems -->
<div id="problemsContainer"> <section class="card problems-list">
{% for folder, description, test_code, difficulty in problems %} <div class="search-controls">
<div class="problem-item" data-name="{{ folder.replace('_',' ').title() }}" data-desc="{{ description }}"> <input type="text" class="search-input" id="problemSearch" placeholder="Search problems..." />
<a href="/problem/{{ folder }}">{{ folder.replace('_',' ').title() }}</a> </div>
<span class="difficulty" data-difficulty="{{ difficulty|lower }}">{{ difficulty }}</span> <h2 style="margin-bottom:6px;font-size:1.1rem">Problems</h2>
</div> <div id="problemsContainer">
{% else %} {% for folder, description, test_code, difficulty in problems %}
<div class="problem-item">No problems yet.</div> <div class="problem-item" data-name="{{ folder.replace('_',' ').title() }}" data-desc="{{ description }}" data-difficulty="{{ difficulty|lower }}">
{% endfor %} <a href="/problem/{{ folder }}">{{ folder.replace('_',' ').title() }}</a>
</div> <span class="difficulty" data-difficulty="{{ difficulty|lower }}">{{ difficulty }}</span>
</section> </div>
{% else %}
<!-- Leaderboard --> <div class="problem-item">No problems yet.</div>
<section class="card" id="leaderboardSection"> {% endfor %}
<div class="leaderboard-head"> </div>
<h2 style="font-size:1.1rem;margin:0">Leaderboard <div class="pagination-controls" id="problemsPagination">
<span id="rankInfoBtn" title="How ranking works"></span> <button class="pagination-btn" id="problemsPrevBtn" disabled>← Previous</button>
</h2> <span class="pagination-info" id="problemsPaginationInfo">Page 1 of 1</span>
</div> <button class="pagination-btn" id="problemsNextBtn" disabled>Next →</button>
<div class="leaderboard-controls"> </div>
<input type="text" class="search-input" id="problemFilter" placeholder="Filter by problem..." /> </section>
<select class="filter-select" id="runtimeFilter">
<option value="">All runtimes</option> <!-- Leaderboard -->
<option value="best">Best runtime</option> <section class="card" id="leaderboardSection">
<option value="worst">Worst runtime</option> <div class="leaderboard-head">
</select> <h2 style="font-size:1.1rem;margin:0">Leaderboard
</div> <span id="rankInfoBtn" title="How ranking works"></span>
<div id="leaderboardContainer"> </h2>
<table class="leaderboard-table"> </div>
<thead> <div class="leaderboard-controls">
<tr> <input type="text" class="search-input" id="problemFilter" placeholder="Filter by problem..." />
<th class="sortable" data-sort="rank">Rank</th> <select class="filter-select" id="runtimeFilter">
<th class="sortable" data-sort="user">User</th> <option value="">All runtimes</option>
<th class="sortable" data-sort="problem">Problem</th> <option value="best">Best runtime</option>
<th class="sortable" data-sort="runtime">Runtime (s)</th> <option value="worst">Worst runtime</option>
<th class="sortable" data-sort="memory">Memory (KB)</th> </select>
<th>Line</th> </div>
<th class="sortable" data-sort="timestamp">Timestamp</th> <div id="leaderboardContainer">
</tr> <table class="leaderboard-table">
</thead> <thead>
<tbody id="leaderboardBody"> <tr>
{% for entry in leaderboard %} <th class="sortable" data-sort="rank">Rank</th>
<tr data-user="{{ entry[0] }}" data-problem="{{ problem_titles.get(entry[1], 'Unknown') }}" <th class="sortable" data-sort="user">User</th>
data-runtime="{{ '%.4f'|format(entry[2]) }}" data-memory="{{ entry[3] }}" <th class="sortable" data-sort="problem">Problem</th>
data-timestamp="{{ entry[5] }}"> <th class="sortable" data-sort="runtime">Runtime (s)</th>
<td>{{ loop.index }}</td> <th class="sortable" data-sort="memory">Memory (KB)</th>
<td>{{ entry[0] }}</td> <th>Line</th>
<td> <th class="sortable" data-sort="timestamp">Timestamp</th>
<a href="/problem/{{ problem_titles.get(entry[1], 'Unknown') }}" </tr>
style="color:#2563eb; text-decoration: none;" </thead>
onmouseover="this.style.textDecoration='underline';" <tbody id="leaderboardBody">
onmouseout="this.style.textDecoration='none';"> {% for entry in leaderboard %}
{{ problem_titles.get(entry[1], 'Unknown') }} <tr data-user="{{ entry[0] }}" data-problem="{{ problem_titles.get(entry[1], 'Unknown') }}"
</a> data-runtime="{{ '%.4f'|format(entry[2]) }}" data-memory="{{ entry[3] }}"
</td> data-timestamp="{{ entry[5] }}">
<td>{{ '%.4f'|format(entry[2]) }}</td> <td>{{ loop.index }}</td>
<td>{{ entry[3] }}</td> <td>{{ entry[0] }}</td>
<td>{{ entry[4] if entry[4] else '-' }}</td> <td>
<td>{{ entry[5] }}</td> <a href="/problem/{{ problem_titles.get(entry[1], 'Unknown') }}"
</tr> style="color:#2563eb; text-decoration: none;"
{% else %} onmouseover="this.style.textDecoration='underline';"
<tr><td colspan="7">No leaderboard entries yet.</td></tr> onmouseout="this.style.textDecoration='none';">
{% endfor %} {{ problem_titles.get(entry[1], 'Unknown') }}
</tbody> </a>
</table> </td>
</div> <td>{{ '%.4f'|format(entry[2]) }}</td>
</section> <td>{{ entry[3] }}</td>
<td>{{ entry[4] if entry[4] else '-' }}</td>
<!-- Ranking explanation --> <td>{{ entry[5] }}</td>
<section class="card" id="rankingExplanation"> </tr>
<h2 style="font-size:1.1rem;margin-bottom:6px">How Ranking Works</h2> {% else %}
<p>The leaderboard uses a <strong>weighted scoring system</strong> to determine overall rank:</p> <tr><td colspan="7">No leaderboard entries yet.</td></tr>
<ul style="margin-left: 15px;"> {% endfor %}
<li><strong>Runtime:</strong> How fast the solution runs (lower is better).</li> </tbody>
<li><strong>Memory Usage:</strong> How much memory the solution uses (lower is better).</li> </table>
</ul> </div>
<p>Overall score is calculated as:</p> </section>
<div style="background:#f9f9f9; border-left:4px solid #007acc; padding:1rem; margin:1rem 0;">
<code> <!-- Ranking explanation -->
runtimeScore = yourRuntime / bestRuntime<br> <section class="card" id="rankingExplanation">
memoryScore = yourMemory / bestMemory<br> <h2 style="font-size:1.1rem;margin-bottom:6px">How Ranking Works</h2>
overallScore = runtimeScore × 0.7 + memoryScore × 0.3 <p>The leaderboard uses a <strong>weighted scoring system</strong> to determine overall rank:</p>
</code> <ul style="margin-left: 15px;">
</div> <li><strong>Runtime:</strong> How fast the solution runs (lower is better).</li>
<p>Lower overall scores are better. If scores are equal, earlier submission ranks higher.</p> <li><strong>Memory Usage:</strong> How much memory the solution uses (lower is better).</li>
</section> </ul>
</div> <p>Overall score is calculated as:</p>
</div> <div style="background:#f9f9f9; border-left:4px solid #007acc; padding:1rem; margin:1rem 0;">
<script src="./script.js"></script> <code>
</body> runtimeScore = yourRuntime / bestRuntime<br>
</html> memoryScore = yourMemory / bestMemory<br>
overallScore = runtimeScore × 0.7 + memoryScore × 0.3
</code>
</div>
<p>Lower overall scores are better. If scores are equal, earlier submission ranks higher.</p>
</section>
</div>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>

View File

@@ -1,85 +1,158 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en" class="">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ problem.title }} - Coding Problem</title> <title>{{ problem.title }} - Coding Problem</title>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css" />
<link rel="stylesheet" href="/static/problem.css"> <link rel="stylesheet" href="/static/problem.css" />
<!-- this is stoopid fucking html link for favicon. just cause of flask--> <!-- this is stoopid fucking html link for favicon. just cause of flask-->
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link
<link href="https://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet"> rel="icon"
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> type="image/x-icon"
</head> href="{{ url_for('static', filename='favicon.ico') }}"
<body> />
<div class="main-container"> <link
<div class="problem-panel"> href="https://fonts.cdnfonts.com/css/jetbrains-mono"
<div class="problem-header"> rel="stylesheet"
<button class="back-btn" onclick="window.location.href='/'">← Back</button> />
<h1>{{ problem.title }}</h1> <link
</div> href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
<div class="problem-desc">{{ problem.description | safe | markdown }}</div> rel="stylesheet"
</div> />
</head>
<div class="editor-container"> <body>
<div class="editor-header"> <div class="main-container">
<h2 style="margin:0;font-size:18px;">Submit Your Solution (Python)</h2> <div class="problem-panel">
</div> <div class="problem-header">
<div class="editor-wrapper"> <button class="back-btn" onclick="window.location.href='/'">
<form method="post"> ← Back
<label for="username">Username (optional):</label> </button>
<input type="text" name="username" id="username" placeholder="Anonymous"> <h1>{{ problem.title }}</h1>
<div id="editor"></div> <button
<textarea name="user_code" id="user_code" style="display:none;"></textarea> id="darkModeToggle"
<div class="editor-actions"> class="dark-mode-toggle"
<button type="submit">Run & Submit</button> title="Toggle dark mode"
</div> >
</form> <span class="dark-mode-icon"></span>
</button>
<div class="result-panel"> </div>
<h3>Result</h3> <div class="problem-desc">
{% if result %} {{ problem.description | safe | markdown }}
<p><b>Runtime:</b> {{ '%.4f'|format(result.runtime) }} seconds</p> </div>
<p><b>Output:</b></p> </div>
<pre>{{ result.output }}</pre>
{% if result.error %} <div class="editor-container">
<p><b>Error:</b></p> <div class="editor-header">
<pre>{{ result.error }}</pre> <h2 style="margin: 0; font-size: 18px">
{% endif %} Submit Your Solution (Python)
{% else %} </h2>
<div class="placeholder"> </div>
Your code execution results will appear here <div class="editor-wrapper">
</div> <form method="post">
{% endif %} <label for="username">Username (optional):</label>
</div> <input
</div> type="text"
</div> name="username"
</div> id="username"
placeholder="Anonymous"
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script> />
<script> <div id="editor"></div>
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } }); <textarea
require(['vs/editor/editor.main'], function() { name="user_code"
var editor = monaco.editor.create(document.getElementById('editor'), { id="user_code"
value: '', style="display: none"
language: 'python', ></textarea>
theme: 'vs-light', <div class="editor-actions">
fontFamily: 'JetBrains Mono, monospace', <button type="submit">Run & Submit</button>
fontLigatures: true, </div>
automaticLayout: true, </form>
fontSize: 16,
minimap: { enabled: false } <div class="result-panel">
}); <h3>Result</h3>
document.querySelector('form').addEventListener('submit', function(e) { {% if result %}
var code = editor.getValue(); <p>
if (!code.trim()) { <b>Runtime:</b> {{ '%.4f'|format(result.runtime) }}
alert('Please enter your code before submitting.'); seconds
e.preventDefault(); </p>
return false; <p><b>Output:</b></p>
} <pre>{{ result.output }}</pre>
document.getElementById('user_code').value = code; {% if result.error %}
}); <p><b>Error:</b></p>
}); <pre>{{ result.error }}</pre>
</script> {% endif %} {% else %}
</body> <div class="placeholder">
</html> Your code execution results will appear here
</div>
{% endif %}
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
<script>
// Dark mode functionality
const darkModeToggle = document.getElementById("darkModeToggle");
const html = document.documentElement;
// Load saved dark mode preference
const savedDarkMode = localStorage.getItem("darkMode");
if (
savedDarkMode === "true" ||
(savedDarkMode === null &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
html.classList.add("dark");
}
darkModeToggle.addEventListener("click", () => {
html.classList.toggle("dark");
localStorage.setItem(
"darkMode",
html.classList.contains("dark"),
);
// Update Monaco editor theme
if (window.monacoEditor) {
const isDark = html.classList.contains("dark");
window.monacoEditor.updateOptions({
theme: isDark ? "vs-dark" : "vs-light",
});
}
});
require.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs",
},
});
require(["vs/editor/editor.main"], function () {
const isDark = html.classList.contains("dark");
window.monacoEditor = monaco.editor.create(
document.getElementById("editor"),
{
value: "",
language: "python",
theme: isDark ? "vs-dark" : "vs-light",
fontFamily: "JetBrains Mono, monospace",
fontLigatures: true,
automaticLayout: true,
fontSize: 16,
minimap: { enabled: false },
},
);
document
.querySelector("form")
.addEventListener("submit", function (e) {
var code = window.monacoEditor.getValue();
if (!code.trim()) {
alert("Please enter your code before submitting.");
e.preventDefault();
return false;
}
document.getElementById("user_code").value = code;
});
});
</script>
</body>
</html>

View File

@@ -1,274 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
// Problem search
const problemSearch = document.getElementById('problemSearch');
const problemsContainer = document.getElementById('problemsContainer');
const problemItems = problemsContainer?.querySelectorAll('.problem-item') || [];
problemSearch?.addEventListener('input', () => {
const term = problemSearch.value.toLowerCase().trim();
problemItems.forEach(item => {
const name = item.dataset.name?.toLowerCase() || '';
const desc = item.dataset.desc?.toLowerCase() || '';
const shouldShow = !term || name.includes(term) || desc.includes(term);
item.style.display = shouldShow ? '' : 'none';
});
});
// 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 {
element: row,
user: row.dataset.user || '',
problem: row.dataset.problem || '',
runtime: parseFloat(row.dataset.runtime) || 0,
memory: parseFloat(row.dataset.memory) || 0,
timestamp: new Date(row.dataset.timestamp || Date.now()).getTime(),
language: row.dataset.language || '',
originalIndex: Array.from(leaderboardBody.children).indexOf(row)
};
});
}
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);
problemBests[problem].bestMemory = Math.min(problemBests[problem].bestMemory, rowData.memory);
});
// Calculate normalized scores for each submission
visibleRows.forEach(rowData => {
const problemBest = problemBests[rowData.problem];
// Prevent division by zero
const runtimeScore = problemBest.bestRuntime > 0 ?
rowData.runtime / problemBest.bestRuntime : 1;
const memoryScore = problemBest.bestMemory > 0 ?
rowData.memory / problemBest.bestMemory : 1;
// Weighted overall score (70% runtime, 30% memory)
rowData.overallScore = runtimeScore * 0.7 + memoryScore * 0.3;
});
// Sort by overall score (lower is better), then by timestamp (earlier is better for ties)
visibleRows.sort((a, b) => {
const scoreDiff = a.overallScore - b.overallScore;
if (Math.abs(scoreDiff) > 0.000001) return scoreDiff; // 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';
}
});
}
// Apply runtime filter (best/worst per user per problem)
if (runtimeType === 'best' || runtimeType === 'worst') {
const userProblemGroups = {};
// 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);
});
// 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) {
rowData.element.style.display = 'none';
}
});
});
}
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;
}
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');
}
}
});