Codex-Rescue: karte.astro surl-Runtime-Bug fix; Textwahrheit (/ + /karte + llms-full + geraete) ehrliche Live-/Demo-Sprache; Offer-Cards (Quelle/Preis/Name/Status getrennt, klickbar) auf Startseite+Karte; klickbare Marktnamen + separater Prüflink; escapeHtml + http(s)-URL-Guard für Crawler-Daten; Empty States Feed/Online
This commit is contained in:
@@ -4,7 +4,7 @@ import ProductCard from '../../components/ProductCard.astro';
|
|||||||
import { allProducts } from '../../lib/pricing';
|
import { allProducts } from '../../lib/pricing';
|
||||||
const sorted = [...allProducts].sort((a,b)=>b.score-a.score);
|
const sorted = [...allProducts].sort((a,b)=>b.score-a.score);
|
||||||
---
|
---
|
||||||
<Base title="Alle portablen Klimageräte im Vergleich – Kühlfinder" description="Kuratierte mobile Split- und Monoblock-Klimageräte im Vergleich, mit Live-Verfügbarkeit und Preis-Check.">
|
<Base title="Alle portablen Klimageräte im Vergleich – Kühlfinder" description="Kuratierte mobile Split- und Monoblock-Klimageräte im Vergleich, mit Live-Preisen und Preis-Check.">
|
||||||
<section class="wrap" style="margin-top:2rem">
|
<section class="wrap" style="margin-top:2rem">
|
||||||
<p class="kicker"><span class="u"></span>Kuratierter Vergleich</p>
|
<p class="kicker"><span class="u"></span>Kuratierter Vergleich</p>
|
||||||
<h1>Alle Geräte</h1>
|
<h1>Alle Geräte</h1>
|
||||||
|
|||||||
+20
-12
@@ -13,7 +13,7 @@ const Bell='<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="
|
|||||||
const IMG_HERO='/img/hero-klima.jpg';
|
const IMG_HERO='/img/hero-klima.jpg';
|
||||||
const IMG_PHONE='/img/alarm-phone.jpg';
|
const IMG_PHONE='/img/alarm-phone.jpg';
|
||||||
---
|
---
|
||||||
<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.">
|
<Base title="Kühlfinder – Wo gibt es gerade ein Klimagerät? Live-Preise & Standort-Abdeckung" description="Live-Preise von Klimageräten direkt bei Händlern (eigener Crawler) in DE & Nachbarländern, plus Standort-Abdeckung, Preis-Check gegen Wucher und Alarm.">
|
||||||
<section class="wrap herowrap">
|
<section class="wrap herowrap">
|
||||||
<WeatherBanner />
|
<WeatherBanner />
|
||||||
<div class="hero-card reveal">
|
<div class="hero-card reveal">
|
||||||
@@ -30,7 +30,7 @@ const IMG_PHONE='/img/alarm-phone.jpg';
|
|||||||
<a class="btn ghost" href="/karte" id="locbtn" data-umami-event="hero_naehe">In meiner Nähe</a>
|
<a class="btn ghost" href="/karte" id="locbtn" data-umami-event="hero_naehe">In meiner Nähe</a>
|
||||||
</form>
|
</form>
|
||||||
<div class="herofeat">
|
<div class="herofeat">
|
||||||
<span><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg> Echtzeit-Verfügbarkeit</span>
|
<span><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg> Live-Preise direkt geprüft</span>
|
||||||
<span><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.5 2.5 15 0 18M12 3c-2.5 2.5-2.5 15 0 18"/></svg> 6 Länder</span>
|
<span><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.5 2.5 15 0 18M12 3c-2.5 2.5-2.5 15 0 18"/></svg> 6 Länder</span>
|
||||||
<span><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M20 6 9 17l-5-5"/></svg> Preis-Check gegen Wucher</span>
|
<span><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M20 6 9 17l-5-5"/></svg> Preis-Check gegen Wucher</span>
|
||||||
<span><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg> Alarm & Preiswecker</span>
|
<span><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg> Alarm & Preiswecker</span>
|
||||||
@@ -53,7 +53,7 @@ const IMG_PHONE='/img/alarm-phone.jpg';
|
|||||||
|
|
||||||
<section class="wrap">
|
<section class="wrap">
|
||||||
<div class="features">
|
<div class="features">
|
||||||
<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" href="/karte"><div class="ic" set:html={Pin}></div><h3>Live-Karte</h3><p class="muted">Live-Preise direkt bei Händlern plus Standort-Abdeckung – 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="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>
|
<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>
|
</div>
|
||||||
@@ -109,7 +109,7 @@ const IMG_PHONE='/img/alarm-phone.jpg';
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="wrap"><div class="split">
|
<section class="wrap"><div class="split">
|
||||||
<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 reveal"><h2>So funktioniert's</h2><ol class="steps"><li>PLZ eingeben oder Standort nutzen.</li><li>Live-Preise vergleichen, Standort-Abdeckung + Prognose sehen.</li><li>Beim Händler prüfen – 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 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>
|
</div></section>
|
||||||
</Base>
|
</Base>
|
||||||
@@ -129,11 +129,9 @@ const IMG_PHONE='/img/alarm-phone.jpg';
|
|||||||
.herofeat span{display:inline-flex;align-items:center;gap:.45rem}.herofeat svg{color:var(--petrol-deep)}
|
.herofeat span{display:inline-flex;align-items:center;gap:.45rem}.herofeat svg{color:var(--petrol-deep)}
|
||||||
.hero-live{display:flex;flex-direction:column;gap:.5rem;padding:1.1rem;justify-self:end;width:100%;max-width:340px;background:rgba(255,255,255,.92);backdrop-filter:blur(4px)}
|
.hero-live{display:flex;flex-direction:column;gap:.5rem;padding:1.1rem;justify-self:end;width:100%;max-width:340px;background:rgba(255,255,255,.92);backdrop-filter:blur(4px)}
|
||||||
.lc-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;font-size:.95rem}
|
.lc-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;font-size:.95rem}
|
||||||
.livelist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
|
.livelist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.45rem}
|
||||||
.livelist li{display:flex;justify-content:space-between;align-items:center;gap:.5rem;padding:.6rem .1rem;border-top:1px solid var(--line);font-size:.88rem}
|
.livelist li{display:block}
|
||||||
.livelist li:first-child{border-top:none}
|
.hero-live .offer-card{box-shadow:none;border-color:var(--line)}
|
||||||
.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);font-weight:600}
|
|
||||||
.stat-tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:.9rem;margin-top:1rem}
|
.stat-tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:.9rem;margin-top:1rem}
|
||||||
.stile{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:1rem 1.1rem;box-shadow:var(--shadow-card)}
|
.stile{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:1rem 1.1rem;box-shadow:var(--shadow-card)}
|
||||||
.st-l{font-size:.82rem;color:var(--slate)}
|
.st-l{font-size:.82rem;color:var(--slate)}
|
||||||
@@ -161,6 +159,8 @@ const IMG_PHONE='/img/alarm-phone.jpg';
|
|||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
const euro=(n,c)=>n==null?'–':new Intl.NumberFormat('de-DE',{style:'currency',currency:c||'EUR',maximumFractionDigits:0}).format(n);
|
const euro=(n,c)=>n==null?'–':new Intl.NumberFormat('de-DE',{style:'currency',currency:c||'EUR',maximumFractionDigits:0}).format(n);
|
||||||
|
const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
|
const safeUrl=u=>{try{const x=new URL(u,location.origin);return (x.protocol==='http:'||x.protocol==='https:')?x.href:'#';}catch(e){return '#';}};
|
||||||
document.getElementById('locbtn')?.addEventListener('click',e=>{ if(navigator.geolocation){ /* Karte hat eigene Standortfunktion */ } });
|
document.getElementById('locbtn')?.addEventListener('click',e=>{ if(navigator.geolocation){ /* Karte hat eigene Standortfunktion */ } });
|
||||||
const CRAWLER=(import.meta.env.PUBLIC_CRAWLER_URL)||'http://vnqwgi4gq520hs1p4bsgi4w6.46.224.49.66.sslip.io';
|
const CRAWLER=(import.meta.env.PUBLIC_CRAWLER_URL)||'http://vnqwgi4gq520hs1p4bsgi4w6.46.224.49.66.sslip.io';
|
||||||
fetch('/data/monitor.json').then(r=>r.json()).then(d=>{
|
fetch('/data/monitor.json').then(r=>r.json()).then(d=>{
|
||||||
@@ -170,9 +170,17 @@ const IMG_PHONE='/img/alarm-phone.jpg';
|
|||||||
window.kfAnim && window.kfAnim();
|
window.kfAnim && window.kfAnim();
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
fetch(CRAWLER+'/api/live').then(r=>r.json()).then(d=>{
|
fetch(CRAWLER+'/api/live').then(r=>r.json()).then(d=>{
|
||||||
const offers=(d.offers||[]).filter(o=>o.price);
|
const when=d.generatedAt?new Date(d.generatedAt):null;
|
||||||
|
const mins=when?Math.max(1,Math.round((Date.now()-when)/60000)):'?';
|
||||||
|
const offers=(d.offers||[]).filter(o=>o.price).sort((a,b)=>a.price-b.price);
|
||||||
document.getElementById('p-ac').textContent=offers.length;
|
document.getElementById('p-ac').textContent=offers.length;
|
||||||
const ac2=document.getElementById('p-ac2'); if(ac2) ac2.textContent=offers.length;
|
const ac2=document.getElementById('p-ac2'); if(ac2) ac2.textContent=offers.length;
|
||||||
document.getElementById('livelist').innerHTML = offers.slice(0,6).map(o=>`<li><span><span class="lm">${o.chain}</span> <span class="lc">${o.name}</span></span><span class="lp">${euro(o.price)}</span></li>`).join('') || '<li class="lc">Crawler startet – gleich wieder reinschauen.</li>';
|
const meta=o=>o.availability==='online_available'?'Online-Angebot gefunden':'Produktseite geprüft';
|
||||||
}).catch(()=>{document.getElementById('p-ac').textContent='–';document.getElementById('livelist').innerHTML='<li class="lc">Live-Preise gerade nicht erreichbar.</li>';});
|
document.getElementById('livelist').innerHTML = offers.slice(0,5).map(o=>
|
||||||
|
`<li><a class="offer-card" href="${safeUrl(o.url)}" target="_blank" rel="sponsored nofollow noopener">`
|
||||||
|
+`<span class="offer-source">${esc(o.chain)}</span><span class="offer-price">${euro(o.price)}</span>`
|
||||||
|
+`<span class="offer-title">${esc(o.name)}</span>`
|
||||||
|
+`<span class="offer-meta"><span class="status-dot status-ok"></span>${meta(o)} · vor ${mins} Min</span></a></li>`
|
||||||
|
).join('') || '<li class="empty-note">Crawler startet – gleich wieder reinschauen.</li>';
|
||||||
|
}).catch(()=>{document.getElementById('p-ac').textContent='–';document.getElementById('livelist').innerHTML='<li class="empty-note">Live-Preise gerade nicht erreichbar.</li>';});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+45
-29
@@ -4,16 +4,16 @@ import WeatherBanner from '../components/WeatherBanner.astro';
|
|||||||
import { allProducts } from '../lib/pricing';
|
import { allProducts } from '../lib/pricing';
|
||||||
const models = allProducts.map(p=>({slug:p.slug,name:p.name}));
|
const models = allProducts.map(p=>({slug:p.slug,name:p.name}));
|
||||||
const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kühlfinder Kontrollzentrum',applicationCategory:'ShoppingApplication',
|
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.'};
|
description:'Live-Preise von Klimageräten direkt bei Händlern (eigener Crawler) plus Standort-Abdeckung, Prognose und Preis-Check gegen Wucher.'};
|
||||||
---
|
---
|
||||||
<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.">
|
<Base title="Klimagerät-Kontrollzentrum – Live-Preise & Standort-Abdeckung | Kühlfinder" description="Live-Preise von Klimageräten direkt bei Händlern (eigener Crawler), Standort-Abdeckung für DE/AT/CH/NL/PL/DK plus Preis-Check gegen Wucher und Prognose.">
|
||||||
<section class="mon-hero"><div class="wrap">
|
<section class="mon-hero"><div class="wrap">
|
||||||
<WeatherBanner />
|
<WeatherBanner />
|
||||||
<div class="mon-hero-row" style="margin-top:1rem">
|
<div class="mon-hero-row" style="margin-top:1rem">
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow"><span class="dot ok live"></span>Live-Preis-Crawler · Standort-Abdeckung (Demo) · 6 Länder</span>
|
<span class="eyebrow"><span class="dot ok live"></span>Live-Preis-Crawler · Standort-Abdeckung (Demo) · 6 Länder</span>
|
||||||
<h1>Dein Klimagerät-Kontrollzentrum</h1>
|
<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>
|
<p class="lead"><strong>Live-Preise</strong> direkt bei Händlern (eigener Crawler) plus Standort-Abdeckung – mit <strong>Prognose</strong> und <strong>Preis-Check gegen Wucher</strong>. Filialbestände sind noch nicht live verifiziert.</p>
|
||||||
</div>
|
</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 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>
|
||||||
@@ -34,11 +34,11 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
|
|||||||
<section class="wrap mon-grid">
|
<section class="wrap mon-grid">
|
||||||
<div class="mapcol">
|
<div class="mapcol">
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<div class="legend"><span><span class="dot ok"></span>verfügbar</span><span><span class="dot" style="background:var(--petrol)"></span>Markt (Cluster)</span></div>
|
<div class="legend"><span><span class="dot" style="background:var(--petrol)"></span>Markt-Standort (Demo-Abdeckung)</span></div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="feedcol card">
|
<aside class="feedcol card">
|
||||||
<h2 class="h-sm">Letzte Verfügbarkeiten</h2>
|
<h2 class="h-sm">Live-Preis-Funde</h2>
|
||||||
<p class="note">Restock-Ereignisse, neueste zuerst.</p>
|
<p class="note">Verifizierte Restock-Ereignisse (eigener Crawler), neueste zuerst.</p>
|
||||||
<ol class="feed" id="feed"><li class="skel" style="height:46px"></li><li class="skel" style="height:46px"></li></ol>
|
<ol class="feed" id="feed"><li class="skel" style="height:46px"></li><li class="skel" style="height:46px"></li></ol>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
@@ -52,7 +52,6 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
|
|||||||
<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>
|
<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>
|
||||||
<div class="tb-row chips">
|
<div class="tb-row chips">
|
||||||
<button class="chip ghost" data-toggle="avail">Nur verfügbare (live)</button>
|
|
||||||
<button class="chip ghost" data-toggle="chance">Beste Prognose</button>
|
<button class="chip ghost" data-toggle="chance">Beste Prognose</button>
|
||||||
<span class="sep"></span>
|
<span class="sep"></span>
|
||||||
<button class="chip on" data-kind="all">Alle Typen</button>
|
<button class="chip on" data-kind="all">Alle Typen</button>
|
||||||
@@ -119,6 +118,8 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
|
|||||||
.pcell{display:flex;flex-direction:column}.pcell .num{font-size:1.05rem;font-weight:600}.pcell small{font-size:.72rem;color:var(--slate)}
|
.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)}
|
.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}
|
.onlinebar{margin-top:.8rem;display:flex;gap:.5rem;flex-wrap:wrap;align-items:stretch;padding:.7rem .9rem}
|
||||||
|
.onlinebar .offer-card{flex:1 1 210px;max-width:280px}
|
||||||
|
@media(max-width:560px){.onlinebar .offer-card{flex:1 1 100%;max-width:none}}
|
||||||
.osrc{display:flex;flex-direction:column;gap:.1rem;border:1px solid var(--line);border-radius:10px;padding:.45rem .65rem;min-width:128px}
|
.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 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)}
|
.osrc.wucher{border-color:var(--danger);background:var(--danger-bg)}.osrc.wucher .num{color:var(--danger)}
|
||||||
@@ -162,6 +163,8 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
|
|||||||
const storeLink=(s)=>{ const q=(state.model!=='all'&&DATA.products[state.model]?DATA.products[state.model].name:'Klimagerät'); const fn=CHAIN_SEARCH[s.c]; return fn?fn(q):('https://www.google.com/search?q='+encodeURIComponent(s.c+' '+s.ci+' '+q)); };
|
const storeLink=(s)=>{ const q=(state.model!=='all'&&DATA.products[state.model]?DATA.products[state.model].name:'Klimagerät'); const fn=CHAIN_SEARCH[s.c]; return fn?fn(q):('https://www.google.com/search?q='+encodeURIComponent(s.c+' '+s.ci+' '+q)); };
|
||||||
const LIVE_URL=(import.meta.env.PUBLIC_CRAWLER_URL)||'http://vnqwgi4gq520hs1p4bsgi4w6.46.224.49.66.sslip.io';
|
const LIVE_URL=(import.meta.env.PUBLIC_CRAWLER_URL)||'http://vnqwgi4gq520hs1p4bsgi4w6.46.224.49.66.sslip.io';
|
||||||
const fmt=(n,c)=>n==null?'–':new Intl.NumberFormat('de-DE',{style:'currency',currency:c||'EUR',maximumFractionDigits:0}).format(n);
|
const fmt=(n,c)=>n==null?'–':new Intl.NumberFormat('de-DE',{style:'currency',currency:c||'EUR',maximumFractionDigits:0}).format(n);
|
||||||
|
const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
|
const safeUrl=u=>{try{const x=new URL(u,location.origin);return (x.protocol==='http:'||x.protocol==='https:')?x.href:'#';}catch(e){return '#';}};
|
||||||
const $=s=>document.querySelector(s),$$=s=>Array.from(document.querySelectorAll(s));
|
const $=s=>document.querySelector(s),$$=s=>Array.from(document.querySelectorAll(s));
|
||||||
let DATA=null,center=null,shown=120;
|
let DATA=null,center=null,shown=120;
|
||||||
const state={q:'',model:'all',countries:new Set(),kind:'all',avail:false,chance:false,radius:500};
|
const state={q:'',model:'all',countries:new Set(),kind:'all',avail:false,chance:false,radius:500};
|
||||||
@@ -208,11 +211,11 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
|
|||||||
const tb=$('#rows');tb.innerHTML='';
|
const tb=$('#rows');tb.innerHTML='';
|
||||||
for(const s of list.slice(0,shown)){
|
for(const s of list.slice(0,shown)){
|
||||||
const st=status(s);
|
const st=status(s);
|
||||||
const stk = st.a?`<span class="stk ok"><span class="dot ok"></span> live verfügbar</span>`:`<a class="chk-link" href="${surl}" target="_blank" rel="noopener">beim Händler prüfen ↗</a>`;
|
const surl=safeUrl(storeLink(s));
|
||||||
|
const stk = `<a class="chk-link" href="${surl}" target="_blank" rel="noopener">beim Händler prüfen ↗</a>`;
|
||||||
const distTd=center?`<td class="ce num">${dist(center[1],center[0],s.la,s.lo).toFixed(0)} km</td>`:'';
|
const distTd=center?`<td class="ce num">${dist(center[1],center[0],s.la,s.lo).toFixed(0)} km</td>`:'';
|
||||||
const surl=storeLink(s);
|
tb.insertAdjacentHTML('beforeend',`<tr><td><span class="flag">${s.flag||FLAG[s.cc]||''}</span> <a class="market-link" href="${surl}" target="_blank" rel="noopener">${esc(s.n)}</a><span class="kindb">${s.kind==='elektro'?'Elektro':'Baumarkt'}</span><div class="faddr">${esc(s.z)} ${esc(s.ci)} · Demo-Standort, Filialbestand nicht verifiziert</div></td>
|
||||||
tb.insertAdjacentHTML('beforeend',`<tr><td><span class="flag">${s.flag||FLAG[s.cc]||''}</span> <a class="fname" href="${surl}" target="_blank" rel="noopener">${s.n}</a><span class="kindb">${s.kind==='elektro'?'Elektro':'Baumarkt'}</span><div class="faddr">${s.z} ${s.ci} · <span title="zuletzt geprüft">Stand vor ${s.t||'?'} Min</span></div></td>
|
<td class="ce">${stk}</td><td class="ce num">–</td>
|
||||||
<td class="ce">${stk}</td><td class="ce num" title="Referenzpreis (Demo)">${st.p?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>`);
|
<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`;
|
$('#rowcount').textContent=`${Math.min(shown,list.length)} von ${list.length} Märkten`;
|
||||||
@@ -222,10 +225,13 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
|
|||||||
function fillStatic(){
|
function fillStatic(){
|
||||||
$('#h-updated').textContent=DATA.stats.updated;
|
$('#h-updated').textContent=DATA.stats.updated;
|
||||||
$$('#stats b[data-k]').forEach(b=>b.textContent=(DATA.stats[b.dataset.k]??'–').toLocaleString('de-DE'));
|
$$('#stats b[data-k]').forEach(b=>b.textContent=(DATA.stats[b.dataset.k]??'–').toLocaleString('de-DE'));
|
||||||
$('#s-wucher').textContent=(DATA.online||[]).filter(o=>o.wucher).length;
|
$('#s-wucher').textContent=(DATA.online||[]).filter(o=>o.wucher).length||'–';
|
||||||
$('#h-feednote').textContent='Feed live';
|
$('#h-feednote').textContent='Standort-Abdeckung (Demo)';
|
||||||
$('#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('');
|
const feed=DATA.feed||[];
|
||||||
$('#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('');
|
$('#feed').innerHTML = feed.length
|
||||||
|
? feed.slice(0,18).map(f=>`<li><div class="fl"><b>${FLAG[f.cc]||''} ${esc(f.name)}</b><span>${esc(f.model)}</span></div><span class="ft">${rel(f.at)}</span></li>`).join('')
|
||||||
|
: '<li class="empty-note">Noch keine verifizierten Restock-Ereignisse. Sobald der Crawler eine Wiederverfügbarkeit erkennt, erscheint sie hier.</li>';
|
||||||
|
// Online-Leiste wird von loadLive() mit echten Live-Angeboten gefüllt
|
||||||
const ccs=[...new Set(DATA.stores.map(s=>s.cc))];
|
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').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();}));
|
$$('#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();}));
|
||||||
@@ -234,20 +240,30 @@ const jsonld={'@context':'https://schema.org','@type':'WebApplication',name:'Kü
|
|||||||
loadLive();
|
loadLive();
|
||||||
}
|
}
|
||||||
function loadLive(){
|
function loadLive(){
|
||||||
fetch(LIVE_URL+'/api/live').then(r=>r.json()).then(d=>{
|
|
||||||
if(!d||!d.offers||!d.offers.length)return;
|
|
||||||
const when=d.generatedAt?new Date(d.generatedAt):null;
|
|
||||||
const mins=when?Math.max(1,Math.round((Date.now()-when)/60000)):'?';
|
|
||||||
const items=d.offers.filter(o=>o.price||o.availability==='discontinued').map(o=>{
|
|
||||||
const av=o.availability==='in_stock'?'Produktseite: verfügbar':(o.availability==='online_available'?'online verfügbar':(o.availability==='discontinued'?'⚠ Seite entfernt (404)':(o.availability==='blocked'?'blockiert':'vergriffen')));
|
|
||||||
const cls=(o.availability==='out_of_stock')?'':'';
|
|
||||||
return `<a class="osrc ${o.availability==='discontinued'?'wucher':''}" href="${o.url}" target="_blank" rel="sponsored nofollow noopener"><b>${o.chain} · LIVE</b><span class="num">${fmt(o.price)}</span><small>${o.name} · ${av}</small></a>`;
|
|
||||||
}).join('');
|
|
||||||
const bar=$('#onlinebar');
|
const bar=$('#onlinebar');
|
||||||
const liveN=d.offers.filter(o=>o.price).length; var le=document.getElementById('s-live'); if(le)le.textContent=liveN;
|
fetch(LIVE_URL+'/api/live').then(r=>r.json()).then(d=>{
|
||||||
const note = d.blocked?` · ${d.blocked} Quelle(n) blockiert (Proxy nötig)`:'';
|
const offers=(d&&d.offers)||[];
|
||||||
bar.insertAdjacentHTML('afterbegin', `<span class="livebadge" title="eigener Live-Crawler"><span class="dot ok live"></span> LIVE gescrapt · Stand vor ${mins} Min${note}</span>`+items);
|
const when=d&&d.generatedAt?new Date(d.generatedAt):null;
|
||||||
}).catch(()=>{});
|
const mins=when?Math.max(1,Math.round((Date.now()-when)/60000)):'?';
|
||||||
|
const liveN=offers.filter(o=>o.price).length; const le=document.getElementById('s-live'); if(le)le.textContent=liveN||'–';
|
||||||
|
const shownOffers=offers.filter(o=>o.price||o.availability==='discontinued')
|
||||||
|
.sort((a,b)=>(a.price||1e9)-(b.price||1e9));
|
||||||
|
const items=shownOffers.map(o=>{
|
||||||
|
let dot='status-ok',meta='Produktseite geprüft',alert='';
|
||||||
|
if(o.availability==='online_available'){meta='Online-Angebot gefunden';}
|
||||||
|
else if(o.availability==='in_stock'){meta='Produktseite: Angebot vorhanden';}
|
||||||
|
else if(o.availability==='discontinued'){dot='status-bad';meta='⚠ Produktseite entfernt (404)';alert=' is-alert';}
|
||||||
|
else if(o.availability==='out_of_stock'){dot='status-mut';meta='derzeit vergriffen';}
|
||||||
|
const price=o.price?fmt(o.price):'—';
|
||||||
|
return `<a class="offer-card${alert}" href="${safeUrl(o.url)}" target="_blank" rel="sponsored nofollow noopener">`
|
||||||
|
+`<span class="offer-source">${esc(o.chain)}</span><span class="offer-price">${price}</span>`
|
||||||
|
+`<span class="offer-title">${esc(o.name)}</span>`
|
||||||
|
+`<span class="offer-meta"><span class="status-dot ${dot}"></span>${meta} · vor ${mins} Min</span></a>`;
|
||||||
|
}).join('');
|
||||||
|
const note = d&&d.blocked?` · ${d.blocked} Quelle(n) blockiert`:'';
|
||||||
|
const head=`<span class="livebadge" title="eigener Live-Crawler"><span class="dot ok live"></span> LIVE-Preise gescrapt · Stand vor ${mins} Min${note}</span>`;
|
||||||
|
bar.innerHTML = items ? (head+items) : '<span class="empty-note">Live-Preise gerade nicht erreichbar – bitte gleich erneut schauen.</span>';
|
||||||
|
}).catch(()=>{bar.innerHTML='<span class="empty-note">Live-Preise gerade nicht erreichbar.</span>';});
|
||||||
}
|
}
|
||||||
map.on('load',()=>{
|
map.on('load',()=>{
|
||||||
map.addSource('all',{type:'geojson',data:{type:'FeatureCollection',features:[]},cluster:true,clusterRadius:46,clusterMaxZoom:9});
|
map.addSource('all',{type:'geojson',data:{type:'FeatureCollection',features:[]},cluster:true,clusterRadius:46,clusterMaxZoom:9});
|
||||||
@@ -256,7 +272,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.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.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.addLayer({id:'av',type:'circle',source:'av',paint:{'circle-color':'#1d9e75','circle-radius':7,'circle-stroke-width':2,'circle-stroke-color':'#fff'}});
|
||||||
map.on('click','av',e=>{const p=e.features[0].properties;new maplibregl.Popup().setLngLat(e.lngLat).setHTML(`<b>${p.n}</b><br>${p.ci} – verfügbar<br><span style=\"color:#666;font-size:.8em\">Stand vor ${p.t||'?'} Min</span>`).addTo(map);});
|
map.on('click','av',e=>{const p=e.features[0].properties;new maplibregl.Popup().setLngLat(e.lngLat).setHTML(`<b>${p.n}</b><br>${p.ci} · Demo-Standort<br><span style=\"color:#666;font-size:.8em\">Filialbestand nicht verifiziert</span>`).addTo(map);});
|
||||||
map.on('click','cl',e=>{const f=map.queryRenderedFeatures(e.point,{layers:['cl']});map.getSource('all').getClusterExpansionZoom(f[0].properties.cluster_id).then(z=>map.easeTo({center:f[0].geometry.coordinates,zoom:z}));});
|
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();
|
if(DATA)render();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { allProducts, priceIntel, euro } from '../lib/pricing';
|
|||||||
export const GET = () => {
|
export const GET = () => {
|
||||||
const L:string[] = [
|
const L:string[] = [
|
||||||
'# Kühlfinder – Vollständige Referenz',
|
'# Kühlfinder – Vollständige Referenz',
|
||||||
'> Kontrollzentrum für Klimagerät-Verfügbarkeit in DE/AT/CH/NL/PL/DK: Live-Bestand in Bau- und Elektromärkten + Online/Amazon/eBay, Verfügbarkeits-Prognose, Preis-Check mit Wucher-Warnung, Custom-Alarm + Preiswecker. Eigener Scraper, keine Drittdaten.',
|
'> Kontrollzentrum für Klimageräte in DE/AT/CH/NL/PL/DK: Live-Preise direkt bei Händlern (eigener Crawler, ausgewählte Produktseiten) + Standort-Abdeckung, Verfügbarkeits-Prognose, Preis-Check mit Wucher-Warnung, Custom-Alarm + Preiswecker. Filialbestände sind noch nicht live verifiziert.',
|
||||||
'',
|
'',
|
||||||
'## Kernfunktionen',
|
'## Kernfunktionen',
|
||||||
'- Karte & Tabelle mit Standort-Abdeckung (Demo) + live geprüften Online-Preisen ausgewählter Produktseiten.',
|
'- Karte & Tabelle mit Standort-Abdeckung (Demo) + live geprüften Online-Preisen ausgewählter Produktseiten.',
|
||||||
|
|||||||
@@ -118,3 +118,19 @@ header.site .brand{letter-spacing:-.02em}
|
|||||||
.anim-in{animation:kf-in .45s cubic-bezier(.16,1,.3,1) both}
|
.anim-in{animation:kf-in .45s cubic-bezier(.16,1,.3,1) both}
|
||||||
.flag{font-size:1.05em;line-height:1}
|
.flag{font-size:1.05em;line-height:1}
|
||||||
@media (prefers-reduced-motion: reduce){.reveal{opacity:1!important;transform:none!important}.dot.live{animation:none}}
|
@media (prefers-reduced-motion: reduce){.reveal{opacity:1!important;transform:none!important}.dot.live{animation:none}}
|
||||||
|
|
||||||
|
/* --- Offer-Cards (Live-Angebote): Quelle/Preis/Name/Status sauber getrennt, ganze Karte klickbar --- */
|
||||||
|
.offer-card{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.2rem .9rem;align-content:start;text-decoration:none;color:var(--ink);border:1px solid var(--line);border-radius:12px;padding:.6rem .8rem;background:var(--card);box-shadow:var(--shadow-card);transition:border-color .15s,transform .1s}
|
||||||
|
.offer-card:hover{border-color:var(--petrol);transform:translateY(-1px)}
|
||||||
|
.offer-card:focus-visible{outline:2px solid var(--petrol);outline-offset:2px}
|
||||||
|
.offer-source{font-weight:600;min-width:0;overflow-wrap:anywhere}
|
||||||
|
.offer-price{font-family:var(--mono);font-weight:700;white-space:nowrap;text-align:right;color:var(--petrol-deep)}
|
||||||
|
.offer-title{grid-column:1 / -1;color:var(--ink-2);font-size:.82rem;line-height:1.35;overflow-wrap:anywhere}
|
||||||
|
.offer-meta{grid-column:1 / -1;color:var(--slate);font-size:.74rem;display:inline-flex;gap:.4rem;align-items:center}
|
||||||
|
.offer-card:hover .offer-source{text-decoration:underline}
|
||||||
|
.offer-card.is-alert{border-color:var(--danger)}.offer-card.is-alert .offer-price{color:var(--danger)}
|
||||||
|
.status-dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto;display:inline-block}
|
||||||
|
.status-ok{background:var(--ok-dot)}.status-warn{background:var(--warm)}.status-bad{background:var(--danger)}.status-mut{background:var(--slate)}
|
||||||
|
.market-link{font-weight:600;font-size:.92rem;color:var(--petrol-deep);text-decoration:none}
|
||||||
|
.market-link:hover{text-decoration:underline}
|
||||||
|
.empty-note{color:var(--slate);font-size:.85rem;padding:.6rem .2rem}
|
||||||
|
|||||||
Reference in New Issue
Block a user