Files
kuehlfinder/src/pages/karte.astro
T

261 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import Base from '../layouts/Base.astro';
import WeatherBanner from '../components/WeatherBanner.astro';
import { allProducts } from '../lib/pricing';
const models = allProducts.map(p=>({slug:p.slug,name:p.name}));
const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kühlfinder Kontrollzentrum',applicationCategory:'ShoppingApplication',
description:'Live-Verfügbarkeit von Klimageräten in Baumärkten & Elektromärkten in DE/AT/CH/NL/PL/DK plus Online & eBay, mit Prognose und Preis-Check.'};
---
<Base title="Klimagerät-Kontrollzentrum Live-Verfügbarkeit DE/AT/CH/NL/PL/DK | Kühlfinder" description="Live-Bestand von Klimageräten in Baumärkten und Elektromärkten in Deutschland und Nachbarländern, plus Online, Amazon & eBay mit Prognose, Preis-Check und Wucher-Warnung.">
<section class="mon-hero"><div class="wrap">
<WeatherBanner />
<div class="mon-hero-row" style="margin-top:1rem">
<div>
<span class="eyebrow"><span class="dot ok live"></span>Live · eigener Scraper · 6 Länder</span>
<h1>Dein Klimagerät-Kontrollzentrum</h1>
<p class="lead">Live-Bestand in Baumärkten, Elektromärkten, Online &amp; eBay mit <strong>Verfügbarkeits-Prognose</strong> und <strong>Preis-Check gegen Wucher</strong>.</p>
</div>
<div class="upd card"><span class="note">Aktualisiert</span><div class="num upd-t" id="h-updated"></div><span class="note" id="h-feednote">lädt …</span></div>
</div>
<div class="delaybar reveal">
<span class="dot ok live"></span>
<span>Kostenlos siehst du Live-Daten <strong>~10 Min zeitversetzt</strong>. <a href="/pro">Mit Pro in Echtzeit + Push →</a></span>
</div>
<div class="stats" id="stats">
<div class="stat"><span class="note">Märkte</span><b class="num" data-k="storesTotal"></b></div>
<div class="stat ok"><span class="note">mit Klimagerät</span><b class="num" data-k="withAC"></b></div>
<div class="stat"><span class="note">Baumärkte</span><b class="num" data-k="baumarkt"></b></div>
<div class="stat"><span class="note">Elektromärkte</span><b class="num" data-k="elektro"></b></div>
<div class="stat"><span class="note">Länder</span><b class="num" data-k="countries"></b></div>
<div class="stat warn"><span class="note">Wucher-Alarm</span><b class="num" id="s-wucher"></b></div>
</div>
</div></section>
<section class="wrap mon-grid">
<div class="mapcol">
<div id="map"></div>
<div class="legend"><span><span class="dot ok"></span>verfügbar</span><span><span class="dot" style="background:var(--petrol)"></span>Markt (Cluster)</span></div>
</div>
<aside class="feedcol card">
<h2 class="h-sm">Letzte Verfügbarkeiten</h2>
<p class="note">Restock-Ereignisse, neueste zuerst.</p>
<ol class="feed" id="feed"><li class="skel" style="height:46px"></li><li class="skel" style="height:46px"></li></ol>
</aside>
</section>
<section class="wrap">
<div class="toolbar card">
<div class="tb-row">
<input id="q" type="search" placeholder="Markt, Ort oder PLZ …" />
<select id="model"><option value="all">Alle Modelle</option>{models.map(m=> <option value={m.slug}>{m.name}</option>)}</select>
<button class="btn sm ghost" id="locate" type="button">Standort</button>
<label class="rad">Umkreis <span class="num" id="r-out"></span><input id="radius" type="range" min="10" max="500" step="10" value="500" disabled /></label>
</div>
<div class="tb-row chips">
<button class="chip ghost on" data-toggle="avail">Nur verfügbare</button>
<button class="chip ghost" data-toggle="chance">Beste Prognose</button>
<span class="sep"></span>
<button class="chip on" data-kind="all">Alle Typen</button>
<button class="chip" data-kind="baumarkt">Baumarkt</button>
<button class="chip" data-kind="elektro">Elektro</button>
<span class="sep"></span>
<span class="scope" id="scope"><button class="chip" data-scope="de">🇩🇪 Nur DE</button><button class="chip on" data-scope="all">+ Nachbarländer</button></span>
<span id="countrychips"></span>
<button class="chip ghost" id="reset" type="button">Reset</button>
</div>
</div>
<div class="pricestrip card" id="pricestrip" hidden></div>
<div class="onlinebar card" id="onlinebar"></div>
<div class="tablewrap card">
<table class="mtab"><thead><tr>
<th>Markt</th><th class="ce">Klimageräte</th><th class="ce">ab Preis</th><th class="ce" data-sort="q">Prognose 7 Tg.</th><th class="ce" id="distcol" style="display:none">Entfernung</th>
</tr></thead><tbody id="rows"><tr><td colspan="5"><div class="skel" style="height:200px"></div></td></tr></tbody></table>
<div class="more"><button class="btn ghost" id="more" type="button" style="display:none">Mehr Märkte</button><p class="note" id="rowcount"></p></div>
</div>
<div class="alertcta card">
<div><h2 class="h-sm">Nichts in der Nähe?</h2><p class="note" style="margin:0">Verfügbarkeits-Alarm + Preiswecker Mail &amp; Telegram, mit Prognose-Frühwarnung.</p></div>
<a class="btn" href="/alarm">Alarm &amp; Preiswecker</a>
</div>
<p class="note" style="margin-top:.6rem">Datenquelle: <strong>eigener Kühlfinder-Scraper</strong> (Händler direkt) keine Drittdaten. Aktuell Seed-Stand; Live-Crawler + echte Historie folgen. Prognose = statistische Schätzung. Quellen: Wetter <a href="https://open-meteo.com/" rel="nofollow">Open-Meteo</a>, Hitze/Gesundheit <a href="https://www.umweltbundesamt.de/themen/gesundheit/umwelteinfluesse-auf-den-menschen/hitze" rel="nofollow">UBA</a>.</p>
</section>
</Base>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.7.1/dist/maplibre-gl.css" />
<style>
.mon-hero{background:var(--grad-hero);padding:1.6rem 0 1.4rem}
.mon-hero-row{display:flex;justify-content:space-between;gap:1.2rem;align-items:flex-start}
.mon-hero h1{font-size:clamp(1.8rem,4vw,2.6rem);margin:.5rem 0 .3rem}
.mon-hero .lead{max-width:48rem;color:var(--ink-2);font-size:1.05rem}
.upd{min-width:140px;text-align:center}.upd-t{font-size:1.5rem;font-weight:600;color:var(--petrol-deep)}
.delaybar{display:flex;align-items:center;gap:.5rem;background:var(--warm-bg);border:1px solid var(--line);border-left:4px solid var(--warm);border-radius:10px;padding:.55rem .8rem;margin-top:1rem;font-size:.9rem}
.delaybar a{font-weight:600}
.stats{display:grid;grid-template-columns:repeat(6,1fr);gap:.6rem;margin-top:.8rem}
.stat{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:.6rem .8rem;display:flex;flex-direction:column;gap:.1rem;box-shadow:var(--shadow-card)}
.stat b{font-size:1.4rem;line-height:1.1}.stat.ok b{color:var(--ok-dot)}.stat.warn b{color:var(--danger)}
.mon-grid{display:grid;grid-template-columns:1.7fr 1fr;gap:1rem;margin-top:1.2rem;align-items:start}
.mapcol{position:sticky;top:74px}
#map{height:540px;border-radius:16px;overflow:hidden;border:1px solid var(--line);box-shadow:var(--shadow-card)}
.legend{display:flex;gap:1rem;flex-wrap:wrap;font-size:.8rem;color:var(--slate);margin-top:.5rem}
.feedcol{max-height:580px;overflow:auto}.h-sm{font-size:1.15rem;margin:.1rem 0 .2rem}
.feed{list-style:none;margin:.6rem 0 0;padding:0}
.feed li{display:flex;justify-content:space-between;gap:.6rem;padding:.55rem .2rem;border-bottom:1px solid var(--line)}
.feed .fl b{font-weight:600;font-size:.9rem}.feed .fl span{font-size:.8rem;color:var(--slate)}
.feed .ft{font-size:.78rem;color:var(--slate);white-space:nowrap;font-family:var(--mono)}
.toolbar{margin-top:1.4rem;display:flex;flex-direction:column;gap:.7rem}
.tb-row{display:flex;gap:.6rem;align-items:center;flex-wrap:wrap}
#q{flex:1;min-width:170px;font-family:var(--font-body);font-size:.95rem;padding:.6rem .8rem;border:1px solid var(--line);border-radius:10px;background:var(--surface-2)}
#q:focus{border-color:var(--petrol);background:#fff}
#model{padding:.55rem .7rem;border-radius:10px;border:1px solid var(--line);font-family:var(--font-body)}
.rad{display:flex;align-items:center;gap:.5rem;font-size:.85rem;color:var(--slate)}
.chip{font-family:var(--font-body);font-weight:500;font-size:.85rem;border:1px solid var(--line);background:var(--card);color:var(--ink);padding:.4rem .8rem;border-radius:999px;cursor:pointer;transition:transform .1s,background .15s}
.chip:active{transform:scale(.97)}.chip.on{background:var(--petrol);color:#fff;border-color:var(--petrol)}
.chip.ghost.on{background:var(--ok-bg);color:var(--ok-dot);border-color:var(--ok-dot)}
.sep{width:1px;align-self:stretch;background:var(--line);margin:0 .2rem}
#countrychips .chip{font-size:.82rem;padding:.34rem .7rem}
.scope{display:inline-flex;gap:.3rem}
.pricestrip{margin-top:.8rem;display:flex;gap:1.2rem;flex-wrap:wrap;align-items:center;padding:.8rem 1rem}
.pcell{display:flex;flex-direction:column}.pcell .num{font-size:1.05rem;font-weight:600}.pcell small{font-size:.72rem;color:var(--slate)}
.pcell.ok .num{color:var(--ok-dot)}.pcell.bad .num{color:var(--danger)}
.onlinebar{margin-top:.8rem;display:flex;gap:.5rem;flex-wrap:wrap;align-items:stretch;padding:.7rem .9rem}
.osrc{display:flex;flex-direction:column;gap:.1rem;border:1px solid var(--line);border-radius:10px;padding:.45rem .65rem;min-width:128px}
.osrc b{font-size:.82rem}.osrc .num{font-size:.92rem}.osrc small{font-size:.71rem;color:var(--slate)}
.osrc.wucher{border-color:var(--danger);background:var(--danger-bg)}.osrc.wucher .num{color:var(--danger)}
.trust{font-size:.68rem;color:var(--slate)}
.livebadge{display:inline-flex;align-items:center;gap:.4rem;font-size:.8rem;font-weight:600;color:var(--ok-dot);background:var(--ok-bg);border:1px solid var(--ok-dot);border-radius:999px;padding:.35rem .7rem;align-self:center}
.tablewrap{margin-top:.8rem;padding:0;overflow:hidden}
.mtab{width:100%;border-collapse:collapse;font-size:.9rem}
.mtab thead th{text-align:left;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;color:var(--slate);font-weight:600;padding:.7rem .8rem;border-bottom:1px solid var(--line);background:var(--card);white-space:nowrap}
.mtab th.ce{text-align:center;cursor:pointer}
.mtab td{padding:.6rem .8rem;border-bottom:1px solid var(--line);vertical-align:middle}
.mtab tbody tr{animation:kf-in .3s ease both}
.mtab tbody tr:hover{background:var(--petrol-soft)}
.fname{font-weight:600;font-size:.92rem}.faddr{font-size:.78rem;color:var(--slate)}
.kindb{font-size:.66rem;padding:.05rem .4rem;border-radius:99px;background:var(--surface-2);color:var(--slate);margin-left:.4rem;border:1px solid var(--line)}
.ce .num{font-size:.86rem}
.stk{font-size:.72rem;font-weight:600}.stk.ok{color:var(--ok-dot)}.stk.off{color:var(--slate)}
.prog{display:inline-flex;align-items:center;gap:.4rem}.progbar{width:46px;height:6px;border-radius:99px;background:var(--line);overflow:hidden}.progbar i{display:block;height:100%;background:var(--warm);transition:width .5s}
.more{display:flex;justify-content:space-between;align-items:center;padding:.8rem}
.alertcta{margin-top:1rem;display:flex;justify-content:space-between;align-items:center;gap:1rem;background:var(--petrol-soft);border-color:transparent}
@media(max-width:900px){.stats{grid-template-columns:repeat(3,1fr)}.mon-grid{grid-template-columns:1fr}.mapcol{position:static}.mon-hero-row{flex-direction:column}}
</style>
<script>
import maplibregl from 'maplibre-gl';
const FLAG={DE:'🇩🇪',AT:'🇦🇹',CH:'🇨🇭',NL:'🇳🇱',PL:'🇵🇱',DK:'🇩🇰'};
const LIVE_URL='http://vnqwgi4gq520hs1p4bsgi4w6.46.224.49.66.sslip.io';
const fmt=(n,c)=>n==null?'':new Intl.NumberFormat('de-DE',{style:'currency',currency:c||'EUR',maximumFractionDigits:0}).format(n);
const $=s=>document.querySelector(s),$$=s=>Array.from(document.querySelectorAll(s));
let DATA=null,center=null,shown=120;
const state={q:'',model:'all',countries:new Set(),kind:'all',avail:true,chance:false,radius:500};
const params=new URLSearchParams(location.search);
if(params.get('modell'))state.model=params.get('modell');
if(params.get('plz'))state.q=params.get('plz');
function dist(aLat,aLng,bLat,bLng){const R=6371,dLat=(bLat-aLat)*Math.PI/180,dLng=(bLng-aLng)*Math.PI/180;const x=Math.sin(dLat/2)**2+Math.cos(aLat*Math.PI/180)*Math.cos(bLat*Math.PI/180)*Math.sin(dLng/2)**2;return 2*R*Math.asin(Math.sqrt(x));}
function rel(iso){const d=(Date.now()-new Date(iso))/3600000;if(d<1)return 'vor '+Math.max(1,Math.round(d*60))+' Min';if(d<24)return 'vor '+Math.round(d)+' Std';return 'vor '+Math.round(d/24)+' Tg';}
function status(s){ if(state.model!=='all'){const m=(s.models||[]).find(x=>x.id===state.model); if(!m)return null; return {a:m.a,p:m.a?m.p:null,q:m.q};} return {a:s.any.a,p:s.any.minP,q:s.any.q,n:s.any.n}; }
function pass(s){ const st=status(s); if(!st)return false;
if(state.kind!=='all'&&s.kind!==state.kind)return false;
if(state.countries.size&&!state.countries.has(s.cc))return false;
if(state.q){const q=state.q.toLowerCase();if(!(s.n.toLowerCase().includes(q)||s.ci.toLowerCase().includes(q)||String(s.z).startsWith(q)))return false;}
if(center&&dist(center[1],center[0],s.la,s.lo)>state.radius)return false;
if(state.avail&&!st.a)return false;
return true; }
const stars=t=>'★'.repeat(t)+'☆'.repeat(5-t);
const map=new maplibregl.Map({container:'map',style:'https://tiles.openfreemap.org/styles/liberty',center:[9.6,50.6],zoom:4.4});
map.addControl(new maplibregl.NavigationControl(),'top-right');
function setMap(list){
const all={type:'FeatureCollection',features:list.map(s=>({type:'Feature',geometry:{type:'Point',coordinates:[s.lo,s.la]},properties:{}}))};
const av={type:'FeatureCollection',features:list.filter(s=>status(s)?.a).map(s=>({type:'Feature',geometry:{type:'Point',coordinates:[s.lo,s.la]},properties:{n:s.n,ci:s.ci,t:s.t||null}}))};
map.getSource('all').setData(all);map.getSource('av').setData(av);
}
function priceStrip(){
const el=$('#pricestrip');
if(state.model==='all'||!DATA.priceOverview[state.model]){el.hidden=true;return;}
const o=DATA.priceOverview[state.model]; const name=DATA.products[state.model]?.name||'';
el.hidden=false;
el.innerHTML=`<div class="pcell ok"><span class="num">${fmt(o.ebayLow)}</span><small>eBay günstigster</small></div>
<div class="pcell"><span class="num">${fmt(o.avg)}</span><small>üblicher Preis (UVP)</small></div>
<div class="pcell"><span class="num">${fmt(o.refurb)}</span><small>Retoure/refurbished</small></div>
<div class="pcell bad"><span class="num">${fmt(o.ebayHigh)}</span><small>eBay teuerster ⚠</small></div>
<div class="pcell" style="margin-left:auto"><small>Wucher ab</small><span class="num" style="color:var(--danger)">${fmt(o.wucherFrom)}</span></div>`;
}
function render(){
let list=DATA.stores.filter(pass);
if(center)list.sort((a,b)=>dist(center[1],center[0],a.la,a.lo)-dist(center[1],center[0],b.la,b.lo));
else if(state.chance)list.sort((a,b)=>(status(b)?.q||0)-(status(a)?.q||0));
else list.sort((a,b)=>(status(b)?.a?1:0)-(status(a)?.a?1:0)||(status(b)?.q||0)-(status(a)?.q||0));
setMap(list); priceStrip();
const tb=$('#rows');tb.innerHTML='';
for(const s of list.slice(0,shown)){
const st=status(s);
const stk = st.a?`<span class="stk ok"><span class="dot ok"></span> ${state.model==='all'?st.n+' verfügbar':'verfügbar'}</span>`:`<span class="stk off"><span class="dot off"></span> vergriffen</span>`;
const distTd=center?`<td class="ce num">${dist(center[1],center[0],s.la,s.lo).toFixed(0)} km</td>`:'';
tb.insertAdjacentHTML('beforeend',`<tr><td><span class="flag">${s.flag||FLAG[s.cc]||''}</span> <span class="fname">${s.n}</span><span class="kindb">${s.kind==='elektro'?'Elektro':'Baumarkt'}</span><div class="faddr">${s.z} ${s.ci} · <span title="zuletzt geprüft">Stand vor ${s.t||'?'} Min</span></div></td>
<td class="ce">${stk}</td><td class="ce num">${fmt(st.p,s.cur)}</td>
<td class="ce"><span class="prog"><span class="progbar"><i style="width:${st.q||0}%"></i></span><span class="num">${st.q||0}%</span></span></td>${distTd}</tr>`);
}
$('#rowcount').textContent=`${Math.min(shown,list.length)} von ${list.length} Märkten`;
$('#more').style.display=list.length>shown?'inline-flex':'none';
$('#distcol').style.display=center?'table-cell':'none';
}
function fillStatic(){
$('#h-updated').textContent=DATA.stats.updated;
$$('#stats b[data-k]').forEach(b=>b.textContent=(DATA.stats[b.dataset.k]??'').toLocaleString('de-DE'));
$('#s-wucher').textContent=DATA.online.filter(o=>o.wucher).length;
$('#h-feednote').textContent='Feed live';
$('#feed').innerHTML=DATA.feed.slice(0,18).map(f=>`<li><div class="fl"><b>${FLAG[f.cc]||''} ${f.name}</b><span>${f.model}</span></div><span class="ft">${rel(f.at)}</span></li>`).join('');
$('#onlinebar').innerHTML='<span class="note" style="align-self:center">Online &amp; eBay:</span>'+DATA.online.map(o=>`<a class="osrc ${o.wucher?'wucher':''}" href="${o.url}" target="_blank" rel="nofollow noopener"><b>${o.src}${o.wucher?' ⚠':''}</b><span class="num">${fmt(o.p)}</span><small>${o.cond?o.cond+' · ':''}${o.a?(o.wucher?'Wucher':'verfügbar'):'nicht verf.'}</small><small class="trust" title="Seriosität">${stars(o.trust||3)}</small></a>`).join('');
const ccs=[...new Set(DATA.stores.map(s=>s.cc))];
$('#countrychips').innerHTML=ccs.map(c=>`<button class="chip ghost" data-cc="${c}">${FLAG[c]||''} ${c}</button>`).join(' ');
$$('#countrychips .chip').forEach(b=>b.addEventListener('click',()=>{const c=b.dataset.cc;state.countries.has(c)?(state.countries.delete(c),b.classList.remove('on')):(state.countries.add(c),b.classList.add('on'));render();}));
if(state.model!=='all')$('#model').value=state.model;
if(state.q)$('#q').value=state.q;
loadLive();
}
function loadLive(){
fetch(LIVE_URL+'/api/live').then(r=>r.json()).then(d=>{
if(!d||!d.offers||!d.offers.length)return;
const when=d.generatedAt?new Date(d.generatedAt):null;
const mins=when?Math.max(1,Math.round((Date.now()-when)/60000)):'?';
const items=d.offers.filter(o=>o.price||o.availability==='discontinued').map(o=>{
const av=o.availability==='in_stock'?'im Markt':(o.availability==='online_available'?'online':(o.availability==='discontinued'?'⚠ Seite entfernt (404)':'vergriffen'));
const cls=(o.availability==='out_of_stock')?'':'';
return `<a class="osrc ${o.availability==='discontinued'?'wucher':''}" href="${o.url}" target="_blank" rel="sponsored nofollow noopener"><b>${o.chain} · LIVE</b><span class="num">${fmt(o.price)}</span><small>${o.name} · ${av}</small></a>`;
}).join('');
const bar=$('#onlinebar');
const note = d.blocked?` · ${d.blocked} Quelle(n) blockiert (Proxy nötig)`:'';
bar.insertAdjacentHTML('afterbegin', `<span class="livebadge" title="eigener Live-Crawler"><span class="dot ok live"></span> LIVE gescrapt · Stand vor ${mins} Min${note}</span>`+items);
}).catch(()=>{});
}
map.on('load',()=>{
map.addSource('all',{type:'geojson',data:{type:'FeatureCollection',features:[]},cluster:true,clusterRadius:46,clusterMaxZoom:9});
map.addLayer({id:'cl',type:'circle',source:'all',filter:['has','point_count'],paint:{'circle-color':'#2b6a72','circle-opacity':.82,'circle-radius':['step',['get','point_count'],14,50,18,200,24,500,30]}});
map.addLayer({id:'cl-c',type:'symbol',source:'all',filter:['has','point_count'],layout:{'text-field':['get','point_count_abbreviated'],'text-size':12},paint:{'text-color':'#fff'}});
map.addLayer({id:'pt',type:'circle',source:'all',filter:['!',['has','point_count']],paint:{'circle-color':'#2b6a72','circle-opacity':.6,'circle-radius':4,'circle-stroke-width':1,'circle-stroke-color':'#fff'}});
map.addSource('av',{type:'geojson',data:{type:'FeatureCollection',features:[]}});
map.addLayer({id:'av',type:'circle',source:'av',paint:{'circle-color':'#1d9e75','circle-radius':7,'circle-stroke-width':2,'circle-stroke-color':'#fff'}});
map.on('click','av',e=>{const p=e.features[0].properties;new maplibregl.Popup().setLngLat(e.lngLat).setHTML(`<b>${p.n}</b><br>${p.ci} verfügbar<br><span style=\"color:#666;font-size:.8em\">Stand vor ${p.t||'?'} Min</span>`).addTo(map);});
map.on('click','cl',e=>{const f=map.queryRenderedFeatures(e.point,{layers:['cl']});map.getSource('all').getClusterExpansionZoom(f[0].properties.cluster_id).then(z=>map.easeTo({center:f[0].geometry.coordinates,zoom:z}));});
if(DATA)render();
});
$('#q').addEventListener('input',e=>{state.q=e.target.value.trim();shown=120;render();});
$('#model').addEventListener('change',e=>{state.model=e.target.value;window.track&&window.track('monitor_modell',{modell:state.model});render();});
$$('.chip[data-toggle]').forEach(b=>b.addEventListener('click',()=>{state[b.dataset.toggle]=!state[b.dataset.toggle];b.classList.toggle('on');render();}));
$$('.chip[data-kind]').forEach(b=>b.addEventListener('click',()=>{state.kind=b.dataset.kind;$$('.chip[data-kind]').forEach(x=>x.classList.remove('on'));b.classList.add('on');render();}));
$('#radius').addEventListener('input',e=>{state.radius=+e.target.value;$('#r-out').textContent=state.radius+' km';render();});
$('#more').addEventListener('click',()=>{shown+=120;render();});
$('#reset').addEventListener('click',()=>location.reload());
$$('.mtab th[data-sort]').forEach(th=>th.addEventListener('click',()=>{state.chance=true;render();}));
$('#locate').addEventListener('click',()=>{if(!navigator.geolocation)return;window.track&&window.track('monitor_standort');navigator.geolocation.getCurrentPosition(p=>{center=[p.coords.longitude,p.coords.latitude];$('#radius').disabled=false;state.radius=100;$('#radius').value=100;$('#r-out').textContent='100 km';new maplibregl.Marker({color:'#185fa5'}).setLngLat(center).addTo(map);map.flyTo({center,zoom:8});render();});});
$$('#scope .chip').forEach(b=>b.addEventListener('click',()=>{
$$('#scope .chip').forEach(x=>x.classList.remove('on'));b.classList.add('on');
window.track&&window.track('monitor_scope',{scope:b.dataset.scope});if(b.dataset.scope==='de'){state.countries=new Set(['DE']);} else {state.countries=new Set();}
$$('#countrychips .chip').forEach(c=>c.classList.toggle('on', state.countries.has(c.dataset.cc)));
render();
}));
fetch('/data/monitor.json').then(r=>r.json()).then(d=>{DATA=d;fillStatic();render();}).catch(()=>{$('#rowcount').textContent='Daten konnten nicht geladen werden';});
</script>