From f818f3623b8cf677beb8de79d7ff1950f1220c1e Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Thu, 13 Aug 2026 12:42:13 +0200 Subject: [PATCH] web: lazy-load leaderboard with infinite scrolling --- src/web.rs | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/web.rs b/src/web.rs index aa8cabe..9feb35a 100644 --- a/src/web.rs +++ b/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#"
Noch keine Landkreisüberquerungen für diese Auswahl.
"#, + r#"
Landkreisübertritte werden geladen …
"#, ); } + html.push_str(r#"
"#); + for group in groups { html.push_str("
"); 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 ? 'Intervall' : ''; + return ''+row.rank+''+escapeHtml(row.athlete)+''+escapeHtml(row.crossing_time)+''+escapeHtml(row.activity_id)+''+interval+''+formatPower(row.average_watts)+''+formatHr(row.average_heartrate)+'
'; +} +function leaderboardGroupKey(group){return group.bucket_start+'|'+group.from_county+'|'+group.to_county;} +function leaderboardGroupHtml(group){return '

'+escapeHtml(group.from_county)+' → '+escapeHtml(group.to_county)+'

10-Minuten-Fenster · '+escapeHtml(group.bucket_start)+' · Standort ±10 m
'+group.rows.map(leaderboardRowHtml).join('')+'
#FahrerÜbertrittAktivitätIntervallPowerHRKarte
';} +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;