// ui.jsx — Shared UI primitives for ImmoBot // Icons, buttons, cards, badges, score visualization, bottom nav, top bar // ─── Icons (line, 24px viewbox, 1.7 stroke) ────────────────────────────── const I = { home: (p={}) => , search: (p={}) => , key: (p={}) => , bookmark: (p={}) => , bookmarkFill: (p={}) => , bell: (p={}) => , user: (p={}) => , heart: (p={}) => , heartFill: (p={}) => , archive: (p={}) => , share: (p={}) => , filter: (p={}) => , sort: (p={}) => , chevR: (p={}) => , chevL: (p={}) => , chevD: (p={}) => , x: (p={}) => , plus: (p={}) => , check: (p={}) => , spark: (p={}) => , brain: (p={}) => , pin: (p={}) => , bed: (p={}) => , ruler: (p={}) => , calendar: (p={}) => , euro: (p={}) => , refresh: (p={}) => , download: (p={}) => , trash: (p={}) => , more: (p={}) => , grid: (p={}) => , list: (p={}) => , swipe: (p={}) => , warn: (p={}) => , trend: (p={}) => , zap: (p={}) => , link: (p={}) => , message: (p={}) => , globe: (p={}) => , flame: (p={}) => , shield: (p={}) => , settings: (p={}) => , logout: (p={}) => , }; // ─── Platform badges ──────────────────────────────────────────────────── function PlatformBadge({ platform, size = 'sm' }) { // Stylized — we draw our own monograms; not the actual brand logos const fontSize = size === 'lg' ? 11 : 10; const pad = size === 'lg' ? '5px 10px' : '3px 8px'; if (platform === 'immoscout24') { return ( Scout24 ); } return ( Kleinanz. ); } // ─── Score Ring (semi-circular with KPIs) ─────────────────────────────── function ScoreRing({ score, size = 96, stroke = 8, showGrade = true }) { const r = (size - stroke) / 2; const c = 2 * Math.PI * r; const pct = score / 100; const color = score >= 80 ? '#2F8F5A' : score >= 60 ? '#C28428' : '#C8331C'; const grade = score >= 90 ? 'A+' : score >= 80 ? 'A' : score >= 70 ? 'B' : score >= 60 ? 'C' : score >= 50 ? 'D' : 'F'; return (
{score}
{showGrade && (
{grade} · /100
)}
); } // ─── Score Bar (horizontal, for compact spots) ────────────────────────── function ScoreBar({ score, label, note }) { const color = score >= 80 ? '#2F8F5A' : score >= 60 ? '#C28428' : '#C8331C'; return (
{label} {score}
{note &&
{note}
}
); } // ─── Photo Slider (horizontal swipeable image gallery) ───────────────── function PhotoSlider({ photos, height = 240, rounded = 0, onPhotoTap }) { const [idx, setIdx] = React.useState(0); const startX = React.useRef(null); const handleStart = (e) => { startX.current = (e.touches ? e.touches[0].clientX : e.clientX); }; React.useEffect(() => { setIdx(0); }, [JSON.stringify(photos)]); const handleEnd = (e) => { if (startX.current == null) return; const endX = (e.changedTouches ? e.changedTouches[0].clientX : e.clientX); const d = endX - startX.current; if (Math.abs(d) > 40) { if (d < 0 && idx < photos.length - 1) setIdx(idx + 1); if (d > 0 && idx > 0) setIdx(idx - 1); } startX.current = null; }; return (
{photos.map((src, i) => (
))}
{/* dots */}
{photos.map((_, i) => (
))}
{/* photo count */}
{idx + 1} / {photos.length}
); } // ─── Listing Card (used in lists, dashboard, results) ────────────────── function ListingCard({ listing, onTap, isFav, onFav, compact = false }) { const isKauf = listing.type === 'kauf'; const priceStr = isKauf ? fmtPrice(listing.price) : fmtPrice(listing.price) + ' /Mt.'; const aiScore = listing.aiScore; const normalizedPricePerSqm = Number(listing.pricePerSqm || 0); const pricePerSqm = isKauf && normalizedPricePerSqm ? normalizedPricePerSqm.toLocaleString('de-DE') + ' €/m²' : (isKauf && listing.area ? Math.round(listing.price / listing.area).toLocaleString('de-DE') + ' €/m²' : null); return (
{listing.provisionsfrei && ( PROVISIONSFREI )}
{aiScore !== null && aiScore !== undefined && (
= 80 ? '#2F8F5A' : aiScore >= 60 ? '#C28428' : '#C8331C', color: '#fff', fontSize: 12, fontWeight: 800, display: 'flex', alignItems: 'center', justifyContent: 'center', }}>{aiScore}
KI-Score
)}
{priceStr}
{pricePerSqm && (
{pricePerSqm}
)}
{listing.title}
{listing.district}, {listing.city}
{listing.area ? fmtArea(listing.area) : '—'} {listing.rooms || '—'} Zi. {listing.baujahr}
); } // ─── Compact Listing Row (for dashboard "best deals") ────────────────── function ListingRow({ listing, onTap, rank }) { return (
{rank != null && (
{rank}
)}
{listing.title}
{listing.district}, {listing.city} · {fmtArea(listing.area)} · {listing.rooms} Zi.
{fmtPriceShort(listing.price)}
{listing.aiScore && (
= 80 ? '#2F8F5A' : listing.aiScore >= 60 ? '#C28428' : '#C8331C', padding: '2px 7px', borderRadius: 999, }}>{listing.aiScore}
)}
); } // ─── Bottom Nav ───────────────────────────────────────────────────────── function BottomNav({ active, onTab, notifBadge = 0 }) { const items = [ { id: 'immobilien', label: 'Kaufen', icon: }, { id: 'mieten', label: 'Mieten', icon: }, { id: 'portfolio', label: 'Merkliste',icon: }, { id: 'notifications', label: 'Mitteil.', icon: , badge: notifBadge }, { id: 'profile', label: 'Profil', icon: }, ]; return (
{items.map(it => ( ))}
); } // ─── Top Bar / Header ─────────────────────────────────────────────────── function TopBar({ title, onBack, trailing, large = false, transparent = false, dark = false }) { return (
{onBack && ( )}
{!large && (
{title}
)}
{trailing}
{large && (

{title}

)}
); } // ─── Empty State ──────────────────────────────────────────────────────── function EmptyState({ icon, title, subtitle, cta }) { return (
{icon}

{title}

{subtitle && (

{subtitle}

)} {cta}
); } // ─── Sheet (modal sheet from bottom) ─────────────────────────────────── function Sheet({ open, onClose, children, title, height = '78%' }) { if (!open) return null; return (
{title && (

{title}

)}
{children}
); } // ─── Stat Tile ────────────────────────────────────────────────────────── function StatTile({ value, label, delta, accent = false, icon }) { return (
{label}
{icon &&
{icon}
}
{value}
{delta && (
{delta}
)}
); } Object.assign(window, { I, PlatformBadge, ScoreRing, ScoreBar, PhotoSlider, ListingCard, ListingRow, BottomNav, TopBar, EmptyState, Sheet, StatTile, });