261 lines
22 KiB
Plaintext
261 lines
22 KiB
Plaintext
---
|
||
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 & 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 & Telegram, mit Prognose-Frühwarnung.</p></div>
|
||
<a class="btn" href="/alarm">Alarm & 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 & 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>
|