163 lines
6.4 KiB
HTML
163 lines
6.4 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Leaderboard — WeightTracker{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<h1>🏆 Leaderboard</h1>
|
|
<p>Ranked by % body weight lost. May the best loser win!</p>
|
|
</div>
|
|
|
|
<!-- Progress-Over-Time Chart -->
|
|
<div class="card" style="margin-bottom: 1.5rem;">
|
|
<div class="card-header">
|
|
<h2>📈 Progress Over Time</h2>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="chart-filters">
|
|
<div class="filter-group">
|
|
<label for="filterStart">From</label>
|
|
<input type="date" id="filterStart" class="form-input">
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="filterEnd">To</label>
|
|
<input type="date" id="filterEnd" class="form-input">
|
|
</div>
|
|
<div class="filter-group filter-group-people">
|
|
<label>People</label>
|
|
<div class="person-filter-list" id="personFilterList">
|
|
{% for u in ranked %}
|
|
<label class="person-checkbox">
|
|
<input type="checkbox" value="{{ u.id }}" checked>
|
|
<span>{{ u.display_name or u.username }}</span>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-container" style="position: relative; height: 380px;">
|
|
<canvas id="progressChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Leaderboard Table -->
|
|
<div class="card">
|
|
{% if ranked %}
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Rank</th>
|
|
<th>Name</th>
|
|
<th>Start</th>
|
|
<th>Current</th>
|
|
<th>Lost (kg)</th>
|
|
<th>Lost (%)</th>
|
|
<th>Goal Progress</th>
|
|
<th>Check-ins</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for u in ranked %}
|
|
<tr>
|
|
<td>
|
|
<span
|
|
class="rank-badge {{ 'rank-1' if loop.index == 1 else 'rank-2' if loop.index == 2 else 'rank-3' if loop.index == 3 else 'rank-other' }}">
|
|
{{ loop.index }}
|
|
</span>
|
|
</td>
|
|
<td style="font-weight: 600; color: var(--text-primary);">{{ u.display_name or u.username }}</td>
|
|
<td>{{ '%.1f' % (u.starting_weight_kg | float) if u.starting_weight_kg else '—' }}</td>
|
|
<td>{{ '%.1f' % (u.current_weight | float) if u.current_weight else '—' }}</td>
|
|
<td>
|
|
<span
|
|
style="color: {{ 'var(--success)' if u.weight_lost > 0 else 'var(--danger)' if u.weight_lost < 0 else 'var(--text-muted)' }}; font-weight: 600;">
|
|
{{ '%+.1f' % (-u.weight_lost) if u.weight_lost != 0 else '0.0' }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span
|
|
style="color: {{ 'var(--success)' if u.pct_lost > 0 else 'var(--danger)' if u.pct_lost < 0 else 'var(--text-muted)' }}; font-weight: 700;">
|
|
{{ '%.1f' % u.pct_lost }}%
|
|
</span>
|
|
</td>
|
|
<td>
|
|
{% if u.goal_progress is not none %}
|
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
<div class="progress-bar-track" style="flex: 1;">
|
|
<div class="progress-bar-fill" style="width: {{ u.goal_progress }}%;"></div>
|
|
</div>
|
|
<span style="font-size: 0.75rem; color: var(--text-muted); white-space: nowrap;">{{ '%.0f' %
|
|
u.goal_progress }}%</span>
|
|
</div>
|
|
{% else %}
|
|
<span style="color: var(--text-muted);">—</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ u.total_checkins }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🏆</div>
|
|
<h3>No competitors yet</h3>
|
|
<p>Start checking in to appear on the leaderboard!</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script src="{{ url_for('static', filename='js/charts.js') }}"></script>
|
|
<script>
|
|
let _progressChart = null;
|
|
let _debounceTimer = null;
|
|
|
|
function loadProgressChart() {
|
|
const start = document.getElementById('filterStart').value;
|
|
const end = document.getElementById('filterEnd').value;
|
|
|
|
// Gather checked user IDs
|
|
const checks = document.querySelectorAll('#personFilterList input[type=checkbox]:checked');
|
|
const userIds = Array.from(checks).map(cb => cb.value).join(',');
|
|
|
|
const params = new URLSearchParams();
|
|
if (start) params.set('start', start);
|
|
if (end) params.set('end', end);
|
|
if (userIds) params.set('user_ids', userIds);
|
|
|
|
fetch('/api/progress-over-time?' + params.toString())
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (_progressChart) {
|
|
_progressChart.destroy();
|
|
_progressChart = null;
|
|
}
|
|
if (data.users && data.users.length > 0) {
|
|
_progressChart = createProgressChart('progressChart', data.users);
|
|
}
|
|
});
|
|
}
|
|
|
|
function debouncedLoad() {
|
|
clearTimeout(_debounceTimer);
|
|
_debounceTimer = setTimeout(loadProgressChart, 300);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Initial load
|
|
loadProgressChart();
|
|
|
|
// Wire up filter changes
|
|
document.getElementById('filterStart').addEventListener('change', debouncedLoad);
|
|
document.getElementById('filterEnd').addEventListener('change', debouncedLoad);
|
|
document.querySelectorAll('#personFilterList input[type=checkbox]').forEach(cb => {
|
|
cb.addEventListener('change', loadProgressChart);
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %} |