v3: eigener Scraper (OBI live verifiziert) statt braucheklima-Daten; Elektromärkte (MediaMarkt/Saturn/expert/Euronics)+Amazon; Startseite reframed 'wo gibt es Klimageräte'; SaaS-Design; Monitor any-AC + Modell-Filter
This commit is contained in:
@@ -1,20 +1,14 @@
|
||||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import { allProducts, getProduct, priceIntel, offersFor, storeById, euro } from '../../lib/pricing';
|
||||
import { allProducts, getProduct, priceIntel, euro } from '../../lib/pricing';
|
||||
export function getStaticPaths(){ return allProducts.map(p => ({ params: { slug: p.slug } })); }
|
||||
const { slug } = Astro.params;
|
||||
const product = getProduct(slug!)!;
|
||||
const pi = priceIntel(product);
|
||||
const typeLabel = product.type === 'mobile_split' ? 'Mobile Split-Klimaanlage' : 'Monoblock mit Abluftschlauch';
|
||||
const availStores = offersFor(product.id)
|
||||
.filter(o => o.sourceType==='store' && ['in_stock','low_stock'].includes(o.availability))
|
||||
.map(o => ({...o, store: storeById(o.sourceId)}));
|
||||
const alternatives = allProducts.filter(p => p.id!==product.id && p.type===product.type).slice(0,3);
|
||||
const jsonld = {
|
||||
'@context':'https://schema.org','@type':'Product',name:product.name,brand:product.brand,
|
||||
category:'Mobile Klimaanlage',
|
||||
offers:{'@type':'AggregateOffer',priceCurrency:'EUR',lowPrice:pi.lowestLegit ?? product.uvp,highPrice:product.uvp,offerCount:offersFor(product.id).length}
|
||||
};
|
||||
const jsonld = {'@context':'https://schema.org','@type':'Product',name:product.name,brand:product.brand,category:'Mobile Klimaanlage',
|
||||
offers:{'@type':'AggregateOffer',priceCurrency:'EUR',lowPrice:pi.lowestLegit,highPrice:product.uvp}};
|
||||
---
|
||||
<Base title={`${product.name} – Test, Eignung & Verfügbarkeit | Kühlfinder`} description={product.shortVerdict} jsonld={jsonld}>
|
||||
<section class="wrap" style="margin-top:1.4rem">
|
||||
@@ -24,75 +18,43 @@ const jsonld = {
|
||||
<h1>{product.name}</h1>
|
||||
<p class="muted">{typeLabel} · {product.btuh.toLocaleString('de-DE')} BTU · {product.coolingKw} kW · {product.roomSizeM2[0]}–{product.roomSizeM2[1]} m² · EEK {product.efficiencyCooling}</p>
|
||||
<p class="lead">{product.shortVerdict}</p>
|
||||
|
||||
<div class="price-box card">
|
||||
<div class="status-grid">
|
||||
<div><span class="note">Üblicher Preis</span><br /><strong>{euro(pi.fairPrice)}</strong></div>
|
||||
<div><span class="note">Seriös aktuell ab</span><br /><strong>{euro(pi.lowestLegit)}</strong></div>
|
||||
<div><span class="note">Ø seriös</span><br /><strong>{euro(pi.avgLegit)}</strong></div>
|
||||
<div><span class="note">Märkte mit Bestand</span><br /><strong>{pi.storesAvailable}</strong></div>
|
||||
<div class="sg">
|
||||
<div><span class="note">Üblicher Preis (UVP)</span><br /><strong class="num">{euro(pi.fairPrice)}</strong></div>
|
||||
<div><span class="note">Fair ab</span><br /><strong class="num">{euro(pi.lowestLegit)}</strong></div>
|
||||
<div><span class="note">Wucher ab</span><br /><strong class="num">{euro(pi.wucherThreshold)}</strong></div>
|
||||
</div>
|
||||
{pi.wucher && <p class="wucher">⚠ <strong>Wucher-Warnung:</strong> bis {euro(pi.wucher.price)} bei Drittanbietern – über {Math.round((pi.wucher.price/pi.fairPrice-1)*100)} % über üblich. Nicht überteuert kaufen.</p>}
|
||||
<div class="status-cta">
|
||||
<a class="btn sm" href={`/karte?produkt=${product.slug}`}>Auf der Karte suchen</a>
|
||||
<a class="btn sm" href={`/karte?modell=${product.slug}`}>Auf der Karte suchen</a>
|
||||
<a class="btn sm ghost" href={`/alarm?produkt=${product.slug}`}>Alarm setzen</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product.type==='monoblock' && <p class="hint">Wichtig: Monoblock-Geräte brauchen eine gute Fensterabdichtung. Ohne Abdichtung wird ständig warme Luft nachgezogen – das kostet Leistung und Strom.</p>}
|
||||
{product.type==='mobile_split' && <p class="hint">Wichtig: Das Außengerät muss sicher und rücksichtsvoll platziert werden. Prüfe Fensterbank/Balkon, Nachbarn und Mietvertrag.</p>}
|
||||
|
||||
{product.type==='monoblock' && <p class="hint">Wichtig: Monoblock-Geräte brauchen eine gute Fensterabdichtung. Ohne Abdichtung wird ständig warme Luft nachgezogen.</p>}
|
||||
{product.type==='mobile_split' && <p class="hint">Wichtig: Das Außengerät sicher und rücksichtsvoll platzieren – Fensterbank/Balkon, Nachbarn, Mietvertrag prüfen.</p>}
|
||||
<div class="cols">
|
||||
<div class="card">
|
||||
<h3>Geeignet für</h3>
|
||||
<ul>{product.bestFor.map(x => <li>{x}</li>)}</ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Nicht geeignet, wenn</h3>
|
||||
<ul>{product.notFor.map(x => <li>{x}</li>)}</ul>
|
||||
</div>
|
||||
<div class="card"><h3>Geeignet für</h3><ul>{product.bestFor.map(x => <li>{x}</li>)}</ul></div>
|
||||
<div class="card"><h3>Nicht geeignet, wenn</h3><ul>{product.notFor.map(x => <li>{x}</li>)}</ul></div>
|
||||
</div>
|
||||
<div class="cols">
|
||||
<div class="card"><h3>Stärken</h3><ul>{product.pros.map(x => <li>{x}</li>)}</ul></div>
|
||||
<div class="card"><h3>Schwächen</h3><ul>{product.cons.map(x => <li>{x}</li>)}</ul></div>
|
||||
</div>
|
||||
|
||||
{availStores.length>0 && (
|
||||
<div style="margin-top:1.4rem">
|
||||
<h2>Aktuell verfügbar in</h2>
|
||||
<div class="grid stores">
|
||||
{availStores.map(o => (
|
||||
<div class="card srow">
|
||||
<div><strong>{o.chain}</strong> {o.store?.city}<br /><span class="note">{o.store?.name}</span></div>
|
||||
<div style="text-align:right">{euro(o.price)}<br /><span class={o.availability==='low_stock'?'badge tag-warn':'badge tag-ok'}>{o.availability==='low_stock'?'wenig Bestand':'verfügbar'}</span></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h2 style="margin-top:1.6rem">Alternativen</h2>
|
||||
<div class="grid stores">
|
||||
{alternatives.map(a => <a class="card altlink" href={`/geraete/${a.slug}`}><strong>{a.name}</strong><br /><span class="note">{a.badge}</span></a>)}
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top:1.6rem">Quellen</h2>
|
||||
<ul>{product.sources.map(s => <li><a href={s.url} rel="nofollow">{s.label}</a></li>)}</ul>
|
||||
</section>
|
||||
</Base>
|
||||
<style>
|
||||
.lead{font-size:1.1rem;color:var(--slate);max-width:46rem}
|
||||
.price-box{margin:1rem 0}
|
||||
.status-grid{display:flex;gap:2rem;flex-wrap:wrap;margin-bottom:.6rem}
|
||||
.status-grid strong{font-size:1.25rem}
|
||||
.wucher{background:var(--danger-bg);color:var(--danger);padding:.7rem .9rem;border-radius:var(--radius);font-size:.92rem}
|
||||
.price-box{margin:1rem 0}.sg{display:flex;gap:2rem;flex-wrap:wrap;margin-bottom:.6rem}.sg strong{font-size:1.25rem}
|
||||
.status-cta{display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.hint{background:var(--warm-bg);color:oklch(0.42 0.11 47);padding:.8rem 1rem;border-radius:var(--radius);font-size:.95rem}
|
||||
.hint{background:var(--warm-bg);color:oklch(0.42 0.11 47);padding:.8rem 1rem;border-radius:var(--radius)}
|
||||
.cols{display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-top:1rem}
|
||||
.stores{grid-template-columns:repeat(2,1fr)}
|
||||
.srow{display:flex;justify-content:space-between;align-items:center}
|
||||
.altlink{text-decoration:none;color:var(--ink)}
|
||||
.altlink:hover{border-color:var(--petrol)}
|
||||
.stores{grid-template-columns:repeat(3,1fr)}
|
||||
.altlink{text-decoration:none;color:var(--ink)}.altlink:hover{border-color:var(--petrol)}
|
||||
ul{margin:.2rem 0;padding-left:1.1rem}
|
||||
@media(max-width:820px){.cols,.stores{grid-template-columns:1fr}}
|
||||
@media(max-width:820px){.cols{grid-template-columns:1fr}.stores{grid-template-columns:1fr}}
|
||||
</style>
|
||||
|
||||
+84
-69
@@ -1,88 +1,103 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import ProductCard from '../components/ProductCard.astro';
|
||||
import { allProducts, priceIntel, euro, getProduct, dataUpdatedAt } from '../lib/pricing';
|
||||
const top = allProducts.filter(p => p.tier === 'A');
|
||||
const ps = getProduct('midea-portasplit-12000')!;
|
||||
const psi = priceIntel(ps);
|
||||
const updated = new Date(dataUpdatedAt).toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});
|
||||
const jsonld = {
|
||||
'@context':'https://schema.org','@type':'WebSite',name:'Kühlfinder',url:'https://kuehlfinder.de',
|
||||
description:'Portable Klimageräte finden und Live-Verfügbarkeit in Baumärkten sehen.'
|
||||
};
|
||||
import { allProducts } from '../lib/pricing';
|
||||
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.'};
|
||||
---
|
||||
<Base title="Kühlfinder – gutes portables Klimagerät finden & Verfügbarkeit sehen" jsonld={jsonld}>
|
||||
<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}>
|
||||
<section class="hero"><div class="wrap">
|
||||
<p class="kicker"><span class="u"></span>Ehrlicher Klima-Finder für Mietwohnungen</p>
|
||||
<h1>Finde ein gutes portables Klimagerät – und sieh, wo es heute verfügbar ist.</h1>
|
||||
<p class="lead">Nur echte Kompressorgeräte, keine Fake-Kühler. Mit Live-Verfügbarkeit in Baumärkten, Preis-Check gegen Wucher und Alarm, sobald dein Gerät wieder da ist.</p>
|
||||
<div class="modes">
|
||||
<a class="mode" href="/finder"><strong>Ich filtere</strong><span>Welches Gerät passt zu meinem Raum?</span></a>
|
||||
<a class="mode" href="/karte"><strong>Ich sehe die Karte</strong><span>Alle Märkte mit aktuellem Bestand</span></a>
|
||||
<a class="mode" href="/alarm"><strong>Ich lasse mich benachrichtigen</strong><span>Alarm bei Verfügbarkeit in der Nähe</span></a>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<section class="wrap"><div class="status card">
|
||||
<div class="status-head">
|
||||
<h2 style="margin:0">Midea PortaSplit – aktueller Status</h2>
|
||||
<span class="note">Stand: {updated}</span>
|
||||
</div>
|
||||
<div class="status-grid">
|
||||
<div><span class="note">Üblicher Preis (UVP)</span><br /><strong>{euro(psi.fairPrice)}</strong></div>
|
||||
<div><span class="note">Seriös aktuell ab</span><br /><strong>{euro(psi.lowestLegit)}</strong></div>
|
||||
<div><span class="note">Märkte mit Bestand</span><br /><strong>{psi.storesAvailable}</strong></div>
|
||||
</div>
|
||||
{psi.wucher && (
|
||||
<p class="wucher">⚠ <strong>Wucher-Warnung:</strong> Einzelne Drittanbieter verlangen aktuell bis {euro(psi.wucher.price)} – das ist deutlich über dem üblichen Preis von {euro(psi.fairPrice)}. Nicht in Panik überteuert kaufen. Besser: Alarm setzen oder eine gute Alternative wählen.</p>
|
||||
)}
|
||||
<div class="status-cta">
|
||||
<a class="btn sm" href="/karte?produkt=midea-portasplit-12000">PortaSplit auf der Karte</a>
|
||||
<a class="btn sm ghost" href="/alarm?produkt=midea-portasplit-12000">Verfügbarkeits-Alarm</a>
|
||||
<a class="btn sm ghost" href="/portasplit">Alternativen ansehen</a>
|
||||
<span class="eyebrow"><span class="dot ok"></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>
|
||||
<form class="herobar" action="/karte" method="get">
|
||||
<input name="plz" inputmode="numeric" placeholder="PLZ oder Ort eingeben …" aria-label="PLZ oder Ort" />
|
||||
<button class="btn" type="submit">Verfügbarkeit zeigen</button>
|
||||
</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>
|
||||
</div></section>
|
||||
|
||||
<section class="wrap">
|
||||
<h2><span class="u"></span>Top-Empfehlungen</h2>
|
||||
<p class="muted">Kuratiert nach Bauart, Kühlleistung, Effizienz, Nutzungsfall und glaubwürdigen Quellen.</p>
|
||||
<div class="grid cards3">
|
||||
{top.map(p => <ProductCard product={p} />)}
|
||||
<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>
|
||||
</div>
|
||||
<p style="margin-top:1rem"><a class="btn ghost" href="/geraete">Alle Geräte & Vergleich</a></p>
|
||||
</section>
|
||||
|
||||
<section class="wrap"><div class="split">
|
||||
<div class="card">
|
||||
<h2>PortaSplit ausverkauft?</h2>
|
||||
<p>Die Midea PortaSplit ist bei Hitze regelmäßig vergriffen. Wir zeigen dir wohnungstaugliche Alternativen, die wirklich kühlen – statt dich teuer hängen zu lassen.</p>
|
||||
<a class="btn ghost" href="/portasplit">Sinnvolle Alternativen</a>
|
||||
<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="card warm">
|
||||
<h2>Warum Kühlung wichtiger wird</h2>
|
||||
<p>Hitzewellen treten in Deutschland früher und intensiver auf. Gute, effiziente Kühlung kann Hitzeschutz sein – mit Quellen statt Panik.</p>
|
||||
<a class="btn ghost" href="/hitzeschutz">Hitzeschutz & Klima</a>
|
||||
<div class="grid cards3">{top.map(p => <ProductCard product={p} />)}</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></section>
|
||||
</Base>
|
||||
<style>
|
||||
.hero{background:linear-gradient(180deg,var(--petrol-soft),var(--light));padding:3rem 0 2.5rem}
|
||||
.kicker{font-weight:600;color:var(--petrol-deep);margin:0 0 .4rem;text-transform:uppercase;letter-spacing:.04em;font-size:.8rem}
|
||||
.lead{font-size:1.15rem;max-width:46rem;color:var(--slate)}
|
||||
.modes{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin-top:1.6rem}
|
||||
.mode{display:flex;flex-direction:column;gap:.2rem;background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:1.1rem;text-decoration:none;color:var(--ink);transition:border-color .2s,transform .08s}
|
||||
.mode:hover{border-color:var(--petrol);transform:translateY(-2px)}
|
||||
.mode strong{font-size:1.05rem;color:var(--petrol-deep)}
|
||||
.mode span{font-size:.9rem;color:var(--slate)}
|
||||
section.wrap{margin-top:2.5rem}
|
||||
.status{border-left:4px solid var(--warm)}
|
||||
.status-head{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:.4rem}
|
||||
.status-grid{display:flex;gap:2rem;margin:.8rem 0;flex-wrap:wrap}
|
||||
.status-grid strong{font-size:1.3rem}
|
||||
.wucher{background:var(--danger-bg);color:var(--danger);padding:.8rem 1rem;border-radius:var(--radius);font-size:.95rem}
|
||||
.status-cta{display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.hero{background:var(--grad-hero);padding:3.4rem 0 2.6rem}
|
||||
.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)}
|
||||
.herobar input:focus{border-color:var(--petrol)}
|
||||
.pills{display:flex;gap:.6rem;flex-wrap:wrap}
|
||||
section.wrap{margin-top:2.6rem}
|
||||
.features{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}
|
||||
.features .feature{text-decoration:none;color:var(--ink)} .features h3{margin:.2rem 0}
|
||||
.cards3{grid-template-columns:repeat(3,1fr)}
|
||||
.split{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
|
||||
.card.warm{background:var(--warm-bg)}
|
||||
@media(max-width:820px){.modes,.cards3,.split{grid-template-columns: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}
|
||||
.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)}
|
||||
.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}}
|
||||
</style>
|
||||
<script>
|
||||
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');
|
||||
}).catch(()=>{});
|
||||
</script>
|
||||
|
||||
+100
-143
@@ -1,23 +1,25 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
const jsonld = {'@context':'https://schema.org','@type':'WebApplication',name:'Kühlfinder Monitor',applicationCategory:'ShoppingApplication',
|
||||
description:'Live-Verfügbarkeit der Midea PortaSplit, PortaSplit Cool und Universalhalterung in über 1.160 Baumärkten, mit Verfügbarkeits-Prognose.'};
|
||||
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.'};
|
||||
---
|
||||
<Base title="PortaSplit Monitor – Live-Verfügbarkeit + Prognose in 1.167 Baumärkten | Kühlfinder" description="Sieh auf einen Blick, welche OBI-, Hornbach-, Bauhaus-, Toom-, Hagebau- und Globus-Filialen Midea PortaSplit, Cool & Halterung auf Lager haben – mit Preis-Check, Wucher-Warnung und Verfügbarkeits-Prognose." jsonld={jsonld}>
|
||||
<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}>
|
||||
<section class="mon-hero"><div class="wrap">
|
||||
<div class="mon-hero-row">
|
||||
<div>
|
||||
<p class="kicker"><span class="u"></span>PortaSplit Monitor · Live</p>
|
||||
<h1>Wo gibt es die Midea PortaSplit gerade?</h1>
|
||||
<p class="lead">Live-Bestand in <span class="num" id="h-stores">1.167</span> Baumärkten – plus <strong>Verfügbarkeits-Prognose</strong>, die dir sagt, wo die Chance in den nächsten 7 Tagen am höchsten ist. Das kann braucheklima nicht.</p>
|
||||
<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>
|
||||
</div>
|
||||
<div class="upd card"><span class="note">Aktualisiert</span><div class="num upd-t" id="h-updated">–</div><span class="note" id="h-feednote">lädt …</span></div>
|
||||
</div>
|
||||
<div class="stats" id="stats">
|
||||
<div class="stat"><span class="note">Filialen</span><b class="num" data-k="storesTotal">–</b></div>
|
||||
<div class="stat ok"><span class="note">mit PortaSplit</span><b class="num" data-k="withPortaSplit">–</b></div>
|
||||
<div class="stat ok"><span class="note">mit Cool</span><b class="num" data-k="withPortaSplitCool">–</b></div>
|
||||
<div class="stat ok"><span class="note">mit Halterung</span><b class="num" data-k="withHalterung">–</b></div>
|
||||
<div class="stat"><span class="note">Märkte</span><b class="num" data-k="storesTotal">–</b></div>
|
||||
<div class="stat ok"><span class="note">mit Klimagerät</span><b class="num" data-k="withAC">–</b></div>
|
||||
<div class="stat"><span class="note">Baumärkte</span><b class="num" data-k="baumarkt">–</b></div>
|
||||
<div class="stat"><span class="note">Elektromärkte</span><b class="num" data-k="elektro">–</b></div>
|
||||
<div class="stat"><span class="note">Restocks 7 Tg.</span><b class="num" id="s-restocks">–</b></div>
|
||||
<div class="stat warn"><span class="note">Wucher-Alarm</span><b class="num" id="s-wucher">–</b></div>
|
||||
</div>
|
||||
@@ -27,227 +29,182 @@ const jsonld = {'@context':'https://schema.org','@type':'WebApplication',name:'K
|
||||
<div class="mapcol">
|
||||
<div id="map"></div>
|
||||
<div class="legend">
|
||||
<span><span class="dot ok"></span>verfügbar</span>
|
||||
<span><span class="dot" style="background:var(--petrol)"></span>Filiale (Cluster)</span>
|
||||
<span><i class="hot"></i>hohe Prognose</span>
|
||||
<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>
|
||||
<aside class="feedcol card">
|
||||
<h2 class="h-sm">Letzte Verfügbarkeiten</h2>
|
||||
<p class="note">Echte Restock-Ereignisse, neueste zuerst.</p>
|
||||
<ol class="feed" id="feed"><li class="skel" style="height:46px"></li><li class="skel" style="height:46px"></li><li class="skel" style="height:46px"></li></ol>
|
||||
<p class="note">Restock-Ereignisse, neueste zuerst.</p>
|
||||
<ol class="feed" id="feed"><li class="skel" style="height:46px"></li><li class="skel" style="height:46px"></li></ol>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="wrap">
|
||||
<div class="toolbar card">
|
||||
<div class="tb-row">
|
||||
<input id="q" type="search" placeholder="Filiale, Ort oder PLZ suchen …" />
|
||||
<input id="q" type="search" placeholder="Markt, Ort oder PLZ suchen …" />
|
||||
<select id="model"><option value="all">Alle Modelle</option>{models.map(m=> <option value={m.slug}>{m.name}</option>)}</select>
|
||||
<button class="btn sm ghost" id="locate" type="button">Standort</button>
|
||||
<label class="rad">Umkreis <span class="num" id="r-out">–</span><input id="radius" type="range" min="10" max="500" step="10" value="500" disabled /></label>
|
||||
</div>
|
||||
<div class="tb-row chips">
|
||||
<span class="note">Produkt:</span>
|
||||
<button class="chip on" data-p="ps">PortaSplit</button>
|
||||
<button class="chip on" data-p="co">Cool</button>
|
||||
<button class="chip on" data-p="ha">Halterung</button>
|
||||
<span class="sep"></span>
|
||||
<button class="chip ghost on" data-toggle="avail">Nur verfügbare</button>
|
||||
<button class="chip ghost" data-toggle="chance">Beste Prognose zuerst</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" data-kind="baumarkt">Baumarkt</button>
|
||||
<button class="chip" data-kind="elektro">Elektro</button>
|
||||
<span class="sep"></span>
|
||||
<span class="note">Kette:</span>
|
||||
<span id="chainchips"></span>
|
||||
<button class="chip ghost" id="reset" type="button">Zurücksetzen</button>
|
||||
<button class="chip ghost" id="reset" type="button">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="onlinebar card" id="onlinebar"></div>
|
||||
|
||||
<div class="tablewrap card">
|
||||
<table class="mtab">
|
||||
<thead><tr>
|
||||
<th>Filiale</th>
|
||||
<th class="ce" data-sort="ps">PortaSplit</th>
|
||||
<th class="ce" data-sort="co">Cool</th>
|
||||
<th class="ce" data-sort="ha">Halterung</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="6"><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 Filialen laden</button><p class="note" id="rowcount"></p></div>
|
||||
<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">Kein Bestand in deiner Nähe?</h2><p class="note" style="margin:0">Wir mailen dir, sobald PortaSplit & Co. in deinem Umkreis auftauchen – inkl. Prognose-Frühwarnung.</p></div>
|
||||
<a class="btn" href="/alarm?produkt=midea-portasplit-12000">Verfügbarkeits-Alarm</a>
|
||||
<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>
|
||||
<p class="note" style="margin-top:.6rem">Datenstand: Snapshot aus öffentlich einsehbaren Filial-/Online-Beständen (Vorbild braucheklima.de). Prognose = statistische Schätzung aus der Restock-Historie, keine Garantie. Preise/Bestände beim Händler prüfen.</p>
|
||||
<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>
|
||||
</section>
|
||||
</Base>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.7.1/dist/maplibre-gl.css" />
|
||||
<style>
|
||||
.mon-hero{background:linear-gradient(180deg,var(--petrol-soft),var(--light));padding:1.8rem 0 1.4rem}
|
||||
.mon-hero{background:var(--grad-hero);padding:1.8rem 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 .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)}
|
||||
.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}
|
||||
.stat{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:.6rem .8rem;display:flex;flex-direction:column;gap:.1rem}
|
||||
.stat b{font-size:1.45rem;line-height:1.1}
|
||||
.stat.ok b{color:var(--ok-dot)} .stat.warn b{color:var(--danger)}
|
||||
.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)}
|
||||
.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:var(--radius);overflow:hidden;border:1px solid var(--line)}
|
||||
.legend{display:flex;gap:1rem;flex-wrap:wrap;font-size:.8rem;color:var(--slate);margin-top:.5rem;align-items:center}
|
||||
.legend i.hot{display:inline-block;width:11px;height:11px;border-radius:50%;border:2px solid var(--warm);margin-right:.3rem;vertical-align:middle}
|
||||
.feedcol{max-height:560px;overflow:auto}
|
||||
.h-sm{font-size:1.15rem;margin:.1rem 0 .2rem}
|
||||
.feed{list-style:none;margin:.6rem 0 0;padding:0;display:flex;flex-direction:column;gap:.1rem}
|
||||
#map{height:520px;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}
|
||||
.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:.92rem}
|
||||
.feed .fl span{font-size:.8rem;color:var(--slate)}
|
||||
.feed .ft{font-size:.8rem;color:var(--slate);white-space:nowrap;font-family:var(--mono)}
|
||||
.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:200px;font-family:var(--font-body);font-size:.95rem;padding:.6rem .8rem;border:1px solid var(--line);border-radius:8px;background:var(--surface-2)}
|
||||
#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: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)}
|
||||
.chips{font-size:.85rem}
|
||||
.chip{font-family:var(--font-body);font-weight:500;font-size:.85rem;border:1px solid var(--line);background:var(--card);color:var(--ink);padding:.4rem .8rem;border-radius:999px;cursor:pointer;transition:transform .1s,background .15s,border-color .15s}
|
||||
.chip:active{transform:scale(.97)}
|
||||
.chip.on{background:var(--petrol);color:#fff;border-color:var(--petrol)}
|
||||
.chip{font-family:var(--font-body);font-weight:500;font-size:.85rem;border:1px solid var(--line);background:var(--card);color:var(--ink);padding:.4rem .8rem;border-radius:999px;cursor:pointer;transition:transform .1s,background .15s}
|
||||
.chip:active{transform:scale(.97)}.chip.on{background:var(--petrol);color:#fff;border-color:var(--petrol)}
|
||||
.chip.ghost.on{background:var(--ok-bg);color:var(--ok-dot);border-color:var(--ok-dot)}
|
||||
.sep{width:1px;align-self:stretch;background:var(--line);margin:0 .2rem}
|
||||
#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:8px;padding:.4rem .6rem;min-width:120px}
|
||||
.osrc b{font-size:.85rem}.osrc .num{font-size:.9rem}
|
||||
.osrc.wucher{border-color:var(--danger);background:var(--danger-bg)}
|
||||
.osrc.wucher .num{color:var(--danger)}
|
||||
.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)}
|
||||
.osrc.wucher{border-color:var(--danger);background:var(--danger-bg)}.osrc.wucher .num{color:var(--danger)}
|
||||
.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:.75rem;text-transform:uppercase;letter-spacing:.04em;color:var(--slate);font-weight:600;padding:.7rem .8rem;border-bottom:1px solid var(--line);position:sticky;top:62px;background:var(--card);cursor:pointer;white-space:nowrap}
|
||||
.mtab th.ce{text-align:center}
|
||||
.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:hover{background:var(--petrol-soft)}
|
||||
.mtab .fname{font-weight:600;font-size:.92rem}.mtab .faddr{font-size:.78rem;color:var(--slate)}
|
||||
.cell{display:flex;flex-direction:column;align-items:center;gap:.15rem;line-height:1.2}
|
||||
.cell .num{font-size:.85rem}
|
||||
.cell .pa{font-size:.72rem;color:var(--ok-dot);font-weight:600}
|
||||
.cell .pn{font-size:.72rem;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)}
|
||||
.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)}
|
||||
.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}.mtab thead th{position:static}}
|
||||
@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 PNAME={ps:'PortaSplit',co:'PortaSplit Cool',ha:'Universalhalterung'};
|
||||
const PKEYFEED={'portasplit':'ps','portasplit-cool':'co','universalhalterung':'ha'};
|
||||
const euro=n=>n==null?'–':new Intl.NumberFormat('de-DE',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n);
|
||||
const FAIR={ps:849,co:779,ha:70};
|
||||
let DATA=null, center=null, shown=120;
|
||||
const state={products:{ps:true,co:true,ha:true},chains:new Set(),avail:true,chance:false,q:'',radius:500,sort:null};
|
||||
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 params=new URLSearchParams(location.search);
|
||||
if(params.get('modell'))state.model=params.get('modell');
|
||||
if(params.get('plz'))state.q=params.get('plz');
|
||||
|
||||
const $=s=>document.querySelector(s), $$=s=>Array.from(document.querySelectorAll(s));
|
||||
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 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(center&&dist(center[1],center[0],s.la,s.lo)>state.radius)return false;
|
||||
if(state.avail&&!st.a)return false;
|
||||
return true; }
|
||||
|
||||
const map=new maplibregl.Map({container:'map',style:'https://tiles.openfreemap.org/styles/liberty',center:[10.2,51.1],zoom:5});
|
||||
map.addControl(new maplibregl.NavigationControl(),'top-right');
|
||||
|
||||
function activeProducts(){return Object.keys(state.products).filter(k=>state.products[k]);}
|
||||
function storeBest(s){ // returns {avail, bestProb, anyTracked}
|
||||
let avail=false,bestProb=0,any=false;
|
||||
for(const k of activeProducts()){ if(s[k]){any=true; if(s[k].a)avail=true; if(s[k].q>bestProb)bestProb=s[k].q;} }
|
||||
return {avail,bestProb,any};
|
||||
}
|
||||
function passFilter(s){
|
||||
const aps=activeProducts(); if(!aps.some(k=>s[k]))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(center && dist(center[1],center[0],s.la,s.lo)>state.radius)return false;
|
||||
if(state.avail && !storeBest(s).avail)return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
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=>storeBest(s).avail).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);
|
||||
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 render(){
|
||||
const list=DATA.stores.filter(passFilter);
|
||||
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)=>storeBest(b).bestProb-storeBest(a).bestProb);}
|
||||
else {list.sort((a,b)=>(storeBest(b).avail?1:0)-(storeBest(a).avail?1:0)||storeBest(b).bestProb-storeBest(a).bestProb);}
|
||||
// map
|
||||
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);
|
||||
// table
|
||||
const tb=$('#rows'); tb.innerHTML='';
|
||||
const slice=list.slice(0,shown);
|
||||
const cell=(o)=>{ if(!o)return '<td class="ce"><span class="pn">–</span></td>';
|
||||
const dotc=o.a?'ok':(o.q>=15?'low':'off');
|
||||
return `<td class="ce"><div class="cell"><span><span class="dot ${dotc}"></span> <span class="num">${euro(o.p)}</span></span>${o.a?'<span class="pa">verfügbar</span>':`<span class="pn">${o.q>0?o.q+'% / 7 Tg.':'—'}</span>`}</div></td>`;};
|
||||
for(const s of slice){
|
||||
const b=storeBest(s);
|
||||
const d=center?` · <span class="num">${dist(center[1],center[0],s.la,s.lo).toFixed(0)} km</span>`:'';
|
||||
const prog=`<td class="ce"><span class="prog"><span class="progbar"><i style="width:${b.bestProb}%"></i></span><span class="num">${b.bestProb}%</span></span></td>`;
|
||||
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><div class="fname">${s.n}</div><div class="faddr">${s.z} ${s.ci}${d}</div></td>${cell(s.ps)}${cell(s.co)}${cell(s.ha)}${prog}${distTd}</tr>`);
|
||||
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>
|
||||
<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=`${slice.length} von ${list.length} Filialen`;
|
||||
$('#rowcount').textContent=`${Math.min(shown,list.length)} von ${list.length} Märkten`;
|
||||
$('#more').style.display=list.length>shown?'inline-flex':'none';
|
||||
$('#distcol').style.display=center?'table-cell':'none';
|
||||
}
|
||||
|
||||
function fillStatic(){
|
||||
$('#h-updated').textContent=DATA.stats.updated;
|
||||
$('#h-stores').textContent=DATA.stats.storesTotal.toLocaleString('de-DE');
|
||||
$$('#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';
|
||||
const wuch=DATA.online.filter(o=>o.ps&&o.ps.wucher).length;
|
||||
$('#s-wucher').textContent=wuch;
|
||||
// feed
|
||||
$('#feed').innerHTML=DATA.feed.slice(0,20).map(f=>`<li><div class="fl"><b>${f.name}</b><span>${PNAME[PKEYFEED[f.productId]]||f.productId}</span></div><span class="ft">${rel(f.at)}</span></li>`).join('');
|
||||
// online bar
|
||||
$('#onlinebar').innerHTML='<span class="note" style="align-self:center">Online:</span>'+DATA.online.map(o=>{
|
||||
const ps=o.ps; if(!ps)return '';
|
||||
const w=ps.wucher;
|
||||
return `<a class="osrc ${w?'wucher':''}" href="${o.url}" target="_blank" rel="nofollow noopener"><b>${o.src}${w?' ⚠':''}</b><span class="num">${euro(ps.p)}</span><span class="note">${ps.a?(w?'Wucher':'verfügbar'):'nicht verf.'}</span></a>`;
|
||||
}).join('');
|
||||
// chains
|
||||
$('#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;if(state.chains.has(c)){state.chains.delete(c);b.classList.remove('on');}else{state.chains.add(c);b.classList.add('on');}render();}));
|
||||
$$('#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();}));
|
||||
if(state.model!=='all')$('#model').value=state.model;
|
||||
if(state.q)$('#q').value=state.q;
|
||||
}
|
||||
|
||||
map.on('load',()=>{
|
||||
map.addSource('all',{type:'geojson',data:{type:'FeatureCollection',features:[]},cluster:true,clusterRadius:46,clusterMaxZoom:9});
|
||||
map.addLayer({id:'cl',type:'circle',source:'all',filter:['has','point_count'],paint:{'circle-color':'#2b6a72','circle-opacity':.82,'circle-radius':['step',['get','point_count'],14,50,18,200,24,500,30]}});
|
||||
map.addLayer({id:'cl-c',type:'symbol',source:'all',filter:['has','point_count'],layout:{'text-field':['get','point_count_abbreviated'],'text-size':12},paint:{'text-color':'#fff'}});
|
||||
map.addLayer({id:'pt',type:'circle',source:'all',filter:['!',['has','point_count']],paint:{'circle-color':'#2b6a72','circle-opacity':.7,'circle-radius':4,'circle-stroke-width':1,'circle-stroke-color':'#fff'}});
|
||||
map.addLayer({id:'pt',type:'circle',source:'all',filter:['!',['has','point_count']],paint:{'circle-color':'#2b6a72','circle-opacity':.6,'circle-radius':4,'circle-stroke-width':1,'circle-stroke-color':'#fff'}});
|
||||
map.addSource('av',{type:'geojson',data:{type:'FeatureCollection',features:[]}});
|
||||
map.addLayer({id:'av',type:'circle',source:'av',paint:{'circle-color':'#1d9e75','circle-radius':7,'circle-stroke-width':2,'circle-stroke-color':'#fff'}});
|
||||
map.on('click','av',e=>{const p=e.features[0].properties;new maplibregl.Popup().setLngLat(e.lngLat).setHTML(`<b>${p.n}</b><br>${p.ci} – verfügbar`).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} – Klimagerät 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();
|
||||
});
|
||||
|
||||
// controls
|
||||
$$('.chip[data-p]').forEach(b=>b.addEventListener('click',()=>{const k=b.dataset.p;state.products[k]=!state.products[k];b.classList.toggle('on');render();}));
|
||||
$$('.chip[data-toggle]').forEach(b=>b.addEventListener('click',()=>{const t=b.dataset.toggle;state[t]=!state[t];b.classList.toggle('on');render();}));
|
||||
$('#q').addEventListener('input',e=>{state.q=e.target.value.trim();shown=120;render();});
|
||||
$('#model').addEventListener('change',e=>{state.model=e.target.value;render();});
|
||||
$$('.chip[data-toggle]').forEach(b=>b.addEventListener('click',()=>{state[b.dataset.toggle]=!state[b.dataset.toggle];b.classList.toggle('on');render();}));
|
||||
$$('.chip[data-kind]').forEach(b=>b.addEventListener('click',()=>{state.kind=b.dataset.kind;$$('.chip[data-kind]').forEach(x=>x.classList.remove('on'));b.classList.add('on');render();}));
|
||||
$('#radius').addEventListener('input',e=>{state.radius=+e.target.value;$('#r-out').textContent=state.radius+' km';render();});
|
||||
$('#more').addEventListener('click',()=>{shown+=120;render();});
|
||||
$('#reset').addEventListener('click',()=>{location.reload();});
|
||||
$$('.mtab th[data-sort]').forEach(th=>th.addEventListener('click',()=>{state.chance=(th.dataset.sort==='q');render();}));
|
||||
$('#reset').addEventListener('click',()=>location.reload());
|
||||
$$('.mtab th[data-sort]').forEach(th=>th.addEventListener('click',()=>{state.chance=true;render();}));
|
||||
$('#locate').addEventListener('click',()=>{if(!navigator.geolocation)return;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();});});
|
||||
|
||||
fetch('/data/monitor.json').then(r=>r.json()).then(d=>{DATA=d;fillStatic();if(map.loaded())render();else render();});
|
||||
fetch('/data/monitor.json').then(r=>r.json()).then(d=>{DATA=d;fillStatic();render();}).catch(()=>{$('#rowcount').textContent='Daten konnten nicht geladen werden';});
|
||||
</script>
|
||||
|
||||
+8
-11
@@ -2,20 +2,17 @@ import { allProducts, priceIntel, euro } from '../lib/pricing';
|
||||
export const GET = () => {
|
||||
const lines = [
|
||||
'# Kühlfinder',
|
||||
'> Kuratierter Finder für portable Klimageräte in Deutschland mit Live-Verfügbarkeit in Baumärkten, Preis-Check (Wucher-Warnung) und Verfügbarkeits-Alarm.',
|
||||
'> Finde, WO es gerade Klimageräte in Deutschland gibt – Live-Verfügbarkeit in Baumärkten (OBI, Toom, Bauhaus, Hornbach, Hagebau, Globus) und Elektromärkten (MediaMarkt, Saturn, expert, Euronics) plus Online (Amazon, idealo), mit Verfügbarkeits-Prognose und Wucher-Warnung. Eigener Scraper, keine Drittdaten.',
|
||||
'',
|
||||
'## Was Kühlfinder besser macht',
|
||||
'- Nur echte Kompressorgeräte (mobile Split, Monoblock) – keine Verdunstungs-/USB-Kühler.',
|
||||
'- Live-Verfügbarkeit pro Baumarkt-Filiale auf einer Deutschlandkarte (idealo zeigt keinen Filialbestand).',
|
||||
'- Warnung vor Wucherpreisen bei Knappheit (z. B. Midea PortaSplit).',
|
||||
'- Fokus auf wohnungstaugliche Geräte und PortaSplit-Alternativen.',
|
||||
'- Zeigt für ALLE kuratierten Klimageräte, welche Märkte sie aktuell vorrätig haben (idealo zeigt keinen Filialbestand).',
|
||||
'- Verfügbarkeits-Prognose aus der Restock-Historie (Chance in den nächsten 7 Tagen).',
|
||||
'- Warnung vor Wucherpreisen bei Knappheit.',
|
||||
'- Beratung (60-Sekunden-Finder) + wohnungstaugliche Alternativen, wenn ein Gerät ausverkauft ist.',
|
||||
'',
|
||||
'## Geräte',
|
||||
];
|
||||
for (const p of allProducts) {
|
||||
const pi = priceIntel(p);
|
||||
lines.push(`- ${p.name} (${p.type==='mobile_split'?'mobile Split':'Monoblock'}, ${p.btuh} BTU): ${p.shortVerdict} Üblicher Preis ${euro(pi.fairPrice)}. /geraete/${p.slug}`);
|
||||
}
|
||||
lines.push('', '## Seiten', '- /finder – 60-Sekunden-Empfehlung', '- /karte – Verfügbarkeits-Karte', '- /portasplit – PortaSplit-Status & Alternativen', '- /alarm – Verfügbarkeits-Alarm', '- /hitzeschutz – Klimawandel-Kontext', '- /transparenz – Methodik & Affiliate');
|
||||
return new Response(lines.join('\n'), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
|
||||
for (const p of allProducts){ const pi=priceIntel(p); lines.push(`- ${p.name} (${p.type==='mobile_split'?'mobile Split':'Monoblock'}, ${p.btuh} BTU): ${p.shortVerdict} Üblicher Preis ${euro(pi.fairPrice)}. /geraete/${p.slug}`); }
|
||||
lines.push('', '## Seiten', '- /karte – Live-Verfügbarkeits-Monitor (Karte + Tabelle + Prognose)', '- /finder – 60-Sekunden-Empfehlung', '- /portasplit – PortaSplit-Status & Alternativen', '- /alarm – Verfügbarkeits-Alarm (E-Mail + Telegram)', '- /hitzeschutz – Klimawandel-Kontext', '- /transparenz – Methodik & Affiliate');
|
||||
return new Response(lines.join('\n'), { headers: { 'Content-Type':'text/plain; charset=utf-8' } });
|
||||
};
|
||||
|
||||
+12
-26
@@ -2,41 +2,27 @@
|
||||
import Base from '../layouts/Base.astro';
|
||||
import ProductCard from '../components/ProductCard.astro';
|
||||
import { allProducts, getProduct, priceIntel, euro } from '../lib/pricing';
|
||||
const ps = getProduct('midea-portasplit-12000')!;
|
||||
const psi = priceIntel(ps);
|
||||
const ps = getProduct('midea-portasplit-12000')!; const pi = priceIntel(ps);
|
||||
const alts = allProducts.filter(p=>['delonghi-pac-ex105','midea-portasplit-cool-8000','bosch-cool-5000','midea-silent-cool-26-pro-wf'].includes(p.id));
|
||||
const jsonld={'@context':'https://schema.org','@type':'FAQPage',mainEntity:[
|
||||
{'@type':'Question',name:'Was tun, wenn die Midea PortaSplit ausverkauft ist?',acceptedAnswer:{'@type':'Answer',text:'Sinnvolle Alternativen sind die kleinere PortaSplit Cool, der Monoblock De\'Longhi PAC EX105 oder die Bosch Cool 5000. Wer auf die PortaSplit warten will, nutzt den Verfügbarkeits-Alarm statt überteuert bei Drittanbietern zu kaufen.'}},
|
||||
{'@type':'Question',name:'Wie viel kostet die Midea PortaSplit normal?',acceptedAnswer:{'@type':'Answer',text:'Der übliche Preis liegt bei rund 899 € (UVP), im Angebot teils um 699 €. Preise deutlich darüber bei Drittanbietern sind Wucher.'}}
|
||||
]};
|
||||
{'@type':'Question',name:'Was tun, wenn die Midea PortaSplit ausverkauft ist?',acceptedAnswer:{'@type':'Answer',text:'Sinnvolle Alternativen: PortaSplit Cool, De\'Longhi PAC EX105 oder Bosch Cool 5000. Wer warten will, nutzt den Verfügbarkeits-Alarm statt überteuert bei Drittanbietern zu kaufen.'}},
|
||||
{'@type':'Question',name:'Was kostet die Midea PortaSplit normal?',acceptedAnswer:{'@type':'Answer',text:'Üblich rund 799 € (UVP), im Angebot teils um 699 €. Deutlich höhere Drittanbieterpreise sind Wucher.'}}]};
|
||||
---
|
||||
<Base title="Midea PortaSplit ausverkauft? Verfügbarkeit, Preis & beste Alternativen | Kühlfinder" description="PortaSplit-Status, Wucher-Warnung und wohnungstaugliche Alternativen, wenn die Midea PortaSplit nicht lieferbar ist." jsonld={jsonld}>
|
||||
<Base title="Midea PortaSplit ausverkauft? Verfügbarkeit, Preis & beste Alternativen | Kühlfinder" description="PortaSplit-Status, Wucher-Warnung und wohnungstaugliche Alternativen." jsonld={jsonld}>
|
||||
<section class="wrap" style="margin-top:1.6rem">
|
||||
<p class="kicker"><span class="u"></span>PortaSplit-Hub</p>
|
||||
<h1>Midea PortaSplit: Status, Preis & sinnvolle Alternativen</h1>
|
||||
<h1>Midea PortaSplit: Preis & sinnvolle Alternativen</h1>
|
||||
<div class="card" style="border-left:4px solid var(--warm)">
|
||||
<div class="sg">
|
||||
<div><span class="note">Üblicher Preis</span><br /><strong>{euro(psi.fairPrice)}</strong></div>
|
||||
<div><span class="note">Seriös ab</span><br /><strong>{euro(psi.lowestLegit)}</strong></div>
|
||||
<div><span class="note">Märkte mit Bestand</span><br /><strong>{psi.storesAvailable}</strong></div>
|
||||
</div>
|
||||
{psi.wucher && <p class="wucher">⚠ <strong>Wucher-Warnung:</strong> Drittanbieter verlangen bis {euro(psi.wucher.price)} – rund {Math.round((psi.wucher.price/psi.fairPrice-1)*100)} % über üblich. Lieber Alarm setzen oder Alternative wählen.</p>}
|
||||
<div class="status-cta">
|
||||
<a class="btn sm" href="/karte?produkt=midea-portasplit-12000">Auf der Karte</a>
|
||||
<a class="btn sm ghost" href="/alarm?produkt=midea-portasplit-12000">Alarm setzen</a>
|
||||
<div><span class="note">Üblicher Preis</span><br /><strong class="num">{euro(pi.fairPrice)}</strong></div>
|
||||
<div><span class="note">Fair ab</span><br /><strong class="num">{euro(pi.lowestLegit)}</strong></div>
|
||||
<div><span class="note">Wucher ab</span><br /><strong class="num">{euro(pi.wucherThreshold)}</strong></div>
|
||||
</div>
|
||||
<p class="wucher">⚠ <strong>Wucher-Warnung:</strong> Drittanbieter (z. B. Amazon-Marketplace) verlangen bei Knappheit das Mehrfache des üblichen Preises. Lieber Alarm setzen oder Alternative wählen.</p>
|
||||
<div class="status-cta"><a class="btn sm" href="/karte?modell=midea-portasplit-12000">Auf der Karte</a><a class="btn sm ghost" href="/alarm?produkt=midea-portasplit-12000">Alarm setzen</a></div>
|
||||
</div>
|
||||
<h2 style="margin-top:1.6rem">Wohnungstaugliche Alternativen</h2>
|
||||
<p class="muted">Geräte, die in normalen Wohnungen wirklich kühlen – nicht nur „auch verfügbar“.</p>
|
||||
<div class="grid cards2">
|
||||
{alts.map(p=> <ProductCard product={p} />)}
|
||||
</div>
|
||||
<div class="grid cards2">{alts.map(p=> <ProductCard product={p} />)}</div>
|
||||
</section>
|
||||
</Base>
|
||||
<style>
|
||||
.sg{display:flex;gap:2rem;flex-wrap:wrap;margin-bottom:.5rem}.sg strong{font-size:1.25rem}
|
||||
.wucher{background:var(--danger-bg);color:var(--danger);padding:.7rem .9rem;border-radius:var(--radius)}
|
||||
.status-cta{display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.cards2{grid-template-columns:1fr 1fr}
|
||||
@media(max-width:820px){.cards2{grid-template-columns:1fr}}
|
||||
</style>
|
||||
<style>.sg{display:flex;gap:2rem;flex-wrap:wrap;margin-bottom:.5rem}.sg strong{font-size:1.25rem}.wucher{background:var(--danger-bg);color:var(--danger);padding:.7rem .9rem;border-radius:var(--radius)}.status-cta{display:flex;gap:.5rem;flex-wrap:wrap}.cards2{grid-template-columns:1fr 1fr}@media(max-width:820px){.cards2{grid-template-columns:1fr}}</style>
|
||||
|
||||
Reference in New Issue
Block a user