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:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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 & 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
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because one or more lines are too long
@@ -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
@@ -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 & 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
@@ -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 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 & 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> Märkte mit Klimagerät jetzt</span>
|
||||
<span class="pill-stat"><b class="num" id="p-total" data-count="0">…</b> Märkte überwacht</span>
|
||||
<span class="pill-stat"><b class="num">6</b> 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 & 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 & 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 & Alarm</h3><p class="muted">Wir schätzen, wo die Chance in 7 Tagen am höchsten ist – Mail & 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 & 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 & 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 & 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 & 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 & 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 & 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
@@ -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 & 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 & 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-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 & 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>
|
||||
|
||||
@@ -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}}
|
||||
|
||||
Reference in New Issue
Block a user