v4 Kontrollzentrum: 6 Länder (DE/AT/CH/NL/PL/DK), Währungs-aware, eBay/Retoure + Shop-Seriosität, Preis-Strip, Wetter-Banner (Open-Meteo)+Quellen, availability-first Home, Animationen, Länder-Scope, Preiswecker

This commit is contained in:
2026-06-28 06:47:30 +00:00
parent 600fcfe97d
commit 0915e0988b
16 changed files with 3655 additions and 129 deletions
+43
View File
@@ -0,0 +1,43 @@
---
// Wetter-Kontext (Open-Meteo, kostenlos/ohne Key, EU). Psychologischer Nudge + Quellen.
---
<div class="wxbar reveal" id="wxbar" hidden>
<span class="wxico" id="wx-ico">🌡</span>
<span class="wxtxt" id="wx-txt">Wetterlage wird geladen …</span>
<span class="wxsrc">Quelle: <a href="https://open-meteo.com/" rel="nofollow noopener" target="_blank">Open-Meteo</a> · <a href="https://www.umweltbundesamt.de/themen/gesundheit/umwelteinfluesse-auf-den-menschen/hitze" rel="nofollow noopener" target="_blank">UBA: Hitze &amp; Gesundheit</a></span>
</div>
<style>
.wxbar{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;background:var(--card);border:1px solid var(--line);border-left:4px solid var(--warm);border-radius:12px;padding:.7rem 1rem;box-shadow:var(--shadow-card);font-size:.92rem}
.wxbar.cold{border-left-color:var(--petrol)}
.wxico{font-size:1.2rem}
.wxtxt{font-weight:500}
.wxsrc{margin-left:auto;font-size:.76rem;color:var(--slate)}
.wxsrc a{color:var(--slate);text-decoration:underline}
@media(max-width:620px){.wxsrc{margin-left:0;width:100%}}
</style>
<script is:inline>
(function(){
const bar=document.getElementById('wxbar'), txt=document.getElementById('wx-txt'), ico=document.getElementById('wx-ico');
function show(lat,lng,place){
const u=`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&daily=temperature_2m_max,temperature_2m_min&forecast_days=2&timezone=auto`;
fetch(u).then(r=>r.json()).then(d=>{
const mx=Math.round(d.daily.temperature_2m_max[0]);
const nmin=Math.round(d.daily.temperature_2m_min[1]??d.daily.temperature_2m_min[0]);
const where=place?` in ${place}`:'';
let msg, cold=false;
if(mx>=30||nmin>=20){ico.textContent='🥵';msg=`Tropennacht voraus: bis ${nmin}°C heute Nacht, ${mx}°C am Tag${where}. Klimageräte sind jetzt schnell vergriffen Verfügbarkeit checken.`;}
else if(mx>=25){ico.textContent='🌡';msg=`Warm${where}: bis ${mx}°C. Gute Zeit, dir jetzt Verfügbarkeit zu sichern.`;}
else if(mx<15){ico.textContent='❄️';cold=true;msg=`Aktuell keine Hitzewelle${where} (${mx}°C). Beste Zeit, um günstig & in Ruhe zu kaufen die nächste Hitzewelle kommt bestimmt.`;}
else {ico.textContent='⛅';msg=`Aktuell entspannt${where} (${mx}°C). Trotzdem clever: vor der nächsten Hitzewelle vorsorgen.`;}
if(cold)bar.classList.add('cold');
txt.textContent=msg; bar.hidden=false; window.kfAnim&&window.kfAnim();
}).catch(()=>{bar.remove();});
}
// Standort wenn bereits erlaubt, sonst Default (Deutschland-Mitte/Berlin)
if(navigator.permissions){ navigator.permissions.query({name:'geolocation'}).then(p=>{
if(p.state==='granted'&&navigator.geolocation){navigator.geolocation.getCurrentPosition(pos=>show(pos.coords.latitude,pos.coords.longitude,''),()=>show(52.52,13.405,'Berlin'));}
else show(52.52,13.405,'Berlin');
}).catch(()=>show(52.52,13.405,'Berlin')); }
else show(52.52,13.405,'Berlin');
})();
</script>
File diff suppressed because one or more lines are too long
+1687
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
[
{"id":"idealo","name":"idealo","kind":"preisvergleich"},
{"id":"amazon","name":"Amazon","kind":"online"},
{"id":"mediamarkt","name":"MediaMarkt","kind":"online"}
]
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+20
View File
@@ -27,6 +27,26 @@ import '../styles/global.css';
</nav>
</div></header>
<main><slot /></main>
<script is:inline>
(function(){
const rm = matchMedia('(prefers-reduced-motion: reduce)').matches;
const io = ('IntersectionObserver' in window) ? new IntersectionObserver((es)=>{
es.forEach(e=>{ if(e.isIntersecting){ e.target.classList.add('in'); io.unobserve(e.target); } });
},{rootMargin:'0px 0px -8% 0px'}) : null;
function wire(){
document.querySelectorAll('.reveal:not(.in)').forEach(el=> io? io.observe(el): el.classList.add('in'));
document.querySelectorAll('[data-count]:not([data-done])').forEach(el=>{
const target=+el.getAttribute('data-count'); if(isNaN(target))return; el.setAttribute('data-done','1');
if(rm){ el.textContent=target.toLocaleString('de-DE'); return; }
const dur=900, t0=performance.now();
(function step(t){ const p=Math.min(1,(t-t0)/dur); const v=Math.round(target*(1-Math.pow(1-p,3)));
el.textContent=v.toLocaleString('de-DE'); if(p<1)requestAnimationFrame(step); })(t0);
});
}
document.addEventListener('DOMContentLoaded',wire); wire();
window.kfAnim=wire;
})();
</script>
<footer class="site"><div class="wrap">
<p class="note"><strong>Transparenz:</strong> Einige Links sind Affiliate-Links kaufst du darüber, erhalten wir ggf. eine Provision, für dich ändert sich der Preis nicht. Empfehlungen basieren auf Nutzungsfall, Technik, Testquellen und redaktioneller Einschätzung nicht auf Provision.</p>
<p class="note">Preise und Verfügbarkeit ändern sich während Hitzewellen sehr schnell. Wir halten Daten so aktuell wie möglich, geben aber keine Garantie. Bitte beim Händler final prüfen.</p>
+37 -16
View File
@@ -2,27 +2,48 @@
import Base from '../layouts/Base.astro';
import { allProducts } from '../lib/pricing';
---
<Base title="Verfügbarkeits-Alarm für Klimageräte | Kühlfinder" description="Lass dich benachrichtigen, sobald dein Wunsch-Klimagerät in deiner Nähe verfügbar ist.">
<section class="wrap" style="margin-top:1.6rem;max-width:640px">
<p class="kicker"><span class="u"></span>Verfügbarkeits-Alarm</p>
<h1>Sag mir Bescheid, sobald es verfügbar ist</h1>
<p class="muted">Kostenloser E-Mail-Alarm. Wir melden uns, wenn ein passendes Gerät in deinem Umkreis auftaucht. Keine Garantie auf Preis oder Bestand.</p>
<form class="card" id="alarmform" style="display:grid;gap:.9rem">
<label>Gerät<select name="produkt">
<option value="">Alle / egal</option>
{allProducts.map(p=> <option value={p.slug}>{p.name}</option>)}
</select></label>
<label>PLZ<input name="plz" inputmode="numeric" pattern="[0-9]{5}" placeholder="z. B. 21255" required /></label>
<label>Umkreis<select name="radius"><option>25 km</option><option selected>50 km</option><option>100 km</option></select></label>
<Base title="Verfügbarkeits-Alarm & Preiswecker für Klimageräte | Kühlfinder" description="Lass dich per E-Mail oder Telegram benachrichtigen, sobald dein Wunsch-Klimagerät verfügbar ist oder unter deinen Wunschpreis fällt.">
<section class="wrap" style="margin-top:1.6rem;max-width:680px">
<span class="eyebrow"><span class="dot ok live"></span>Alarm &amp; Preiswecker</span>
<h1>Sei der Erste, wenn dein Gerät da ist oder günstig wird</h1>
<p class="muted">Zwei Wecker in einem: <strong>Verfügbarkeits-Alarm</strong> (Markt in der Nähe hat Bestand) und <strong>Preiswecker</strong> (Preis fällt unter dein Limit). Frühwarnung dank Prognose. E-Mail kostenlos, Telegram im Pro-Pass.</p>
<form class="card alarmform" id="alarmform">
<div class="seg"><button type="button" class="segbtn on" data-mode="stock">Verfügbarkeit</button><button type="button" class="segbtn" data-mode="price">Preiswecker</button></div>
<label>Gerät<select name="produkt"><option value="">Alle / egal</option>{allProducts.map(p=> <option value={p.slug}>{p.name}</option>)}</select></label>
<div class="row2">
<label>PLZ<input name="plz" inputmode="numeric" placeholder="z. B. 21255" required /></label>
<label>Umkreis<select name="radius"><option>25 km</option><option selected>50 km</option><option>100 km</option><option>Grenzregion +Ausland</option></select></label>
</div>
<label id="pricewrap" hidden>Wunschpreis (max.)<input name="zielpreis" inputmode="numeric" placeholder="z. B. 750 €" /></label>
<label>Kanal
<select name="kanal"><option>E-Mail (kostenlos)</option><option>E-Mail + Telegram (Pro)</option></select>
</label>
<label>E-Mail<input name="email" type="email" placeholder="du@example.de" required /></label>
<label style="font-weight:400;font-size:.9rem;display:flex;gap:.5rem"><input type="checkbox" required /> Ich akzeptiere die <a href="/datenschutz">Datenschutzhinweise</a>.</label>
<button class="btn" type="submit">Alarm aktivieren</button>
<label class="chk"><input type="checkbox" required /> Ich akzeptiere die <a href="/datenschutz">Datenschutzhinweise</a>.</label>
<button class="btn" type="submit">Wecker aktivieren</button>
<p class="note" id="alarmmsg"></p>
</form>
<p class="note" style="margin-top:1rem">Später: Telegram/Push und ein Sommer-Pass für engmaschige Alerts. Heute startet der Alarm als kostenloser Lead-Magnet.</p>
<p class="note" style="margin-top:1rem">Hinweis: Demo Alarm-Backend (Listmonk-Mail + Telegram-Bot) folgt. Keine Garantie auf Preis/Bestand. Datenquelle: eigener Kühlfinder-Scraper.</p>
</section>
</Base>
<style>
.alarmform{display:grid;gap:.9rem;margin-top:1rem}
.alarmform label{display:block;font-weight:500;font-size:.92rem}
.alarmform input,.alarmform select{width:100%;margin-top:.3rem;font-family:var(--font-body);font-size:1rem;padding:.6rem .8rem;border:1px solid var(--line);border-radius:10px;background:var(--surface-2)}
.alarmform input:focus,.alarmform select:focus{border-color:var(--petrol);background:#fff}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:.9rem}
.chk{display:flex;gap:.5rem;align-items:center;font-weight:400;font-size:.9rem}.chk input{width:auto;margin:0}
.seg{display:inline-flex;background:var(--surface-2);border:1px solid var(--line);border-radius:999px;padding:3px;gap:3px;width:max-content}
.segbtn{font-family:var(--font-body);font-weight:600;font-size:.88rem;border:0;background:transparent;color:var(--slate);padding:.4rem 1rem;border-radius:999px;cursor:pointer}
.segbtn.on{background:var(--petrol);color:#fff}
@media(max-width:560px){.row2{grid-template-columns:1fr}}
</style>
<script>
const f=document.getElementById('alarmform');
f.addEventListener('submit',e=>{e.preventDefault();document.getElementById('alarmmsg').textContent='Demo: Alarm-Backend folgt (Listmonk/Telegram). Eingaben wurden nicht gespeichert.';});
const pw=document.getElementById('pricewrap');
document.querySelectorAll('.segbtn').forEach(b=>b.addEventListener('click',()=>{
document.querySelectorAll('.segbtn').forEach(x=>x.classList.remove('on'));b.classList.add('on');
pw.hidden = b.dataset.mode!=='price';
}));
f.addEventListener('submit',e=>{e.preventDefault();document.getElementById('alarmmsg').textContent='Demo: Eingaben wurden nicht gespeichert. Alarm-/Preiswecker-Backend (Listmonk + Telegram) folgt.';});
</script>
+83 -61
View File
@@ -2,102 +2,124 @@
import Base from '../layouts/Base.astro';
import ProductCard from '../components/ProductCard.astro';
import { allProducts } from '../lib/pricing';
import WeatherBanner from '../components/WeatherBanner.astro';
const top = allProducts.filter(p => p.tier === 'A').slice(0,3);
const jsonld = {'@context':'https://schema.org','@type':'WebSite',name:'Kühlfinder',url:'https://kuehlfinder.de',
description:'Finde, wo es gerade Klimageräte in Baumärkten und Elektromärkten gibt mit Prognose, Preis-Check und Alarm.'};
description:'Finde live, wo es Klimageräte gibt Baumärkte & Elektromärkte in DE, AT, CH, NL, PL, DK, plus Online/eBay mit Prognose, Preis-Check und Alarm.'};
const Pin='<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M12 21s7-5.6 7-11a7 7 0 1 0-14 0c0 5.4 7 11 7 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>';
const Compass='<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="12" cy="12" r="9"/><path d="m15.5 8.5-2 5-5 2 2-5 5-2Z"/></svg>';
const Bell='<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M6 9a6 6 0 1 1 12 0c0 5 2 6 2 6H4s2-1 2-6Z"/><path d="M10 19a2 2 0 0 0 4 0"/></svg>';
---
<Base title="Kühlfinder Wo gibt es gerade ein Klimagerät? Live-Verfügbarkeit in DE" description="Live-Bestand von Klimageräten in Baumärkten (OBI, Toom, Bauhaus …) und Elektromärkten (MediaMarkt, Saturn …) plus Amazon/idealo mit Verfügbarkeits-Prognose, Preis-Check und Alarm." jsonld={jsonld}>
<Base title="Kühlfinder Wo gibt es gerade ein Klimagerät? Live-Verfügbarkeit (DE/AT/CH/NL/PL/DK)" description="Live-Bestand von Klimageräten in Baumärkten & Elektromärkten in Deutschland und Nachbarländern, plus Online, Amazon & eBay mit Prognose, Preis-Check gegen Wucher und Alarm.">
<section class="hero"><div class="wrap">
<span class="eyebrow"><span class="dot ok"></span>Live-Verfügbarkeit · eigener Scraper</span>
<WeatherBanner />
<span class="eyebrow" style="margin-top:1rem"><span class="dot ok live"></span>Live-Verfügbarkeit · eigener Scraper</span>
<h1>Wo gibt es gerade ein&nbsp;Klimagerät?</h1>
<p class="lead">Klimageräte sind bei Hitze überall vergriffen. Kühlfinder zeigt dir <strong>live, welcher Markt in deiner Nähe eins auf Lager hat</strong> Baumärkte und Elektromärkte mit Verfügbarkeits-Prognose, Preis-Check gegen Wucher und Alarm, sobald wieder was da ist.</p>
<p class="lead">Bei Hitze ist alles vergriffen. Kühlfinder ist dein Kontrollzentrum: <strong>live sehen, welcher Markt in der Nähe eins auf Lager hat</strong> Baumärkte, Elektromärkte, Online &amp; eBay mit Prognose, Preis-Check gegen Wucher und Alarm. <em>Die nächste Hitzewelle kommt bestimmt.</em></p>
<form class="herobar" action="/karte" method="get">
<input name="plz" inputmode="numeric" placeholder="PLZ oder Ort eingeben …" aria-label="PLZ oder Ort" />
<input name="plz" inputmode="numeric" placeholder="PLZ oder Ort …" aria-label="PLZ oder Ort" />
<button class="btn" type="submit">Verfügbarkeit zeigen</button>
<a class="btn ghost" href="/karte" id="locbtn">In meiner Nähe</a>
</form>
<div class="pills" id="livepills">
<span class="pill-stat"><b id="p-ac">…</b> Märkte mit Klimagerät jetzt</span>
<span class="pill-stat"><b id="p-total">…</b> Märkte überwacht</span>
<span class="pill-stat"><b id="p-chains">10</b> Ketten · Baumarkt + Elektro</span>
<div class="livewrap reveal">
<div class="pills">
<span class="pill-stat"><b class="num" id="p-ac" data-count="0">…</b>&nbsp;Märkte mit Klimagerät jetzt</span>
<span class="pill-stat"><b class="num" id="p-total" data-count="0">…</b>&nbsp;Märkte überwacht</span>
<span class="pill-stat"><b class="num">6</b>&nbsp;Länder · <span id="p-chains">…</span> Ketten</span>
</div>
<div class="livecard card">
<div class="lc-head"><span class="dot ok live"></span> Gerade verfügbar</div>
<ul class="livelist" id="livelist"><li class="skel" style="height:40px"></li><li class="skel" style="height:40px"></li><li class="skel" style="height:40px"></li></ul>
<a class="btn sm" href="/karte">Alle auf der Karte →</a>
</div>
</div>
</div></section>
<section class="wrap">
<div class="features">
<a class="feature" href="/karte">
<div class="ic"><i class="ti">◎</i></div>
<h3>Live-Karte</h3>
<p class="muted">Alle Märkte mit aktuellem Bestand auf einen Blick filterbar nach Modell, Kette und Umkreis.</p>
</a>
<a class="feature" href="/finder">
<div class="ic">◆</div>
<h3>60-Sekunden-Finder</h3>
<p class="muted">Welches Gerät passt zu deinem Raum? Ehrliche Empfehlung nur echte Kompressorgeräte.</p>
</a>
<a class="feature" href="/alarm">
<div class="ic">◔</div>
<h3>Prognose &amp; Alarm</h3>
<p class="muted">Wir schätzen, wo die Chance in 7 Tagen am höchsten ist und melden uns per Mail/Telegram.</p>
</a>
<a class="feature reveal" href="/karte"><div class="ic" set:html={Pin}></div><h3>Live-Karte</h3><p class="muted">Alle Märkte mit Bestand auf einen Blick Modell, Kette, Land &amp; Umkreis filtern.</p></a>
<a class="feature reveal" data-d="1" href="/finder"><div class="ic" set:html={Compass}></div><h3>60-Sekunden-Finder</h3><p class="muted">Welches Gerät passt zu deinem Raum? Nur echte Kompressorgeräte.</p></a>
<a class="feature reveal" data-d="2" href="/alarm"><div class="ic" set:html={Bell}></div><h3>Prognose &amp; Alarm</h3><p class="muted">Wir schätzen, wo die Chance in 7 Tagen am höchsten ist Mail &amp; Telegram.</p></a>
</div>
</section>
<section class="wrap"><div class="band reveal">
<div>
<h2>Vergriffen? Wir warnen vor Wucher.</h2>
<p class="muted" style="max-width:42rem">Bei Knappheit verlangen Drittanbieter auf Amazon &amp; eBay schnell das Mehrfache. Kühlfinder kennt den üblichen Preis, zeigt <strong>günstigsten und teuersten</strong> Anbieter, prüft die <strong>Seriosität der Shops</strong> und markiert Wucher.</p>
<div class="bandcta"><a class="btn" href="/karte">Zur Live-Karte</a><a class="btn ghost" href="/portasplit">PortaSplit &amp; Alternativen</a></div>
</div>
<div class="band-card card">
<div class="bc-row ok"><span>eBay günstigster</span><b class="num">607 €</b></div>
<div class="bc-row"><span>Üblicher Preis</span><b class="num">799 €</b></div>
<div class="bc-row danger"><span>⚠ eBay/Marketplace-Wucher</span><b class="num">2.999 €</b></div>
</div>
</div></section>
<section class="wrap">
<div class="section-head"><div><span class="kicker"><span class="u"></span>Grenzregionen</span><h2 style="margin:.2rem 0 0">Auch im Nachbarland verfügbar</h2></div></div>
<div class="countries reveal">
<span class="cflag"><span class="flag">🇩🇪</span> Deutschland</span>
<span class="cflag"><span class="flag">🇦🇹</span> Österreich</span>
<span class="cflag"><span class="flag">🇨🇭</span> Schweiz</span>
<span class="cflag"><span class="flag">🇳🇱</span> Niederlande</span>
<span class="cflag"><span class="flag">🇵🇱</span> Polen</span>
<span class="cflag"><span class="flag">🇩🇰</span> Dänemark</span>
</div>
</section>
<section class="wrap">
<div class="section-head">
<div><span class="kicker"><span class="u"></span>Kuratiert</span><h2 style="margin:.2rem 0 0">Beliebte Geräte und wo es sie gibt</h2></div>
<a class="btn ghost sm" href="/geraete">Alle Geräte &amp; Vergleich</a>
</div>
<div class="grid cards3">{top.map(p => <ProductCard product={p} />)}</div>
<div class="section-head"><div><span class="kicker"><span class="u"></span>Kuratiert</span><h2 style="margin:.2rem 0 0">Beliebte Geräte und wo es sie gibt</h2></div><a class="btn ghost sm" href="/geraete">Alle Geräte</a></div>
<div class="grid cards3">{top.map((p,i) => <div class="reveal" data-d={String(i)}><ProductCard product={p} /></div>)}</div>
</section>
<section class="wrap"><div class="band">
<div>
<h2>Vergriffen? Wir warnen vor Wucher.</h2>
<p class="muted" style="max-width:42rem">Bei Knappheit verlangen Drittanbieter (z. B. Amazon-Marketplace) schnell das Mehrfache. Kühlfinder kennt den üblichen Preis und markiert Wucher und schlägt dir wohnungstaugliche Alternativen vor, die wirklich kühlen.</p>
<div class="bandcta"><a class="btn" href="/karte">Zur Live-Karte</a><a class="btn ghost" href="/portasplit">PortaSplit &amp; Alternativen</a></div>
</div>
<div class="band-card card">
<div class="bc-row"><span>Üblicher Preis</span><b class="num">799 €</b></div>
<div class="bc-row danger"><span>⚠ Marketplace-Wucher</span><b class="num">2.999 €</b></div>
<div class="bc-row ok"><span>Fair ab</span><b class="num">699 €</b></div>
</div>
</div></section>
<section class="wrap"><div class="split">
<div class="card"><h2>So funktioniert's</h2>
<ol class="steps"><li>PLZ eingeben oder Standort nutzen.</li><li>Karte zeigt Märkte mit Bestand + Prognose.</li><li>Hingehen, oder Alarm setzen für später.</li></ol>
</div>
<div class="card warm"><h2>Hitzeschutz statt Panik</h2>
<p class="muted">Hitzewellen nehmen zu. Gute, effiziente Kühlung kann Hitzeschutz sein mit Quellen statt Alarmismus.</p>
<a class="btn ghost" href="/hitzeschutz">Klima &amp; Hitzeschutz</a>
</div>
<div class="card reveal"><h2>So funktioniert's</h2><ol class="steps"><li>PLZ eingeben oder Standort nutzen.</li><li>Karte zeigt Märkte mit Bestand + Prognose.</li><li>Hingehen oder Alarm/Preiswecker setzen.</li></ol></div>
<div class="card warm reveal" data-d="1"><h2>Hitzeschutz statt Panik</h2><p class="muted">Hitzewellen nehmen zu. Gute, effiziente Kühlung kann Hitzeschutz sein mit Quellen statt Alarmismus.</p><a class="btn ghost" href="/hitzeschutz">Klima &amp; Hitzeschutz</a></div>
</div></section>
</Base>
<style>
.hero{background:var(--grad-hero);padding:3.4rem 0 2.6rem}
.hero{background:var(--grad-hero);padding:3.4rem 0 2.8rem}
.hero h1{font-size:clamp(2.3rem,6vw,4rem);max-width:18ch;margin-top:.6rem}
.hero .lead{font-size:1.18rem;max-width:46rem;color:var(--ink-2)}
.herobar{display:flex;gap:.6rem;margin:1.4rem 0 1rem;max-width:520px}
.herobar input{flex:1;font-family:var(--font-body);font-size:1rem;padding:.85rem 1.1rem;border:1px solid var(--line);border-radius:999px;background:#fff;box-shadow:var(--shadow-card)}
.hero .lead{font-size:1.15rem;max-width:48rem;color:var(--ink-2)}
.hero .lead em{color:var(--petrol-deep);font-style:normal;font-weight:600}
.herobar{display:flex;gap:.6rem;margin:1.4rem 0 1.4rem;max-width:640px;flex-wrap:wrap}
.herobar input{flex:1;min-width:180px;font-family:var(--font-body);font-size:1rem;padding:.85rem 1.1rem;border:1px solid var(--line);border-radius:999px;background:#fff;box-shadow:var(--shadow-card)}
.herobar input:focus{border-color:var(--petrol)}
.pills{display:flex;gap:.6rem;flex-wrap:wrap}
section.wrap{margin-top:2.6rem}
.livewrap{display:grid;grid-template-columns:1fr 320px;gap:1.2rem;align-items:start;margin-top:.4rem}
.pills{display:flex;gap:.6rem;flex-wrap:wrap;align-content:start}
.livecard{display:flex;flex-direction:column;gap:.6rem;padding:1rem}
.lc-head{display:flex;align-items:center;gap:.5rem;font-weight:600;font-size:.9rem}
.livelist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.1rem}
.livelist li{display:flex;justify-content:space-between;align-items:center;gap:.5rem;padding:.45rem .1rem;border-bottom:1px solid var(--line);font-size:.88rem}
.livelist .lm{font-weight:600}.livelist .lc{font-size:.78rem;color:var(--slate)}
.livelist .lp{font-family:var(--mono);font-size:.82rem;color:var(--petrol-deep)}
section.wrap{margin-top:2.8rem}
.features{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}
.features .feature{text-decoration:none;color:var(--ink)} .features h3{margin:.2rem 0}
.features .feature{text-decoration:none;color:var(--ink)}.features h3{margin:.2rem 0}
.features .ic{color:var(--petrol-deep)}
.cards3{grid-template-columns:repeat(3,1fr)}
.band{display:grid;grid-template-columns:1.6fr 1fr;gap:1.5rem;align-items:center;background:var(--petrol-soft);border-radius:20px;padding:1.8rem}
.bandcta{display:flex;gap:.6rem;margin-top:1rem;flex-wrap:wrap}
.band-card{display:flex;flex-direction:column;gap:.5rem}
.band-card{display:flex;flex-direction:column;gap:.4rem}
.bc-row{display:flex;justify-content:space-between;align-items:center;padding:.5rem .2rem;border-bottom:1px solid var(--line)}
.bc-row b{font-size:1.1rem}.bc-row.danger b{color:var(--danger)}.bc-row.ok b{color:var(--ok-dot)}
.countries{display:flex;gap:.6rem;flex-wrap:wrap}
.cflag{display:inline-flex;align-items:center;gap:.5rem;background:var(--card);border:1px solid var(--line);border-radius:999px;padding:.45rem .9rem;box-shadow:var(--shadow-card);font-size:.9rem;font-weight:500}
.split{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.card.warm{background:var(--warm-bg)}
.steps{margin:.4rem 0 0;padding-left:1.2rem}.steps li{margin:.3rem 0}
.ic i{font-style:normal}
@media(max-width:820px){.features,.cards3,.split{grid-template-columns:1fr}.band{grid-template-columns:1fr}}
@media(max-width:860px){.livewrap{grid-template-columns:1fr}.features,.cards3,.split{grid-template-columns:1fr}.band{grid-template-columns:1fr}}
</style>
<script>
const euro=(n,c)=>n==null?'':new Intl.NumberFormat('de-DE',{style:'currency',currency:c||'EUR',maximumFractionDigits:0}).format(n);
document.getElementById('locbtn')?.addEventListener('click',e=>{ if(navigator.geolocation){ /* Karte hat eigene Standortfunktion */ } });
fetch('/data/monitor.json').then(r=>r.json()).then(d=>{
document.getElementById('p-ac').textContent=(d.stats.withAC??'').toLocaleString('de-DE');
document.getElementById('p-total').textContent=(d.stats.storesTotal??'').toLocaleString('de-DE');
const ac=document.getElementById('p-ac'), tot=document.getElementById('p-total');
ac.setAttribute('data-count', d.stats.withAC); ac.removeAttribute('data-done');
tot.setAttribute('data-count', d.stats.storesTotal); tot.removeAttribute('data-done');
const chains=new Set(d.stores.map(s=>s.c)); document.getElementById('p-chains').textContent=chains.size;
window.kfAnim && window.kfAnim();
const avail=d.stores.filter(s=>s.any.a && s.any.minP).sort(()=>Math.random()-0.5).slice(0,6);
document.getElementById('livelist').innerHTML = avail.map(s=>`<li><span><span class="flag">${s.flag||''}</span> <span class="lm">${s.c}</span> <span class="lc">${s.ci}</span></span><span class="lp">${euro(s.any.minP,s.cur)}</span></li>`).join('') || '<li class="lc">Aktuell sehr knapp Alarm setzen lohnt sich.</li>';
}).catch(()=>{});
</script>
+75 -49
View File
@@ -1,17 +1,19 @@
---
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 Monitor',applicationCategory:'ShoppingApplication',
description:'Live-Verfügbarkeit von Klimageräten in Baumärkten und Elektromärkten in Deutschland, mit Verfügbarkeits-Prognose.'};
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äte-Monitor Live-Verfügbarkeit in Baumärkten & Elektromärkten | Kühlfinder" description="Sieh live, welche Märkte (OBI, Toom, Bauhaus, Hornbach, MediaMarkt, Saturn, expert, Euronics …) gerade Klimageräte auf Lager haben mit Prognose, Preis-Check und Wucher-Warnung." jsonld={jsonld}>
<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">
<div class="mon-hero-row">
<WeatherBanner />
<div class="mon-hero-row" style="margin-top:1rem">
<div>
<span class="eyebrow"><span class="dot ok"></span>Live · eigener Scraper</span>
<h1>Wo gibt es gerade ein Klimagerät?</h1>
<p class="lead">Live-Bestand in Baumärkten <em>und</em> Elektromärkten plus <strong>Verfügbarkeits-Prognose</strong>, wo die Chance in 7 Tagen am höchsten ist.</p>
<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>
@@ -20,7 +22,7 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
<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">Restocks 7 Tg.</span><b class="num" id="s-restocks"></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>
@@ -28,10 +30,7 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
<section class="wrap mon-grid">
<div class="mapcol">
<div id="map"></div>
<div class="legend">
<span><span class="dot ok"></span>Klimagerät verfügbar</span>
<span><span class="dot" style="background:var(--petrol)"></span>Markt (Cluster)</span>
</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>
@@ -43,7 +42,7 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
<section class="wrap">
<div class="toolbar card">
<div class="tb-row">
<input id="q" type="search" placeholder="Markt, Ort oder PLZ suchen …" />
<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>
@@ -52,50 +51,55 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
<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</button>
<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 id="chainchips"></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">Wir mailen/telegrammen dir, sobald ein Klimagerät in deinem Umkreis auftaucht inkl. Prognose-Frühwarnung.</p></div>
<a class="btn" href="/alarm">Alarm einrichten</a>
<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-Seiten direkt) keine Drittdaten. Aktuell Seed-Stand; Live-Crawler + echte Historie folgen. Prognose = statistische Schätzung, keine Garantie.</p>
<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.8rem 0 1.4rem}
.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.7rem);margin:.5rem 0 .3rem}
.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.6rem;font-weight:600;color:var(--petrol-deep)}
.stats{display:grid;grid-template-columns:repeat(6,1fr);gap:.6rem;margin-top:1.2rem}
.upd{min-width:140px;text-align:center}.upd-t{font-size:1.5rem;font-weight:600;color:var(--petrol-deep)}
.stats{display:grid;grid-template-columns:repeat(6,1fr);gap:.6rem;margin-top:1.1rem}
.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.45rem;line-height:1.1}.stat.ok b{color:var(--ok-dot)}.stat.warn b{color:var(--danger)}
.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:520px;border-radius:16px;overflow:hidden;border:1px solid var(--line);box-shadow:var(--shadow-card)}
#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:560px;overflow:auto}.h-sm{font-size:1.15rem;margin:.1rem 0 .2rem}
.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:180px;font-family:var(--font-body);font-size:.95rem;padding:.6rem .8rem;border:1px solid var(--line);border-radius:10px;background:var(--surface-2)}
#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)}
@@ -103,68 +107,86 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
.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}
#chainchips .chip{font-size:.8rem;padding:.32rem .7rem}
.onlinebar{margin-top:.8rem;display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;padding:.7rem .9rem}
.osrc{display:flex;flex-direction:column;border:1px solid var(--line);border-radius:10px;padding:.4rem .65rem;min-width:120px}
.osrc b{font-size:.82rem}.osrc .num{font-size:.9rem}.osrc small{font-size:.72rem;color:var(--slate)}
#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)}
.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)}
.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 euro=n=>n==null?'':new Intl.NumberFormat('de-DE',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n);
const FLAG={DE:'🇩🇪',AT:'🇦🇹',CH:'🇨🇭',NL:'🇳🇱',PL:'🇵🇱',DK:'🇩🇰'};
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',chains:new Set(),kind:'all',avail:true,chance:false,radius:500};
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,n:m.a?1:0};} return {a:s.any.a,p:s.any.minP,q:s.any.q,n:s.any.n}; }
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.chains.size&&!state.chains.has(s.c))return false;
if(state.q){const q=state.q.toLowerCase();if(!(s.n.toLowerCase().includes(q)||s.ci.toLowerCase().includes(q)||s.z.startsWith(q)))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:[10.2,51.1],zoom:5});
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}}))};
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);
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="fname">${s.n}</span><span class="kindb">${s.kind==='elektro'?'Elektro':'Baumarkt'}</span><div class="faddr">${s.z} ${s.ci}</div></td>
<td class="ce">${stk}</td><td class="ce num">${euro(st.p)}</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}</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`;
@@ -174,15 +196,13 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
function fillStatic(){
$('#h-updated').textContent=DATA.stats.updated;
$$('#stats b[data-k]').forEach(b=>b.textContent=(DATA.stats[b.dataset.k]??'').toLocaleString('de-DE'));
const r7=DATA.feed.filter(f=>(Date.now()-new Date(f.at))/86400000<=7).length;
$('#s-restocks').textContent=r7+(r7>=DATA.feed.length?'+':'');
$('#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>${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:</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">${euro(o.p)}</span><small>${o.model} · ${o.a?(o.wucher?'Wucher':'verfügbar'):'nicht verf.'}</small></a>`).join('');
const chains=[...new Set(DATA.stores.map(s=>s.c))].sort();
$('#chainchips').innerHTML=chains.map(c=>`<button class="chip ghost" data-chain="${c}">${c}</button>`).join(' ');
$$('#chainchips .chip').forEach(b=>b.addEventListener('click',()=>{const c=b.dataset.chain;state.chains.has(c)?(state.chains.delete(c),b.classList.remove('on')):(state.chains.add(c),b.classList.add('on'));render();}));
$('#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;
}
@@ -193,7 +213,7 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
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} Klimagerät verfügbar`).addTo(map);});
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`).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();
});
@@ -206,5 +226,11 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
$('#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;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');
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>
+11
View File
@@ -100,3 +100,14 @@ header.site .brand{letter-spacing:-.02em}
.pill-stat{display:inline-flex;align-items:center;gap:.5rem;background:var(--card);border:1px solid var(--line);border-radius:999px;padding:.45rem .9rem;box-shadow:var(--shadow-card);font-size:.9rem}
.pill-stat b{font-family:var(--mono);color:var(--petrol-deep)}
.section-head{display:flex;align-items:end;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-bottom:1rem}
/* --- v4 animations + polish --- */
@keyframes kf-pulse{0%{box-shadow:0 0 0 0 oklch(0.62 0.13 155 / .5)}70%{box-shadow:0 0 0 8px oklch(0.62 0.13 155 / 0)}100%{box-shadow:0 0 0 0 oklch(0.62 0.13 155 / 0)}}
.dot.live{animation:kf-pulse 2.2s infinite}
.reveal{opacity:0;transform:translateY(16px)}
.reveal.in{opacity:1;transform:none;transition:opacity .6s var(--ease,cubic-bezier(.16,1,.3,1)),transform .6s var(--ease,cubic-bezier(.16,1,.3,1))}
.reveal[data-d="1"]{transition-delay:.08s}.reveal[data-d="2"]{transition-delay:.16s}.reveal[data-d="3"]{transition-delay:.24s}
@keyframes kf-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.anim-in{animation:kf-in .45s cubic-bezier(.16,1,.3,1) both}
.flag{font-size:1.05em;line-height:1}
@media (prefers-reduced-motion: reduce){.reveal{opacity:1!important;transform:none!important}.dot.live{animation:none}}