feat: KöPi - echte Angebote mit Bild, Preis, Markt direkt in der App

This commit is contained in:
2026-07-08 17:09:09 +02:00
parent ec872eb76d
commit 017decf59f
2 changed files with 270 additions and 242 deletions

View File

@@ -64,7 +64,7 @@ function isTargetPublisher(name) {
return TARGET_PUBLISHERS.some(p => n.includes(p));
}
// ── Angebote via Puppeteer ────────────────────────────────────────────────────
// ── Angebote via Puppeteer (BrochureBox_Basic + OfferGrid) ──────────────────
async function scrapeOffersWithPuppeteer() {
const cacheKey = 'koepi:offers';
const cached = cacheGet(cacheKey);
@@ -77,70 +77,69 @@ async function scrapeOffersWithPuppeteer() {
const browser = await puppeteer.launch({
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium-browser',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
args: ['--no-sandbox','--disable-setuid-sandbox','--disable-dev-shm-usage','--disable-gpu','--single-process'],
headless: true,
});
try {
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
await page.setExtraHTTPHeaders({ 'Accept-Language': 'de-DE,de;q=0.9' });
await page.goto(
'https://www.kaufda.de/Angebote/Koenig-Pilsener?lat=51.4332&lng=6.7625&zip=47051&city=Duisburg',
{ waitUntil: 'domcontentloaded', timeout: 25000 }
);
await new Promise(r => setTimeout(r, 3000));
await page.goto('https://www.kaufda.de/Angebote/Koenig-Pilsener?lat=51.4332&lng=6.7625&zip=47051&city=Duisburg', {
waitUntil: 'networkidle2',
timeout: 30000,
});
const data = await page.evaluate(() => {
// ── BrochureBox: Prospekte wo König Pilsener drin ist ──────────────────
const brochureOffers = Array.from(document.querySelectorAll('[data-testid="BrochureBox_Basic"]')).map(b => {
const text = b.textContent.trim();
const imgs = Array.from(b.querySelectorAll('img'));
const productImg = imgs[0]?.src || null;
const publisherImg = imgs[1]?.src || null;
// Warte auf Produktkacheln
await page.waitForSelector('[data-testid="offer-card"], .offer-card, article, [class*="offer"]', {
timeout: 10000,
}).catch(() => {});
// Text parsen: "neuKönig Pilsener bei Trinkgut"Aktuelle Angebote", Seite 6Mo. 6.7. - Sa. 11.7.2026..."
const storeMatch = text.match(/bei\s+(.+?)\s*[""„]/);
const titleMatch = text.match(/[""„](.+?)[""„]/);
const pageMatch = text.match(/Seite\s+(\d+)/);
const dateMatch = text.match(/(\w+\.\s+\d+\.\d+\.)\s*-\s*(\w+\.\s+\d+\.\d+\.\d+)/);
const validMatch = text.match(/(Noch \d+ Tage? gültig|Gültig bis \S+)/);
const isNew = text.startsWith('neu');
// Alle Angebotsdaten extrahieren
const offers = await page.evaluate(() => {
const results = [];
// Methode 1: Schema.org
document.querySelectorAll('script[type="application/ld+json"]').forEach(s => {
try {
const data = JSON.parse(s.textContent);
const items = Array.isArray(data) ? data : [data];
items.forEach(item => {
if (item['@type'] === 'Product') {
const offer = Array.isArray(item.offers) ? item.offers[0] : item.offers;
results.push({
name: item.name,
price: offer?.price || null,
store: offer?.seller?.name || null,
image: Array.isArray(item.image) ? item.image[0] : item.image || null,
validFrom: offer?.validFrom || null,
validTo: offer?.validThrough || null,
url: item.url || window.location.href,
});
}
});
} catch {}
return {
type: 'brochure',
store: storeMatch?.[1]?.trim() || null,
title: titleMatch?.[1]?.trim() || null,
page: pageMatch?.[1] ? parseInt(pageMatch[1]) : null,
dateRange: dateMatch ? `${dateMatch[1]} - ${dateMatch[2]}` : null,
validity: validMatch?.[1] || null,
isNew,
productImage: productImg,
publisherLogo: publisherImg,
};
});
// Methode 2: DOM-Kacheln
const cards = document.querySelectorAll('[class*="offer-card"], [class*="OfferCard"], [data-testid*="offer"], article[class*="offer"]');
cards.forEach(card => {
const name = card.querySelector('[class*="title"], [class*="name"], h2, h3')?.textContent?.trim();
const price = card.querySelector('[class*="price"], [class*="Price"]')?.textContent?.trim();
const store = card.querySelector('[class*="retailer"], [class*="publisher"], [class*="store"]')?.textContent?.trim();
const img = card.querySelector('img')?.src;
if (name) results.push({ name, price, store, image: img, url: window.location.href });
});
// ── OfferGrid: einzelne Produktkacheln mit Preisen ──────────────────────
const grid = document.querySelector('[data-testid="OfferGrid"]');
const gridOffers = grid ? Array.from(grid.querySelectorAll('[role="listitem"]')).map(item => {
const img = item.querySelector('img');
const ps = item.querySelectorAll('p');
const brand = ps[0]?.textContent?.trim() || null;
const name = ps[1]?.textContent?.trim() || null;
const store = ps[2]?.textContent?.trim() || null;
const price = item.querySelector('.text-primary, [class*="text-primary"]')?.textContent?.trim() || null;
const baseUnit = ps[3]?.textContent?.trim() || null;
return { type:'offer', brand, name, store, price, baseUnit, image: img?.src || null };
}).filter(o => o.name) : [];
return results;
return { brochureOffers, gridOffers };
});
const result = {
offers: offers.filter((o, i, arr) =>
arr.findIndex(x => x.name === o.name && x.store === o.store) === i
),
scrapedAt: new Date().toISOString(),
source: 'kaufda.de (puppeteer)',
brochureOffers: data.brochureOffers,
gridOffers: data.gridOffers,
scrapedAt: new Date().toISOString(),
source: 'kaufda.de',
};
cacheSet(cacheKey, result);

View File

@@ -1,102 +1,158 @@
import { useState, useEffect } from 'react';
import { api, S } from '../lib.js';
const MY_COLOR = '#f59e0b';
const GOLD = '#f59e0b';
function getMyRole() {
try { return JSON.parse(atob(localStorage.getItem('sk_token').split('.')[1])).role; } catch { return null; }
}
// ── Händler-Farben ────────────────────────────────────────────────────────────
const PUBLISHER_COLORS = {
'rewe': '#e2001a',
'edeka': '#ffd700',
'netto': '#0057a8',
'trinkgut': '#e87722',
'penny': '#e2001a',
'aldi': '#00538f',
'lidl': '#f5c900',
};
function publisherColor(name) {
if (!name) return '#666';
const n = name.toLowerCase();
for (const [key, color] of Object.entries(PUBLISHER_COLORS)) {
if (n.includes(key)) return color;
}
return '#555';
}
// ── Datum formatieren ─────────────────────────────────────────────────────────
function fmtDate(str) {
if (!str) return null;
try { return new Date(str).toLocaleDateString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric' }); }
catch { return str; }
}
function isExpiringSoon(validTo) {
if (!validTo) return false;
const diff = new Date(validTo) - new Date();
return diff > 0 && diff < 3 * 24 * 60 * 60 * 1000;
function publisherColor(name) {
if (!name) return '#555';
const n = name.toLowerCase();
if (n.includes('rewe')) return '#e2001a';
if (n.includes('edeka')) return '#e8a800';
if (n.includes('e center')) return '#e8a800';
if (n.includes('netto')) return '#0057a8';
if (n.includes('trinkgut')) return '#e87722';
if (n.includes('penny')) return '#e2001a';
if (n.includes('aldi')) return '#00538f';
if (n.includes('lidl')) return '#f5c900';
if (n.includes('hit')) return '#e2001a';
if (n.includes('alldrink')) return '#2ecc71';
if (n.includes('getränke')) return '#3498db';
return '#666';
}
function isNew(validFrom) {
if (!validFrom) return false;
const diff = new Date() - new Date(validFrom);
return diff >= 0 && diff < 3 * 24 * 60 * 60 * 1000;
}
// ── Angebots-Karte ────────────────────────────────────────────────────────────
function OfferCard({ offer }) {
// ── Prospekt-Angebot Karte ────────────────────────────────────────────────────
function BrochureOfferCard({ offer }) {
const color = publisherColor(offer.store);
return (
<a href={offer.url} target="_blank" rel="noopener noreferrer" style={{ textDecoration:'none' }}>
<div style={{
...S.card, padding:0, overflow:'hidden', cursor:'pointer',
border:'1px solid rgba(255,255,255,0.08)',
transition:'border-color 0.15s',
}}>
{offer.image && (
<img src={offer.image} alt={offer.name}
style={{ width:'100%', height:140, objectFit:'cover', display:'block' }}
<div style={{
...S.card, padding:0, overflow:'hidden',
border:'1px solid rgba(255,255,255,0.08)',
display:'flex', flexDirection:'column',
}}>
{/* Prospektseite als Bild */}
{offer.productImage && (
<div style={{ position:'relative', height:160, overflow:'hidden', flexShrink:0 }}>
<img src={offer.productImage} alt={`König Pilsener bei ${offer.store}`}
style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }}
onError={e => { e.target.style.display='none'; }}
/>
)}
<div style={{ padding:'10px 12px' }}>
<div style={{
display:'inline-block', background:`${color}22`, border:`1px solid ${color}44`,
borderRadius:4, padding:'2px 8px', marginBottom:6,
color, fontFamily:'monospace', fontSize:10, fontWeight:700,
}}>
{offer.store || 'Unbekannt'}
</div>
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:12, marginBottom:4, lineHeight:1.4 }}>
{offer.name}
</div>
{offer.price && (
<div style={{ color:MY_COLOR, fontFamily:'Space Mono,monospace', fontSize:16, fontWeight:700 }}>
{typeof offer.price === 'number' ? `${offer.price.toFixed(2)}` : offer.price}
</div>
)}
{(offer.validFrom || offer.validTo) && (
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:10, marginTop:4 }}>
{offer.validFrom && `Ab ${fmtDate(offer.validFrom)}`}
{offer.validFrom && offer.validTo && ' · '}
{offer.validTo && `Bis ${fmtDate(offer.validTo)}`}
</div>
{offer.isNew && (
<span style={{ position:'absolute', top:8, left:8,
background:'#4ecdc4', color:'#000', borderRadius:4,
padding:'2px 7px', fontSize:9, fontFamily:'monospace', fontWeight:700 }}>
NEU
</span>
)}
</div>
)}
<div style={{ padding:'10px 12px', flex:1 }}>
{/* Händler + Logo */}
<div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:8 }}>
{offer.publisherLogo && (
<img src={offer.publisherLogo} alt={offer.store}
style={{ height:20, width:'auto', objectFit:'contain', flexShrink:0 }}
onError={e => { e.target.style.display='none'; }}
/>
)}
<span style={{
background:`${color}22`, border:`1px solid ${color}44`,
borderRadius:4, padding:'2px 8px',
color, fontFamily:'monospace', fontSize:10, fontWeight:700,
}}>
{offer.store}
</span>
</div>
{/* Prospekttitel + Seite */}
{offer.title && (
<div style={{ color:'rgba(255,255,255,0.6)', fontFamily:'monospace', fontSize:11, marginBottom:4 }}>
📄 {offer.title}"
{offer.page && <span style={{ color:'rgba(255,255,255,0.35)', marginLeft:4 }}>Seite {offer.page}</span>}
</div>
)}
{/* Gültigkeit */}
{offer.validity && (
<div style={{ color: offer.validity.includes('Noch') ? '#f59e0b' : 'rgba(255,255,255,0.4)',
fontFamily:'monospace', fontSize:10, marginTop:4 }}>
🕐 {offer.validity}
</div>
)}
{offer.dateRange && (
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:10, marginTop:2 }}>
{offer.dateRange}
</div>
)}
</div>
</a>
</div>
);
}
// ── Prospekt-Karte ────────────────────────────────────────────────────────────
function ProspektCard({ b }) {
const color = publisherColor(b.publisher);
const expiring = isExpiringSoon(b.validTo);
const fresh = isNew(b.validFrom) || b.badges?.includes('new');
// ── Grid-Angebot Karte (mit Preis) ────────────────────────────────────────────
function GridOfferCard({ offer }) {
const color = publisherColor(offer.store);
return (
<div style={{
...S.card, padding:0, overflow:'hidden',
border:'1px solid rgba(255,255,255,0.08)',
display:'flex', flexDirection:'column',
background:'rgba(255,255,255,0.03)',
}}>
{offer.image && (
<div style={{ height:130, overflow:'hidden', flexShrink:0, background:'#fff', display:'flex', alignItems:'center', justifyContent:'center' }}>
<img src={offer.image} alt={offer.name}
style={{ width:'auto', height:'100%', maxWidth:'100%', objectFit:'contain', display:'block' }}
onError={e => { e.target.parentElement.style.display='none'; }}
/>
</div>
)}
<div style={{ padding:'10px 12px', flex:1 }}>
{offer.brand && (
<div style={{ color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:10, marginBottom:2 }}>
{offer.brand}
</div>
)}
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:12, fontWeight:600, marginBottom:6, lineHeight:1.3 }}>
{offer.name}
</div>
{offer.price && (
<div style={{ color:GOLD, fontFamily:'Space Mono,monospace', fontSize:18, fontWeight:700, marginBottom:2 }}>
{offer.price}
</div>
)}
{offer.baseUnit && (
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9 }}>
{offer.baseUnit}
</div>
)}
{offer.store && (
<div style={{
marginTop:8, display:'inline-block',
background:`${color}22`, border:`1px solid ${color}44`,
borderRadius:4, padding:'2px 7px',
color, fontFamily:'monospace', fontSize:10, fontWeight:700,
}}>
{offer.store}
</div>
)}
</div>
</div>
);
}
// ── Prospekt-Karte (shelf) ────────────────────────────────────────────────────
function ProspektCard({ b }) {
const color = publisherColor(b.publisher);
const expiring = b.badges?.includes('expiring_soon');
const fresh = b.badges?.includes('new');
return (
<a href={b.url} target="_blank" rel="noopener noreferrer" style={{ textDecoration:'none' }}>
<div style={{
@@ -106,48 +162,29 @@ function ProspektCard({ b }) {
<div style={{ position:'relative' }}>
{b.image && (
<img src={b.image} alt={b.title}
style={{ width:'100%', height:120, objectFit:'cover', display:'block' }}
style={{ width:'100%', height:110, objectFit:'cover', display:'block' }}
onError={e => { e.target.style.display='none'; }}
/>
)}
<div style={{ position:'absolute', top:6, left:6, display:'flex', gap:4 }}>
{fresh && (
<span style={{ background:'#4ecdc4', color:'#000', borderRadius:4, padding:'2px 6px', fontSize:9, fontFamily:'monospace', fontWeight:700 }}>NEU</span>
)}
{expiring && (
<span style={{ background:'#ef4444', color:'#fff', borderRadius:4, padding:'2px 6px', fontSize:9, fontFamily:'monospace', fontWeight:700 }}>ENDET BALD</span>
)}
{fresh && <span style={{ background:'#4ecdc4', color:'#000', borderRadius:4, padding:'2px 6px', fontSize:9, fontFamily:'monospace', fontWeight:700 }}>NEU</span>}
{expiring && <span style={{ background:'#ef4444', color:'#fff', borderRadius:4, padding:'2px 6px', fontSize:9, fontFamily:'monospace', fontWeight:700 }}>ENDET BALD</span>}
</div>
</div>
<div style={{ padding:'10px 12px' }}>
<div style={{
display:'inline-block', background:`${color}22`, border:`1px solid ${color}44`,
<div style={{ display:'inline-block', background:`${color}22`, border:`1px solid ${color}44`,
borderRadius:4, padding:'2px 8px', marginBottom:6,
color, fontFamily:'monospace', fontSize:10, fontWeight:700,
}}>
color, fontFamily:'monospace', fontSize:10, fontWeight:700 }}>
{b.publisher}
</div>
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:11, marginBottom:3, lineHeight:1.4 }}>
{b.title}
</div>
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:11, marginBottom:3, lineHeight:1.4 }}>{b.title}</div>
<div style={{ color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:10, marginBottom:2 }}>
📍 {b.store}{b.city && b.store !== b.city ? ` · ${b.city}` : ''}
</div>
{b.street && (
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:10, marginBottom:2 }}>
{b.street}{b.zip ? `, ${b.zip}` : ''}
</div>
)}
{b.street && <div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9 }}>{b.street}</div>}
{(b.validFrom || b.validTo) && (
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:10, marginTop:4 }}>
{b.validFrom && `Ab ${fmtDate(b.validFrom)}`}
{b.validFrom && b.validTo && ' · '}
{b.validTo && `Bis ${fmtDate(b.validTo)}`}
</div>
)}
{b.pageCount && (
<div style={{ color:'rgba(255,255,255,0.2)', fontFamily:'monospace', fontSize:9, marginTop:2 }}>
{b.pageCount} Seiten
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9, marginTop:4 }}>
{b.validFrom && `Ab ${fmtDate(b.validFrom)}`}{b.validFrom && b.validTo && ' · '}{b.validTo && `Bis ${fmtDate(b.validTo)}`}
</div>
)}
</div>
@@ -158,151 +195,143 @@ function ProspektCard({ b }) {
// ── Hauptkomponente ───────────────────────────────────────────────────────────
export default function Koepi({ toast }) {
const [tab, setTab] = useState('prospekte');
const [offers, setOffers] = useState(null);
const [prospekte, setProspekte] = useState(null);
const [loading, setLoading] = useState(false);
const [lastUpdate, setLastUpdate] = useState(null);
const [showAll, setShowAll] = useState(false);
const [tab, setTab] = useState('angebote');
const [offers, setOffers] = useState(null);
const [prospekte, setProspekte] = useState(null);
const [loading, setLoading] = useState(false);
const [showAll, setShowAll] = useState(false);
const isAdmin = getMyRole() === 'admin';
const load = async (t) => {
const loadOffers = async () => {
if (offers) return;
setLoading(true);
try {
if (t === 'offers' && !offers) {
const d = await api('/tools/koepi/offers');
setOffers(d);
setLastUpdate(d.scrapedAt);
} else if (t === 'prospekte' && !prospekte) {
const d = await api('/tools/koepi/prospekte');
setProspekte(d);
setLastUpdate(d.scrapedAt);
}
} catch(e) {
toast?.(e.message || 'Fehler beim Laden', 'error');
} finally {
setLoading(false);
}
try { setOffers(await api('/tools/koepi/offers')); }
catch(e) { toast?.(e.message || 'Fehler', 'error'); }
finally { setLoading(false); }
};
useEffect(() => { load(tab); }, [tab]);
const loadProspekte = async () => {
if (prospekte) return;
setLoading(true);
try { setProspekte(await api('/tools/koepi/prospekte')); }
catch(e) { toast?.(e.message || 'Fehler', 'error'); }
finally { setLoading(false); }
};
useEffect(() => {
if (tab === 'angebote') loadOffers();
else loadProspekte();
}, [tab]);
const clearCache = async () => {
try {
await api('/tools/koepi/clear-cache', { body:{} });
setOffers(null);
setProspekte(null);
setLastUpdate(null);
toast('Cache geleert — Daten werden neu geladen');
load(tab);
setOffers(null); setProspekte(null);
toast('Cache geleert');
if (tab === 'angebote') { setLoading(true); setOffers(await api('/tools/koepi/offers')); setLoading(false); }
else { setLoading(true); setProspekte(await api('/tools/koepi/prospekte')); setLoading(false); }
} catch(e) { toast?.(e.message, 'error'); }
};
const currentProspekte = prospekte ? [...(prospekte.targeted || []), ...(showAll ? (prospekte.others || []) : [])] : [];
return (
<div style={{ maxWidth:700, margin:'0 auto' }}>
{/* Header */}
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:24, flexWrap:'wrap' }}>
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:20, flexWrap:'wrap' }}>
<h2 style={{ margin:0, fontSize:15, fontFamily:'monospace', color:'rgba(255,255,255,0.55)', letterSpacing:2, fontWeight:400 }}>
🍺 KÖPI
🍺 KÖPI — König Pilsener Angebote
</h2>
<div style={{ flex:1 }}/>
{isAdmin && (
<button onClick={clearCache} style={{ ...S.btn('#666666', true), fontSize:10 }}>
🗑 Cache leeren
</button>
<button onClick={clearCache} style={{ ...S.btn('#666666', true), fontSize:10 }}>🗑 Cache</button>
)}
</div>
{/* Subheader */}
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:11, marginBottom:16, lineHeight:1.6 }}>
König Pilsener Angebote im Raum Duisburg · Daten von kaufDA.de
{lastUpdate && <span style={{ marginLeft:8 }}>· Stand: {new Date(lastUpdate).toLocaleString('de-DE')}</span>}
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:10, marginBottom:14 }}>
Raum Duisburg/Essen/Düsseldorf · Quelle: kaufDA.de · Cache 1h
</div>
{/* Tabs */}
<div style={{ display:'flex', gap:8, marginBottom:20 }}>
{[['prospekte','📋 Prospekte'],['offers','🏷️ Angebote']].map(([t, label]) => (
{[['angebote','🍺 Angebote'],['prospekte','📋 Alle Prospekte']].map(([t, label]) => (
<button key={t} onClick={() => setTab(t)} style={{
...S.btn(tab===t ? MY_COLOR : '#444444', true),
fontSize:12, padding:'6px 14px',
...S.btn(tab===t ? GOLD : '#444444', true), fontSize:12, padding:'6px 14px',
}}>{label}</button>
))}
</div>
{/* Laden */}
{loading && (
<div style={{ color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:13, textAlign:'center', padding:'40px 0' }}>
Lade Daten von kaufDA.de
⏳ Lade Angebote… (kann bis zu 15 Sekunden dauern)
</div>
)}
{/* Prospekte */}
{!loading && tab === 'prospekte' && prospekte && (
{/* Angebote Tab */}
{!loading && tab === 'angebote' && offers && (
<div>
{prospekte.targeted?.length > 0 ? (
{/* Grid-Angebote mit Preisen */}
{offers.gridOffers?.length > 0 && (
<>
<div style={{ ...S.head, marginBottom:12 }}>
REWE · EDEKA · NETTO · TRINKGUT & mehr ({prospekte.targeted.length})
</div>
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(200px, 1fr))', gap:12, marginBottom:20 }}>
{prospekte.targeted.map(b => <ProspektCard key={b.id} b={b} />)}
<div style={{ ...S.head, marginBottom:10 }}>AKTUELLE ANGEBOTE MIT PREIS ({offers.gridOffers.length})</div>
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(160px,1fr))', gap:12, marginBottom:24 }}>
{offers.gridOffers.map((o, i) => <GridOfferCard key={i} offer={o} />)}
</div>
</>
) : (
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:13, textAlign:'center', padding:'30px 0' }}>
Aktuell keine Prospekte von REWE, EDEKA, Netto oder Trinkgut gefunden.
)}
{/* BrochureBox: Prospektseiten mit KöPi */}
{offers.brochureOffers?.length > 0 && (
<>
<div style={{ ...S.head, marginBottom:10 }}>
KÖNIG PILSENER IN DIESEN PROSPEKTEN ({offers.brochureOffers.length})
</div>
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(180px,1fr))', gap:12 }}>
{offers.brochureOffers.map((o, i) => <BrochureOfferCard key={i} offer={o} />)}
</div>
</>
)}
{!offers.gridOffers?.length && !offers.brochureOffers?.length && (
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:13, textAlign:'center', padding:'40px 0' }}>
Aktuell keine König Pilsener Angebote gefunden.
</div>
)}
<div style={{ marginTop:20, textAlign:'center' }}>
<a href="https://www.kaufda.de/Angebote/Koenig-Pilsener" target="_blank" rel="noopener noreferrer"
style={{ color:GOLD, fontFamily:'monospace', fontSize:11 }}>
Alle Angebote auf kaufDA.de
</a>
</div>
</div>
)}
{/* Prospekte Tab */}
{!loading && tab === 'prospekte' && prospekte && (
<div>
{prospekte.targeted?.length > 0 && (
<>
<div style={{ ...S.head, marginBottom:10 }}>REWE · EDEKA · NETTO · TRINKGUT & mehr ({prospekte.targeted.length})</div>
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(180px,1fr))', gap:12, marginBottom:16 }}>
{prospekte.targeted.map(b => <ProspektCard key={b.id} b={b} />)}
</div>
</>
)}
{prospekte.others?.length > 0 && (
<>
<button onClick={() => setShowAll(v => !v)} style={{
...S.btn('#444444', true), marginBottom:12, fontSize:11,
}}>
{showAll ? '▾ Weitere ausblenden' : `▸ Alle weiteren Prospekte (${prospekte.others.length})`}
<button onClick={() => setShowAll(v => !v)} style={{ ...S.btn('#444444',true), marginBottom:10, fontSize:11 }}>
{showAll ? '▾ Weniger' : `▸ Weitere Prospekte (${prospekte.others.length})`}
</button>
{showAll && (
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(200px, 1fr))', gap:12 }}>
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(180px,1fr))', gap:12 }}>
{prospekte.others.map(b => <ProspektCard key={b.id} b={b} />)}
</div>
)}
</>
)}
<div style={{ color:'rgba(255,255,255,0.2)', fontFamily:'monospace', fontSize:10, marginTop:20, textAlign:'center' }}>
Klick auf einen Prospekt öffnet ihn auf kaufDA.de · Cache: 1 Stunde
</div>
</div>
)}
{/* Angebote */}
{!loading && tab === 'offers' && offers && (
<div>
{offers.offers?.length > 0 ? (
<>
<div style={{ ...S.head, marginBottom:12 }}>
AKTUELLE ANGEBOTE ({offers.offers.length})
</div>
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(200px, 1fr))', gap:12 }}>
{offers.offers.map((o, i) => <OfferCard key={i} offer={o} />)}
</div>
</>
) : (
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:13, textAlign:'center', padding:'30px 0', lineHeight:1.8 }}>
Aktuell keine einzelnen Produktangebote gefunden.<br/>
<span style={{ fontSize:11 }}>Die Angebote werden direkt im Prospekt angezeigt schau im Prospekte-Tab nach.</span>
<br/><br/>
<a href="https://www.kaufda.de/Angebote/Koenig-Pilsener" target="_blank" rel="noopener noreferrer"
style={{ color:MY_COLOR, fontFamily:'monospace', fontSize:12 }}>
Direkt auf kaufDA.de suchen
</a>
{!prospekte.targeted?.length && !prospekte.others?.length && (
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:13, textAlign:'center', padding:'40px 0' }}>
Keine Prospekte gefunden.
</div>
)}
<div style={{ color:'rgba(255,255,255,0.2)', fontFamily:'monospace', fontSize:10, marginTop:20, textAlign:'center' }}>
Quelle: kaufDA.de · Cache: 1 Stunde
</div>
</div>
)}
</div>