fix: KöPi - OfferCard Layout, Bilder sequenziell, Badge Position

This commit is contained in:
2026-07-09 10:03:46 +02:00
parent 66919b509a
commit 51aa706856
2 changed files with 66 additions and 82 deletions

View File

@@ -93,7 +93,8 @@ async function scrapeMarktguru() {
const cookies = await page.cookies('https://cdn.marktguru.de'); const cookies = await page.cookies('https://cdn.marktguru.de');
const cookieStr = cookies.map(c => `${c.name}=${c.value}`).join('; '); const cookieStr = cookies.map(c => `${c.name}=${c.value}`).join('; ');
await Promise.all(offers.map(offer => new Promise(resolve => { // Sequenziell laden damit Cookie-Session stabil bleibt
for (const offer of offers) { await new Promise(resolve => {
if (!offer.image || !offer.image.startsWith('https://')) return resolve(); if (!offer.image || !offer.image.startsWith('https://')) return resolve();
const req = require('https').get(offer.image, { const req = require('https').get(offer.image, {
headers: { headers: {
@@ -114,7 +115,7 @@ async function scrapeMarktguru() {
}); });
req.on('error', () => { offer.image = null; resolve(); }); req.on('error', () => { offer.image = null; resolve(); });
req.setTimeout(8000, () => { req.destroy(); offer.image = null; resolve(); }); req.setTimeout(8000, () => { req.destroy(); offer.image = null; resolve(); });
}))); }); }
// Filtern auf Ziel-Händler + nur König Pilsener // Filtern auf Ziel-Händler + nur König Pilsener
const filtered = offers.filter(o => { const filtered = offers.filter(o => {

View File

@@ -30,59 +30,51 @@ function OfferCard({ offer }) {
const color = publisherColor(offer.retailer); const color = publisherColor(offer.retailer);
return ( return (
<div style={{ <div style={{
...S.card, padding:0, overflow:'hidden', ...S.card, padding:'12px 14px',
border:'1px solid rgba(255,255,255,0.1)', border:'1px solid rgba(255,255,255,0.1)',
display:'flex', flexDirection:'column', display:'flex', flexDirection:'column', gap:8,
}}> }}>
{/* Produktbild */} {/* Oben: Bild links + Info rechts */}
{offer.image && ( <div style={{ display:'flex', gap:10, alignItems:'flex-start' }}>
<div style={{ height:160, background:'#f5f5f5', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, overflow:'hidden', position:'relative' }}> {/* Bild */}
<img <div style={{ width:80, height:80, flexShrink:0, background:'#f5f5f5', borderRadius:6, overflow:'hidden', display:'flex', alignItems:'center', justifyContent:'center' }}>
src={offer.image} {offer.image
alt="König Pilsener" ? <img src={offer.image} alt="König Pilsener"
style={{ maxHeight:'100%', maxWidth:'100%', objectFit:'contain' }} style={{ width:'100%', height:'100%', objectFit:'contain' }}
onError={e => { e.target.parentElement.style.display='none'; }} onError={e => { e.target.parentElement.style.background='transparent'; e.target.style.display='none'; }}
/> />
{offer.oldPrice && ( : <span style={{ fontSize:28 }}>🍺</span>
<div style={{ position:'absolute', top:6, left:6, background:'#e2001a', color:'#fff', }
borderRadius:4, padding:'2px 6px', fontSize:9, fontFamily:'monospace', fontWeight:700 }}>
-29%
</div> </div>
)} {/* Info */}
</div> <div style={{ flex:1, minWidth:0 }}>
)} <div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:3, flexWrap:'wrap' }}>
<span style={{
<div style={{ padding:'12px 14px', flex:1, display:'flex', flexDirection:'column', gap:5 }}>
{/* Händler Badge + Adresse */}
<div>
<div style={{
display:'inline-block',
background:`${color}22`, border:`1px solid ${color}55`, background:`${color}22`, border:`1px solid ${color}55`,
borderRadius:4, padding:'2px 8px', marginBottom:3, borderRadius:4, padding:'2px 7px',
color, fontFamily:'monospace', fontSize:10, fontWeight:700, color, fontFamily:'monospace', fontSize:10, fontWeight:700,
}}>{offer.retailer}</div> }}>{offer.retailer}</span>
{offer.oldPrice && <span style={{ background:'#e2001a', color:'#fff', borderRadius:4, padding:'2px 6px', fontSize:9, fontFamily:'monospace', fontWeight:700 }}>-29%</span>}
</div>
{offer.address && ( {offer.address && (
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9 }}> <div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9, marginBottom:3 }}>
📍 {offer.address} 📍 {offer.address}
</div> </div>
)} )}
</div> <div style={{ color:'#fff', fontFamily:'monospace', fontSize:13, fontWeight:600 }}>
{/* König Pilsener */}
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:13, fontWeight:600, lineHeight:1.3 }}>
König Pilsener König Pilsener
</div> </div>
{/* Flaschengröße/Beschreibung */}
{offer.description && ( {offer.description && (
<div style={{ color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:10, lineHeight:1.5 }}> <div style={{ color:'rgba(255,255,255,0.45)', fontFamily:'monospace', fontSize:9, marginTop:2, lineHeight:1.4 }}>
{offer.description} {offer.description}
</div> </div>
)} )}
</div>
</div>
{/* Preis */} {/* Preis */}
<div style={{ display:'flex', alignItems:'baseline', gap:8, marginTop:4 }}> <div style={{ display:'flex', alignItems:'baseline', gap:8 }}>
<span style={{ color:GOLD, fontFamily:'Space Mono,monospace', fontSize:20, fontWeight:700 }}> <span style={{ color:GOLD, fontFamily:'Space Mono,monospace', fontSize:22, fontWeight:700 }}>
{offer.price} {offer.price}
</span> </span>
{offer.oldPrice && ( {offer.oldPrice && (
@@ -93,20 +85,15 @@ function OfferCard({ offer }) {
</div> </div>
{/* Gültigkeit */} {/* Gültigkeit */}
<div style={{ marginTop:'auto', paddingTop:6, borderTop:'1px solid rgba(255,255,255,0.06)', display:'flex', flexDirection:'column', gap:3 }}> <div style={{ borderTop:'1px solid rgba(255,255,255,0.06)', paddingTop:6, display:'flex', gap:12, flexWrap:'wrap' }}>
{offer.validity && ( {offer.validity && (
<div style={{ color:'#f59e0b', fontFamily:'monospace', fontSize:10 }}> <span style={{ color:'#f59e0b', fontFamily:'monospace', fontSize:10 }}> {offer.validity}</span>
{offer.validity}
</div>
)} )}
{offer.dateRange && ( {offer.dateRange && (
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:10 }}> <span style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:10 }}>📅 {offer.dateRange}</span>
📅 Gültig: {offer.dateRange}
</div>
)} )}
</div> </div>
</div> </div>
</div>
); );
} }
@@ -121,24 +108,20 @@ function ProspektCard({ b }) {
...S.card, padding:0, overflow:'hidden', cursor:'pointer', ...S.card, padding:0, overflow:'hidden', cursor:'pointer',
border: expiring ? '1px solid rgba(239,68,68,0.4)' : '1px solid rgba(255,255,255,0.08)', border: expiring ? '1px solid rgba(239,68,68,0.4)' : '1px solid rgba(255,255,255,0.08)',
}}> }}>
<div style={{ position:'relative' }}>
{b.image && ( {b.image && (
<img src={`/api/tools/koepi/img?url=${encodeURIComponent(b.image)}`} <img src={`/api/tools/koepi/img?url=${encodeURIComponent(b.image)}`}
alt={b.title} style={{ width:'100%', height:110, objectFit:'cover', display:'block' }} alt={b.title} style={{ width:'100%', height:110, objectFit:'cover', display:'block' }}
onError={e => { e.target.style.display='none'; }} onError={e => { e.target.style.display='none'; }}
/> />
)} )}
{expiring && (
<div style={{ position:'absolute', top:6, left:6 }}>
<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={{ padding:'10px 12px' }}>
<div style={{ display:'inline-block', background:`${color}22`, border:`1px solid ${color}44`, <div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:4, flexWrap:'wrap' }}>
borderRadius:4, padding:'2px 8px', marginBottom:6, color, fontFamily:'monospace', fontSize:10, fontWeight:700 }}> <div style={{ background:`${color}22`, border:`1px solid ${color}44`,
borderRadius:4, padding:'2px 8px', color, fontFamily:'monospace', fontSize:10, fontWeight:700 }}>
{b.publisher} {b.publisher}
</div> </div>
{expiring && <span style={{ background:'#ef4444', color:'#fff', borderRadius:4, padding:'2px 6px', fontSize:9, fontFamily:'monospace', fontWeight:700 }}>ENDET BALD</span>}
</div>
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:11, marginBottom:3 }}>{b.title}</div> <div style={{ color:'#fff', fontFamily:'monospace', fontSize:11, marginBottom:3 }}>{b.title}</div>
<div style={{ color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:10, marginBottom:2 }}> <div style={{ color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:10, marginBottom:2 }}>
📍 {b.street || b.store} 📍 {b.street || b.store}