// ───────────────────────────────────────────────────────────────────────── // components.jsx — shared UI building blocks // ───────────────────────────────────────────────────────────────────────── const { useState, useEffect, useRef, useMemo, useCallback } = React; /* ────────── Logo ────────── */ function KreditLogo({ size = 28 }) { return (
KreditEU europe · 2026
); } /* ────────── Sparkline ────────── */ function Sparkline({ data, width = 90, height = 28, stroke = "var(--accent)", fillOpacity = 0.18, showDot = true }) { if (!data || data.length === 0) return null; const min = Math.min(...data); const max = Math.max(...data); const range = max - min || 1; const step = width / (data.length - 1 || 1); const pts = data.map((v, i) => { const x = i * step; const y = height - ((v - min) / range) * (height - 4) - 2; return [x, y]; }); const d = pts.map((p, i) => (i === 0 ? `M ${p[0]} ${p[1]}` : `L ${p[0]} ${p[1]}`)).join(" "); const area = d + ` L ${width} ${height} L 0 ${height} Z`; const last = pts[pts.length - 1]; return ( {showDot && } ); } /* ────────── Animated number ────────── */ function AnimatedNumber({ value, digits = 0, duration = 600 }) { const [display, setDisplay] = useState(value); const prev = useRef(value); useEffect(() => { const from = prev.current; const to = value; if (from === to) return; const start = performance.now(); let raf; const tick = (t) => { const p = Math.min(1, (t - start) / duration); const eased = 1 - Math.pow(1 - p, 3); setDisplay(from + (to - from) * eased); if (p < 1) raf = requestAnimationFrame(tick); else prev.current = to; }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [value, duration]); return {display.toLocaleString("pl-PL", { minimumFractionDigits: digits, maximumFractionDigits: digits })}; } /* ────────── Live ticker ────────── */ function Ticker() { const items = [...window.FX, ...window.REFERENCE_RATES.slice(0, 3).map(r => ({ pair: r.name, value: r.value, d24: r.change, source: "—", isRate: true }))]; const all = [...items, ...items]; // duplicate for seamless loop return (
{all.map((it, i) => { const up = it.d24 > 0; const flat = Math.abs(it.d24) < 0.005; return (
{it.pair} {it.isRate ? it.value.toFixed(2) + "%" : it.value.toFixed(4)} {flat ? "→" : (up ? "▲" : "▼")} {Math.abs(it.d24).toFixed(2)}%
); })}
); } /* ────────── Header ────────── */ function Header({ route, navigate, lang, setLang, country, setCountry, mode, setMode, theme, setTheme, openSearch }) { const [navOpen, setNavOpen] = useState(false); const [langOpen, setLangOpen] = useState(false); const [countryOpen, setCountryOpen] = useState(false); const t = window.I18N[lang]?.nav || window.I18N.pl.nav; const cta = window.I18N[lang]?.cta || window.I18N.pl.cta; const current = window.COUNTRIES.find(c => c.code === country) || window.COUNTRIES[0]; const items = [ { key: "home", label: t.home }, { key: "banks", label: t.banks }, { key: "markets", label: t.markets }, { key: "programs", label: t.programs }, { key: "articles", label: t.articles }, { key: "faq", label: t.faq }, { key: "contact", label: t.contact }, ]; useEffect(() => { const close = () => { setLangOpen(false); setCountryOpen(false); }; if (langOpen || countryOpen) { document.addEventListener("click", close); return () => document.removeEventListener("click", close); } }, [langOpen, countryOpen]); return (
{/* Desktop nav */} {/* Right controls */}
{/* Country */}
{countryOpen && (
{window.t(lang, "common.creditCountry", "Credit Country")}
{window.COUNTRIES.map(c => ( ))}
)}
{/* Lang */}
{langOpen && (
{window.LANGS.map(l => ( ))}
)}
{/* Mode toggle */} {/* CTA */} {/* Mobile nav burger */}
{/* Mobile nav drawer */} {navOpen && (
{items.map(it => ( ))}
)}
); } function iconBtn() { return { width: 36, height: 36, display: "inline-flex", alignItems: "center", justifyContent: "center", background: "var(--surface)", border: "1px solid var(--border-soft)", borderRadius: 10, color: "var(--text-2)", cursor: "pointer", transition: "all 0.16s var(--ease)", }; } function chipBtn() { return { display: "inline-flex", alignItems: "center", gap: 6, padding: "8px 10px", background: "var(--surface)", border: "1px solid var(--border-soft)", borderRadius: 10, color: "var(--text)", fontSize: 12, fontWeight: 600, cursor: "pointer", transition: "all 0.16s var(--ease)", }; } function dropdown(width) { return { position: "absolute", top: "calc(100% + 8px)", right: 0, width, background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 12, boxShadow: "var(--shadow-lg)", overflow: "hidden", zIndex: 60, }; } function rowBtn() { return { display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", background: "transparent", border: "none", color: "var(--text)", fontSize: 13, fontWeight: 500, cursor: "pointer", borderRadius: 8, transition: "background 0.14s", textAlign: "left", }; } /* ────────── Footer ────────── */ function Footer({ lang, navigate }) { const t = window.I18N[lang]?.footer || window.I18N.pl.footer; const nav = window.I18N[lang]?.nav || window.I18N.pl.nav; return ( ); } function FooterCol({ title, items, navigate }) { const currentLang = (document.documentElement.lang || "pl").slice(0, 2).toLowerCase(); return (

{title}

{items.map((it, i) => ( { e.preventDefault(); if (it.onClick) it.onClick(); else navigate(it.route); }} style={{ background: "transparent", border: "none", padding: 0, textDecoration: "none", color: "var(--text-2)", textAlign: "left", fontSize: 13, fontWeight: 500, cursor: "pointer", transition: "color 0.14s", }} onMouseEnter={e => e.currentTarget.style.color = "var(--accent)"} onMouseLeave={e => e.currentTarget.style.color = "var(--text-2)"}> {it.label} ))}
); } /* ────────── Ad Slot ────────── */ function AdSlot({ size = "leaderboard", label, hint }) { const sizes = { leaderboard: { width: "100%", height: 90, max: 728 }, billboard: { width: "100%", height: 250, max: 970 }, banner: { width: "100%", height: 180, max: 1200 }, rect: { width: "100%", height: 250, max: 300 }, skyscraper: { width: 160, height: 600 }, mobile: { width: "100%", height: 50, max: 320 }, in_grid: { width: "100%", height: "100%", min: 200 }, sidebar: { width: "100%", height: 600 }, }; const s = sizes[size] || sizes.leaderboard; return (
Reklama
{label || "AdSense"}
{hint || size}
); } /* ────────── Breadcrumbs ────────── */ function Breadcrumbs({ items, navigate }) { const currentLang = (document.documentElement.lang || "pl").slice(0, 2).toLowerCase(); return ( ); } function buildInternalHref(route, lang = "pl") { const safeLang = (lang || "pl").slice(0, 2).toLowerCase(); if (!route) return `/?lang=${safeLang}`; if (typeof route === "object") { const name = route.name || "home"; if (name === "article") return `/?lang=${safeLang}&page=article&id=${route.id || 1}`; return name === "home" ? `/?lang=${safeLang}` : `/?lang=${safeLang}&page=${name}`; } return route === "home" ? `/?lang=${safeLang}` : `/?lang=${safeLang}&page=${route}`; } function InternalLinksCard({ title, links = [], navigate, lang = "pl" }) { return (

{title}

{links.map((item, idx) => ( { e.preventDefault(); navigate(item.route); }} className="chip" style={{ textDecoration: "none", cursor: "pointer" }} > {item.label} ))}
); } /* ────────── Page header ────────── */ function PageHeader({ kicker, title, sub, actions }) { return (
{kicker &&
{kicker}
}

{title}

{sub &&

{sub}

}
{actions}
); } /* ────────── Search modal ────────── */ function SearchModal({ open, onClose, navigate, lang = "pl" }) { const [q, setQ] = useState(""); const inputRef = useRef(null); const T = (k, fb) => window.t(lang, `common.${k}`, fb); const nav = window.I18N[lang]?.nav || window.I18N.pl.nav; useEffect(() => { if (open) setTimeout(() => inputRef.current?.focus(), 50); const onKey = (e) => { if (e.key === "Escape") onClose(); }; if (open) window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); if (!open) return null; const ql = q.toLowerCase().trim(); const articleHits = ql ? window.ARTICLES.filter(a => a.title.toLowerCase().includes(ql) || a.excerpt.toLowerCase().includes(ql)).slice(0, 5) : []; const bankHits = ql ? window.BANKS.filter(b => b.name.toLowerCase().includes(ql)).slice(0, 4) : []; const navHits = ql ? [ ["home", nav.home], ["banks", nav.banks], ["markets", nav.markets], ["programs", nav.programs], ["articles", nav.articles], ["faq", nav.faq], ["contact", nav.contact], ["admin", window.t(lang, "footer.admin", "Admin")] ].filter(([_, l]) => l.toLowerCase().includes(ql)) : []; return (
e.stopPropagation()} style={{ width: "min(640px, 100%)", background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 16, boxShadow: "var(--shadow-lg)", overflow: "hidden", }}>
setQ(e.target.value)} placeholder={T("searchPlaceholder", "Search articles, banks, tools...")} style={{ flex: 1, background: "none", border: "none", outline: "none", color: "var(--text)", fontSize: 16, fontFamily: "inherit" }} />
{!ql && (
{T("startTyping", "Start typing to search articles, banks and sections.")}
)} {ql && navHits.length > 0 && ( {navHits.map(([r, l]) => ( ))} )} {ql && articleHits.length > 0 && ( {articleHits.map(a => ( ))} )} {ql && bankHits.length > 0 && ( {bankHits.map(b => { const c = window.COUNTRIES.find(c => c.code === b.country); return ( ); })} )} {ql && navHits.length + articleHits.length + bankHits.length === 0 && (
{T("noResults", "No results for")} "{q}"
)}
); } function SearchGroup({ title, children }) { return (
{title}
{children}
); } /* ────────── Icons ────────── */ function SearchIcon({ size = 16 }) { return ; } function SunIcon({ size = 16 }) { return ; } function MoonIcon({ size = 16 }) { return ; } function ChevronDown({ size = 14 }) { return ; } function CheckIcon({ size = 14 }) { return ; } function BurgerIcon({ open, size = 18 }) { return ( ); } function ArrowRight({ size = 14 }) { return ; } /* ────────── Progress bar ────────── */ function ProgressBar({ value, label }) { return (
{label &&
{label} {Math.round(value)}%
}
); } /* ────────── Trend pill ────────── */ function TrendPill({ value, suffix = "%" }) { const up = value > 0; const flat = Math.abs(value) < 0.005; return ( {flat ? "→" : (up ? "▲" : "▼")} {Math.abs(value).toFixed(2)}{suffix} ); } /* ────────── useInView (one-shot intersection observer) ────────── */ function useInView(threshold = 0.2) { const ref = useRef(null); const [inView, setInView] = useState(false); useEffect(() => { if (!ref.current || inView) return; const obs = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setInView(true); obs.disconnect(); } }, { threshold } ); obs.observe(ref.current); return () => obs.disconnect(); }, [inView, threshold]); return [ref, inView]; } /* ────────── CountUp (animates 0 → target when in view) ────────── */ function CountUp({ to, duration = 1800, digits = 0, suffix = "", prefix = "" }) { const [ref, inView] = useInView(0.3); const [val, setVal] = useState(0); useEffect(() => { if (!inView) return; const start = performance.now(); let raf; const tick = (t) => { const p = Math.min(1, (t - start) / duration); const eased = 1 - Math.pow(1 - p, 4); setVal(to * eased); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [inView, to, duration]); return ( {prefix}{val.toLocaleString("pl-PL", { minimumFractionDigits: digits, maximumFractionDigits: digits })}{suffix} ); } /* ────────── ImpactStrip — animated stats above footer ────────── */ function ImpactStrip({ visits, lang = "pl" }) { const T = (k, fb) => window.t(lang, `impact.${k}`, fb); const flagsRow = [...window.COUNTRIES, ...window.COUNTRIES]; // duplicate for seamless loop const chips = window.t(lang, "impact.chips", []); return (
{/* Decorative blobs */}
{T("kicker", "KreditEU Snapshot")}

{T("title1", "Already")} {T("title2", "people")}
{T("title3", "have calculated their loan with us.")}

{T("sub", "Free, no sign-up required, with live-updated numbers.")}

{/* Big stats */}
} sub={T("subCountries", "Poland, Germany, France +27 others")} /> } sub={T("subBanks", "Updated weekly")} /> } sub={T("subLanguages", "PL · EN · DE · FR · CZ · HU · NL")} /> } sub={T("subCalculations", "Completed in calculator")} /> } sub={T("subPrograms", "Across 7 European countries")} /> } sub={T("subVisits", "Since launch")} featured />
{/* Moving flags strip */}
{flagsRow.map((c, i) => (
{c.flag}
{c.name}
{c.currency} · {c.rate.toFixed(2)}%
))}
{/* Mini-stats row */}
{(Array.isArray(chips) ? chips : ["🇪🇺 30 European Countries", "💯 100% Free", "🚫 No Signup", "🤖 AI Analysis", "⚡ Live WIBOR & EURIBOR", "📱 Responsive", "🔄 Weekly Updates"]).map((label, i) => ( {label} ))}
); } function ImpactCard({ kicker, value, sub, featured }) { return (
{kicker}
{value}
{sub}
); } /* ────────── Visitor counter chip (small, in header area) ────────── */ function VisitorChip({ visits, lang = "pl" }) { return ( {window.t(lang, "visitor.already", "Already")} {visits.toLocaleString("pl-PL")} {window.t(lang, "visitor.visits", "visits")} ); } Object.assign(window, { KreditLogo, Sparkline, AnimatedNumber, Ticker, Header, Footer, AdSlot, Breadcrumbs, PageHeader, SearchModal, ProgressBar, TrendPill, InternalLinksCard, buildInternalHref, SearchIcon, SunIcon, MoonIcon, ChevronDown, CheckIcon, BurgerIcon, ArrowRight, CountUp, useInView, ImpactStrip, VisitorChip, });