web: lazy-load leaderboard with infinite scrolling
This commit is contained in:
parent
b26486c110
commit
f818f3623b
1 changed files with 36 additions and 4 deletions
40
src/web.rs
40
src/web.rs
|
|
@ -50,9 +50,7 @@ pub async fn index(
|
|||
_ => None,
|
||||
};
|
||||
|
||||
let groups = leaderboard::build(&state.db, valid_group_id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let groups = Vec::new();
|
||||
|
||||
let available_groups = match current.as_ref() {
|
||||
Some((id, _)) => futures_groups(&state, *id).await.unwrap_or_default(),
|
||||
|
|
@ -419,10 +417,12 @@ fn render_index(
|
|||
|
||||
if groups.is_empty() {
|
||||
html.push_str(
|
||||
r#"<div class="card">Noch keine Landkreisüberquerungen für diese Auswahl.</div>"#,
|
||||
r#"<div id="leaderboard-status" class="card">Landkreisübertritte werden geladen …</div>"#,
|
||||
);
|
||||
}
|
||||
|
||||
html.push_str(r#"<div id="leaderboard-list"></div><div id="leaderboard-sentinel" aria-hidden="true"></div>"#);
|
||||
|
||||
for group in groups {
|
||||
html.push_str("<section class=\"card\">");
|
||||
html.push_str(&format!(
|
||||
|
|
@ -711,6 +711,38 @@ document.querySelectorAll('[data-time]').forEach(el=>{
|
|||
el.textContent=fmtTime(el.dataset.time);
|
||||
el.title=Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
});
|
||||
function escapeHtml(value){return String(value).replace(/[&<>\"']/g,ch=>({'&':'&','<':'<','>':'>','\"':'"',"'":'''}[ch]));}
|
||||
function formatPower(value){return value == null ? '—' : Number(value).toFixed(0)+' W';}
|
||||
function formatHr(value){return value == null ? '—' : Number(value).toFixed(0)+' bpm';}
|
||||
function leaderboardRowHtml(row){
|
||||
const interval=row.interval_url ? '<a href="'+escapeHtml(row.interval_url)+'" target="_blank" rel="noreferrer">Intervall</a>' : '<span class="small">—</span>';
|
||||
return '<tr><td class="rank">'+row.rank+'</td><td>'+escapeHtml(row.athlete)+'</td><td class="time" data-time="'+escapeHtml(row.crossing_time)+'">'+escapeHtml(row.crossing_time)+'</td><td><a href="'+escapeHtml(row.activity_url)+'">'+escapeHtml(row.activity_id)+'</a></td><td>'+interval+'</td><td>'+formatPower(row.average_watts)+'</td><td>'+formatHr(row.average_heartrate)+'</td><td><div class="mini-map" data-lat="'+row.lat+'" data-lon="'+row.lon+'"></div></td></tr>';
|
||||
}
|
||||
function leaderboardGroupKey(group){return group.bucket_start+'|'+group.from_county+'|'+group.to_county;}
|
||||
function leaderboardGroupHtml(group){return '<section class="card leaderboard-group"><h2>'+escapeHtml(group.from_county)+' → '+escapeHtml(group.to_county)+'</h2><div class="small">10-Minuten-Fenster · '+escapeHtml(group.bucket_start)+' · Standort ±10 m</div><div class="group-map" data-lat="'+group.crossing_lat+'" data-lon="'+group.crossing_lon+'"></div><table><thead><tr><th>#</th><th>Fahrer</th><th>Übertritt</th><th>Aktivität</th><th>Intervall</th><th>Power</th><th>HR</th><th>Karte</th></tr></thead><tbody>'+group.rows.map(leaderboardRowHtml).join('')+'</tbody></table></section>';}
|
||||
function initLeaderboardElements(root){
|
||||
root.querySelectorAll('[data-time]').forEach(el=>{el.textContent=fmtTime(el.dataset.time);el.title=Intl.DateTimeFormat().resolvedOptions().timeZone;});
|
||||
root.querySelectorAll('.group-map').forEach(el=>{if(el.dataset.mapReady)return;const lat=Number(el.dataset.lat),lon=Number(el.dataset.lon);if(!Number.isFinite(lat)||!Number.isFinite(lon))return;el.dataset.mapReady='1';const map=L.map(el,{scrollWheelZoom:false}).setView([lat,lon],15);tileLayer(map);L.circle([lat,lon],{radius:10}).addTo(map);L.marker([lat,lon]).addTo(map);});
|
||||
root.querySelectorAll('.mini-map').forEach(el=>{if(el.dataset.mapReady)return;const lat=Number(el.dataset.lat),lon=Number(el.dataset.lon);if(!Number.isFinite(lat)||!Number.isFinite(lon))return;el.dataset.mapReady='1';const map=L.map(el,{zoomControl:false,dragging:false,scrollWheelZoom:false,doubleClickZoom:false,touchZoom:false}).setView([lat,lon],16);tileLayer(map);L.circle([lat,lon],{radius:10}).addTo(map);L.marker([lat,lon]).addTo(map);});
|
||||
}
|
||||
const leaderboardList=document.getElementById('leaderboard-list');
|
||||
if(leaderboardList){
|
||||
const status=document.getElementById('leaderboard-status'),sentinel=document.getElementById('leaderboard-sentinel'),groups=new Map();
|
||||
let offset=0,loading=false,hasMore=true; const pageSize=50;
|
||||
async function loadLeaderboard(){
|
||||
if(loading||!hasMore)return; loading=true;
|
||||
try{
|
||||
const params=new URLSearchParams({offset:String(offset),limit:String(pageSize)}),selected=new URLSearchParams(location.search).get('group_id');
|
||||
if(selected)params.set('group_id',selected);
|
||||
const response=await fetch('/api/leaderboard?'+params,{headers:{Accept:'application/json'}}); if(!response.ok)throw new Error('HTTP '+response.status);
|
||||
const page=await response.json();
|
||||
if(offset===0&&page.groups.length===0){status.textContent='Noch keine Landkreisüberquerungen für diese Auswahl.';status.style.display='block';}else{status.style.display='none';}
|
||||
for(const group of page.groups){const key=leaderboardGroupKey(group),existing=groups.get(key);if(existing){const tbody=existing.querySelector('tbody');tbody.insertAdjacentHTML('beforeend',group.rows.map(leaderboardRowHtml).join(''));initLeaderboardElements(tbody);}else{leaderboardList.insertAdjacentHTML('beforeend',leaderboardGroupHtml(group));const section=leaderboardList.lastElementChild;groups.set(key,section);initLeaderboardElements(section);}}
|
||||
offset=page.next_offset;hasMore=page.has_more;if(!hasMore)sentinel.style.display='none';
|
||||
}catch(error){console.error(error);status.textContent='Landkreisübertritte konnten nicht geladen werden.';status.style.display='block';}finally{loading=false;}
|
||||
}
|
||||
const observer=new IntersectionObserver(entries=>{if(entries.some(entry=>entry.isIntersecting))loadLeaderboard();},{rootMargin:'800px 0px'});observer.observe(sentinel);loadLeaderboard();
|
||||
}
|
||||
document.querySelectorAll('.group-map').forEach(el=>{
|
||||
const lat=Number(el.dataset.lat),lon=Number(el.dataset.lon);
|
||||
if(!Number.isFinite(lat)||!Number.isFinite(lon)) return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue