Files
dickendock/frontend/src/tools/koepi.jsx

285 lines
12 KiB
JavaScript

import { useState, useEffect } from 'react';
import { api, S } from '../lib.js';
const GOLD = '#f59e0b';
function getMyRole() {
try { return JSON.parse(atob(localStorage.getItem('sk_token').split('.')[1])).role; } catch { return null; }
}
function publisherColor(name) {
if (!name) return '#555';
const n = name.toLowerCase();
if (n.includes('rewe')) return '#e2001a';
if (n.includes('edeka') || 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('kaufland')) return '#e2001a';
return '#888';
}
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; }
}
// ── Angebots-Karte (marktguru) ────────────────────────────────────────────────
function OfferCard({ offer }) {
const color = publisherColor(offer.retailer);
return (
<div style={{
...S.card, padding:0, overflow:'hidden',
border:'1px solid rgba(255,255,255,0.1)',
display:'flex', flexDirection:'column',
}}>
{/* Produktbild */}
{offer.image && (
<div style={{ height:160, background:'#f5f5f5', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, overflow:'hidden', position:'relative' }}>
<img
src={`/api/tools/koepi/img?url=${encodeURIComponent(offer.image)}`}
alt="König Pilsener"
style={{ maxHeight:'100%', maxWidth:'100%', objectFit:'contain' }}
onError={e => { e.target.parentElement.style.display='none'; }}
/>
{offer.oldPrice && (
<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>
)}
<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`,
borderRadius:4, padding:'2px 8px', marginBottom:3,
color, fontFamily:'monospace', fontSize:10, fontWeight:700,
}}>{offer.retailer}</div>
{offer.address && (
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9 }}>
📍 {offer.address}
</div>
)}
</div>
{/* König Pilsener */}
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:13, fontWeight:600, lineHeight:1.3 }}>
König Pilsener
</div>
{/* Flaschengröße/Beschreibung */}
{offer.description && (
<div style={{ color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:10, lineHeight:1.5 }}>
{offer.description}
</div>
)}
{/* Preis */}
<div style={{ display:'flex', alignItems:'baseline', gap:8, marginTop:4 }}>
<span style={{ color:GOLD, fontFamily:'Space Mono,monospace', fontSize:20, fontWeight:700 }}>
{offer.price}
</span>
{offer.oldPrice && (
<span style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:12, textDecoration:'line-through' }}>
{offer.oldPrice}
</span>
)}
</div>
{/* Gültigkeit */}
<div style={{ marginTop:'auto', paddingTop:6, borderTop:'1px solid rgba(255,255,255,0.06)', display:'flex', flexDirection:'column', gap:3 }}>
{offer.validity && (
<div style={{ color:'#f59e0b', fontFamily:'monospace', fontSize:10 }}>
{offer.validity}
</div>
)}
{offer.dateRange && (
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:10 }}>
📅 Gültig: {offer.dateRange}
</div>
)}
</div>
</div>
</div>
);
}
// ── Prospekt-Karte (kaufda) ───────────────────────────────────────────────────
function ProspektCard({ b }) {
const color = publisherColor(b.publisher);
const isNew = b.badges?.includes('new');
const expiring = b.badges?.includes('expiring_soon');
return (
<a href={b.url} target="_blank" rel="noopener noreferrer" style={{ textDecoration:'none' }}>
<div style={{
...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)',
}}>
<div style={{ position:'relative' }}>
{b.image && (
<img src={`/api/tools/koepi/img?url=${encodeURIComponent(b.image)}`}
alt={b.title} 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 }}>
{isNew && <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`,
borderRadius:4, padding:'2px 8px', marginBottom:6, color, fontFamily:'monospace', fontSize:10, fontWeight:700 }}>
{b.publisher}
</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 }}>
📍 {b.street || b.store}
</div>
{(b.validFrom || b.validTo) && (
<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>
</div>
</a>
);
}
// ── Hauptkomponente ───────────────────────────────────────────────────────────
export default function Koepi({ toast }) {
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 loadOffers = async (force=false) => {
if (offers && !force) return;
setLoading(true);
try { setOffers(await api('/tools/koepi/offers')); }
catch(e) { toast?.(e.message||'Fehler','error'); }
finally { setLoading(false); }
};
const loadProspekte = async (force=false) => {
if (prospekte && !force) 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);
toast('Cache geleert');
setTimeout(() => { if (tab==='angebote') loadOffers(true); else loadProspekte(true); }, 100);
} catch(e) { toast?.(e.message,'error'); }
};
return (
<div style={{ maxWidth:700, margin:'0 auto' }}>
{/* Header */}
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:20 }}>
<h2 style={{ margin:0, fontSize:15, fontFamily:'monospace', color:'rgba(255,255,255,0.55)', letterSpacing:2, fontWeight:400 }}>
🍺 KÖPI König Pilsener
</h2>
<div style={{ flex:1 }}/>
{isAdmin && (
<button onClick={clearCache} style={{ ...S.btn('#666666',true), fontSize:10 }}>🗑 Cache leeren</button>
)}
</div>
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:10, marginBottom:14 }}>
Raum Duisburg 47259 · EDEKA · REWE · Netto · Kaufland · Penny · Trinkgut · Cache 1h
</div>
{/* Tabs */}
<div style={{ display:'flex', gap:8, marginBottom:20 }}>
{[['angebote','🍺 Angebote'],['prospekte','📋 Prospekte']].map(([t,l]) => (
<button key={t} onClick={()=>setTab(t)} style={{ ...S.btn(tab===t?GOLD:'#444444',true), fontSize:12, padding:'6px 16px' }}>{l}</button>
))}
</div>
{/* Laden */}
{loading && (
<div style={{ color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:13, textAlign:'center', padding:'40px 0' }}>
Lade (kann bis zu 20 Sekunden dauern)
</div>
)}
{/* Angebote */}
{!loading && tab==='angebote' && offers && (
<div>
{offers.offers?.length > 0 ? (
<>
<div style={{ ...S.head, marginBottom:12 }}>
{offers.offers.length} AKTUELLE ANGEBOTE · Quelle: marktguru.de
</div>
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(180px,1fr))', gap:14 }}>
{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:'40px 0' }}>
Aktuell keine König Pilsener Angebote bei deinen Märkten gefunden.
</div>
)}
<div style={{ marginTop:20, textAlign:'center' }}>
<a href="https://www.marktguru.de/search/k%C3%B6nig%20pilsener?zipCode=47259" target="_blank" rel="noopener noreferrer"
style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:10 }}>
Alle Angebote auf marktguru.de
</a>
</div>
</div>
)}
{/* Prospekte */}
{!loading && tab==='prospekte' && prospekte && (
<div>
{prospekte.targeted?.length > 0 && (
<>
<div style={{ ...S.head, marginBottom:12 }}>DEINE MÄRKTE ({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:10, fontSize:11 }}>
{showAll ? '▾ Weniger' : `▸ Weitere Prospekte (${prospekte.others.length})`}
</button>
{showAll && (
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(180px,1fr))', gap:12 }}>
{prospekte.others.map(b=><ProspektCard key={b.id} b={b}/>)}
</div>
)}
</>
)}
{!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>
)}
</div>
);
}