This commit is contained in:
2025-08-14 22:05:20 +02:00
parent 04dc638cf0
commit 6079813e2c
12 changed files with 957 additions and 419 deletions

View File

@@ -4,10 +4,17 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Quick Problem Platform</title>
<!--<link rel="favicon" href="/favicon/favicon.ico">-->
<script src="script.js" async defer></script>
<link rel="stylesheet" href="/static/index.css">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='index.css') }}">
<style>
/* Minimal inline CSS to support full-width explanation and popout */
#rankingExplanation {
display:none;
grid-column: 1 / span 2;
transition: all 0.35s ease;
}
#rankInfoBtn.active { color: #2563eb; }
</style>
</head>
<body>
<div class="wrap">
@@ -16,47 +23,30 @@
</header>
<div class="content" id="contentContainer">
<!-- Problems -->
<section class="card problems-list">
<div class="search-controls">
<input
type="text"
class="search-input"
id="problemSearch"
placeholder="Search problems..."
/>
</div>
<h2 style="margin-bottom:6px;font-size:1.1rem">Problems</h2>
<div id="problemsContainer">
{% for folder, description, test_code, difficulty in problems %}
<div class="problem-item" data-name="{{ folder.replace('_',' ').title() }}" data-desc="{{ description }}">
<a href="/problem/{{ folder }}">{{ folder.replace('_',' ').title() }}</a>
<span class="difficulty" data-difficulty="{{ difficulty|lower }}">{{ difficulty }}</span>
<section class="card problems-list">
<div class="search-controls">
<input type="text" class="search-input" id="problemSearch" placeholder="Search problems..." />
</div>
{% else %}
<div class="problem-item">No problems yet.</div>
{% endfor %}
</div>
</section>
<h2 style="margin-bottom:6px;font-size:1.1rem">Problems</h2>
<div id="problemsContainer">
{% for folder, description, test_code, difficulty in problems %}
<div class="problem-item" data-name="{{ folder.replace('_',' ').title() }}" data-desc="{{ description }}">
<a href="/problem/{{ folder }}">{{ folder.replace('_',' ').title() }}</a>
<span class="difficulty" data-difficulty="{{ difficulty|lower }}">{{ difficulty }}</span>
</div>
{% else %}
<div class="problem-item">No problems yet.</div>
{% endfor %}
</div>
</section>
<!-- Leaderboard -->
<section class="card" id="leaderboardSection">
<div class="leaderboard-head">
<h2 style="font-size:1.1rem;margin:0">Leaderboard</h2>
<button class="btn" id="toggleLeaderboard">Hide</button>
</div>
<div class="leaderboard-controls">
<input
type="text"
class="search-input"
id="userSearch"
placeholder="Filter by user..."
/>
<input
type="text"
class="search-input"
id="problemFilter"
placeholder="Filter by problem..."
/>
<input type="text" class="search-input" id="problemFilter" placeholder="Filter by problem..." />
<select class="filter-select" id="runtimeFilter">
<option value="">All runtimes</option>
<option value="best">Best runtime</option>
@@ -67,7 +57,7 @@
<table class="leaderboard-table">
<thead>
<tr>
<th class="sortable" data-sort="rank">Rank</th>
<th class="sortable" data-sort="rank">Rank <span id="rankInfoBtn" title="How ranking works" style="cursor:pointer;"></span></th>
<th class="sortable" data-sort="user">User</th>
<th class="sortable" data-sort="problem">Problem</th>
<th class="sortable" data-sort="runtime">Runtime (s)</th>
@@ -83,7 +73,14 @@
data-timestamp="{{ entry[5] }}">
<td>{{ loop.index }}</td>
<td>{{ entry[0] }}</td>
<td>{{ problem_titles.get(entry[1], 'Unknown') }}</td>
<td>
<a href="/problem/{{ problem_titles.get(entry[1], 'Unknown') }}"
style="color: #0077ff; text-decoration: none;"
onmouseover="this.style.textDecoration='underline';"
onmouseout="this.style.textDecoration='none';">
{{ problem_titles.get(entry[1], 'Unknown') }}
</a>
</td>
<td>{{ '%.4f'|format(entry[2]) }}</td>
<td>{{ entry[3] }}</td>
<td>{{ entry[4] if entry[4] else '-' }}</td>
@@ -96,7 +93,150 @@
</table>
</div>
</section>
<!-- Ranking explanation popout -->
<section class="card" id="rankingExplanation">
<h2 style="font-size:1.1rem;margin-bottom:6px">How Ranking Works</h2>
<p>
The leaderboard uses a <strong>weighted scoring system</strong> to determine overall rank.
It considers multiple metrics for each submission:
</p>
<ul style="margin-left: 15px;">
<li><strong>Runtime:</strong> How fast the solution runs (lower is better).</li>
<li><strong>Memory Usage:</strong> How much memory the solution uses (lower is better).</li>
</ul>
<p>Each metric is normalized against the best in the leaderboard, and the overall score is calculated as:</p>
<div style="background:#f9f9f9; border-left:4px solid #007acc; padding:1rem; margin:1rem 0;">
<code>
runtimeScore = yourRuntime / bestRuntime<br>
memoryScore = yourMemory / bestMemory<br>
overallScore = runtimeScore × 0.7 + memoryScore × 0.3
</code>
</div>
<p>Lower overall scores are better. If two scores are equal, the earlier submission ranks higher.</p>
</section>
</div>
</div>
<!-- Inline JS -->
<script>
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();
problemItems.forEach(item => {
const name = item.dataset.name.toLowerCase();
const desc = item.dataset.desc?.toLowerCase() || '';
item.style.display = (name.includes(term) || desc.includes(term)) ? '' : 'none';
});
});
// Leaderboard
const problemFilter = document.getElementById('problemFilter');
const runtimeFilter = document.getElementById('runtimeFilter');
const leaderboardBody = document.getElementById('leaderboardBody');
const leaderboardRows = Array.from(leaderboardBody.querySelectorAll('tr'));
const sortableHeaders = document.querySelectorAll('.sortable');
let currentSort = { column: null, direction: 'asc' };
function filterLeaderboard() {
const problemTerm = problemFilter.value.toLowerCase();
const runtimeType = runtimeFilter.value;
leaderboardRows.forEach(row => {
const problem = row.dataset.problem.toLowerCase();
const runtime = parseFloat(row.dataset.runtime);
const showProblem = problem.includes(problemTerm);
let showRuntime = true;
if(runtimeType === 'best') {
const userProblemRows = leaderboardRows.filter(r => r.dataset.user === row.dataset.user && r.dataset.problem === row.dataset.problem);
const best = Math.min(...userProblemRows.map(r=>parseFloat(r.dataset.runtime)));
showRuntime = runtime === best;
} else if(runtimeType === 'worst') {
const userProblemRows = leaderboardRows.filter(r => r.dataset.user === row.dataset.user && r.dataset.problem === row.dataset.problem);
const worst = Math.max(...userProblemRows.map(r=>parseFloat(r.dataset.runtime)));
showRuntime = runtime === worst;
}
row.style.display = (showProblem && showRuntime) ? '' : 'none';
});
}
problemFilter?.addEventListener('input', filterLeaderboard);
runtimeFilter?.addEventListener('change', filterLeaderboard);
function getCellValue(row, column) {
const index = Array.from(document.querySelectorAll('th')).findIndex(th => th.dataset.sort===column);
let val = row.cells[index].textContent.trim();
if(['runtime','memory','rank'].includes(column)) return parseFloat(val)||0;
if(column==='timestamp') return new Date(val).getTime();
return val.toLowerCase();
}
function compareValues(a,b,direction) {
if(typeof a==='number' && typeof b==='number') return direction==='asc'?a-b:b-a;
if(a<b) return direction==='asc'?-1:1;
if(a>b) return direction==='asc'?1:-1;
return 0;
}
function calculateOverallRank() {
const rows = Array.from(leaderboardBody.querySelectorAll('tr'));
const runtimes = rows.map(r=>parseFloat(r.dataset.runtime)||Infinity);
const memories = rows.map(r=>parseFloat(r.dataset.memory)||Infinity);
const minRuntime = Math.min(...runtimes);
const minMemory = Math.min(...memories);
rows.forEach(row=>{
const runtimeScore = (parseFloat(row.dataset.runtime)||Infinity)/minRuntime;
const memoryScore = (parseFloat(row.dataset.memory)||Infinity)/minMemory;
const score = runtimeScore*0.7 + memoryScore*0.3;
row.dataset.overallScore = score;
});
}
function sortLeaderboard(column,direction) {
let rows = Array.from(leaderboardBody.querySelectorAll('tr'));
if(column==='rank') {
calculateOverallRank();
rows.sort((a,b)=>parseFloat(a.dataset.overallScore)-parseFloat(b.dataset.overallScore));
rows.forEach((row,index)=>row.cells[0].textContent = index+1);
} else {
rows.sort((a,b)=>compareValues(getCellValue(a,column),getCellValue(b,column),direction));
}
rows.forEach(row=>leaderboardBody.appendChild(row));
}
sortableHeaders.forEach(header=>{
header.addEventListener('click',()=>{
const column = header.dataset.sort;
sortableHeaders.forEach(h=>h.classList.remove('sort-asc','sort-desc'));
if(currentSort.column===column) currentSort.direction = currentSort.direction==='asc'?'desc':'asc';
else { currentSort.column=column; currentSort.direction='asc'; }
header.classList.add(`sort-${currentSort.direction}`);
sortLeaderboard(column,currentSort.direction);
});
});
// Rank info popout
const rankInfoBtn = document.getElementById('rankInfoBtn');
const rankingExplanation = document.getElementById('rankingExplanation');
rankInfoBtn?.addEventListener('click',()=>{
if(rankingExplanation.style.display==='none' || rankingExplanation.style.display==='') {
rankingExplanation.style.display='block';
rankInfoBtn.classList.add('active');
} else {
rankingExplanation.style.display='none';
rankInfoBtn.classList.remove('active');
}
});
});
</script>
</body>
</html>