// ───────────────────────────────────────────────────────────────────────── // app.jsx — KreditEU shell, routing, theme, tweaks // ───────────────────────────────────────────────────────────────────────── const { useState: useStateApp, useEffect: useEffectApp, useMemo: useMemoApp } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "aurora", "mode": "dark", "tileLayout": "bento", "primaryHue": 80, "showTicker": true, "showAds": true }/*EDITMODE-END*/; const SUPPORTED_LANGS = ["pl", "en", "de", "fr", "cs", "hu", "nl"]; const SEO_ROUTES = new Set(["home", "mortgage", "cash", "banks", "markets", "programs", "articles", "article", "faq", "contact", "privacy", "terms", "cookies", "admin"]); function normalizeLang(lang) { const l = (lang || "").toLowerCase(); return SUPPORTED_LANGS.includes(l) ? l : null; } function normalizeCountry(code) { const c = (code || "").toLowerCase(); if (!c) return null; return ({ gb: "uk" }[c] || c); } function parseRouteFromUrl() { try { const q = new URLSearchParams(window.location.search); const page = q.get("page"); if (page === "article") { const id = Math.max(1, parseInt(q.get("id") || "1", 10) || 1); return { name: "article", id }; } if (page && SEO_ROUTES.has(page)) return page; return null; } catch { return null; } } function parseLangFromUrl() { try { const q = new URLSearchParams(window.location.search); return normalizeLang(q.get("lang")); } catch { return null; } } function buildStateUrl(origin, lang, routeName, routeData = null) { const q = new URLSearchParams(); if (lang) q.set("lang", lang); if (routeName && routeName !== "home") q.set("page", routeName); if (routeName === "article") q.set("id", String(routeData?.id || 1)); const qs = q.toString(); return `${origin}/${qs ? `?${qs}` : ""}`; } function setMetaTag(kind, key, value) { if (!value) return; let tag = document.head.querySelector(`meta[${kind}="${key}"]`); if (!tag) { tag = document.createElement("meta"); tag.setAttribute(kind, key); document.head.appendChild(tag); } tag.setAttribute("content", value); } function applySeo({ routeName, routeData, lang }) { const t = (key, fb) => window.t(lang, key, fb); const pageTitleByRoute = { home: t("hero.title1", "Credit Calculator"), mortgage: t("pages.mortgage.title", "Mortgage Calculator"), cash: t("pages.cash.title", "Cash Loan Calculator"), banks: t("pages.banks.title", "Compare Bank Rates"), markets: t("pages.markets.title", "Reference Indicators"), programs: t("pages.programs.title", "Government Programs"), articles: t("pages.articles.title", "Credit Guides"), article: t("pages.article.calcCard", "Credit Guide"), faq: t("pages.faq.title", "FAQ"), contact: t("pages.contact.title", "Contact"), privacy: t("pages.legal.privacyTitle", "Privacy Policy"), terms: t("pages.legal.termsTitle", "Terms of Service"), cookies: t("footer.cookies", "Cookie Policy"), }; const pageTitle = pageTitleByRoute[routeName] || "KreditEU"; const title = `${pageTitle} | KreditEU`; const description = t( "footer.brand", "Free credit calculator for 30 countries in Europe. Compare banks, rates, and loan options with AI insights." ); const isIndexable = routeName !== "admin"; const origin = window.location.origin; const canonical = buildStateUrl(origin, lang, routeName, routeData); document.title = title; setMetaTag("name", "description", description); setMetaTag("property", "og:title", title); setMetaTag("property", "og:description", description); setMetaTag("property", "og:type", "website"); setMetaTag("property", "og:url", canonical); setMetaTag("name", "twitter:card", "summary_large_image"); setMetaTag("name", "twitter:title", title); setMetaTag("name", "twitter:description", description); setMetaTag("name", "robots", isIndexable ? "index,follow,max-image-preview:large" : "noindex,nofollow"); let canonicalTag = document.getElementById("kr-canonical"); if (!canonicalTag) { canonicalTag = document.createElement("link"); canonicalTag.id = "kr-canonical"; canonicalTag.rel = "canonical"; document.head.appendChild(canonicalTag); } canonicalTag.href = canonical; document.head.querySelectorAll("link[data-kr-hreflang='1']").forEach(n => n.remove()); if (isIndexable) { SUPPORTED_LANGS.forEach((code) => { const link = document.createElement("link"); link.rel = "alternate"; link.hreflang = code; link.href = buildStateUrl(origin, code, routeName, routeData); link.setAttribute("data-kr-hreflang", "1"); document.head.appendChild(link); }); const xDefault = document.createElement("link"); xDefault.rel = "alternate"; xDefault.hreflang = "x-default"; xDefault.href = buildStateUrl(origin, "en", routeName, routeData); xDefault.setAttribute("data-kr-hreflang", "1"); document.head.appendChild(xDefault); } const ldGraph = [ { "@type": "Organization", "@id": "https://krediteu.com/#org", "name": "KreditEU", "url": "https://krediteu.com/", "contactPoint": [{ "@type": "ContactPoint", "contactType": "customer support", "email": "kontakt@krediteu.com" }], }, { "@type": "WebSite", "@id": "https://krediteu.com/#website", "url": "https://krediteu.com/", "name": "KreditEU", "inLanguage": lang, "potentialAction": { "@type": "SearchAction", "target": "https://krediteu.com/?lang=" + lang + "&page=articles&query={search_term_string}", "query-input": "required name=search_term_string" } }, { "@type": "WebPage", "@id": canonical + "#webpage", "url": canonical, "name": title, "description": description, "inLanguage": lang, "isPartOf": { "@id": "https://krediteu.com/#website" } } ]; if (routeName === "article" && window.ARTICLES?.length) { const article = window.ARTICLES.find((x) => x.id === (routeData?.id || 1)); if (article) { ldGraph.push({ "@type": "Article", "@id": `${canonical}#article`, "headline": article.title, "description": article.excerpt || description, "author": { "@type": "Organization", "name": "KreditEU Editorial" }, "publisher": { "@id": "https://krediteu.com/#org" }, "mainEntityOfPage": canonical, "inLanguage": lang }); } } const ld = { "@context": "https://schema.org", "@graph": ldGraph }; let ldNode = document.getElementById("kr-jsonld"); if (!ldNode) { ldNode = document.createElement("script"); ldNode.type = "application/ld+json"; ldNode.id = "kr-jsonld"; document.head.appendChild(ldNode); } ldNode.textContent = JSON.stringify(ld); } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); /* ───── Persistent app state (localStorage) ───── */ // Wstępny fallback: navigator.language jeśli pierwszy raz, potem auto-detect ulepsza const detectInitialLang = () => { const fromUrl = parseLangFromUrl(); if (fromUrl) return fromUrl; const stored = normalizeLang(readLS("kr_lang", null)); if (stored) return stored; const browserLang = (navigator.language || navigator.userLanguage || "pl").slice(0, 2).toLowerCase(); return normalizeLang(browserLang) || "pl"; }; const detectInitialCountry = (initialLang) => { const stored = normalizeCountry(readLS("kr_country", null)); if (stored) return stored; const langToCountry = { pl: "pl", de: "de", fr: "fr", cs: "cz", hu: "hu", nl: "nl", en: "uk" }; return langToCountry[initialLang] || "pl"; }; const [route, setRoute] = useStateApp(() => parseRouteFromUrl() || readLS("kr_route", "home")); const [lang, setLang] = useStateApp(detectInitialLang); const [country, setCountry] = useStateApp(() => detectInitialCountry(detectInitialLang())); const [autoDetected, setAutoDetected] = useStateApp(() => { const hasStoredLang = !!normalizeLang(readLS("kr_lang", null)); const hasLangInUrl = !!parseLangFromUrl(); return !hasStoredLang && !hasLangInUrl; }); const [tiles, setTiles] = useStateApp(() => readLS("kr_tiles", window.DEFAULT_TILES)); const [search, setSearch] = useStateApp(false); const [loading, setLoading] = useStateApp(true); const [visits, setVisits] = useStateApp(() => readLS("kr_visits", 47238)); /* ───── Auto-detect języka po IP (uzupełnienie, jeśli user jeszcze nie wybrał ręcznie) ───── */ useEffectApp(() => { // Tylko jeśli user jeszcze nie ustawił preferencji świadomie (autoDetected = pierwsza wizyta) if (!autoDetected) return; fetch("/api/geo.php", { credentials: "omit" }) .then(r => r.ok ? r.json() : null) .then(data => { if (!data) return; const geoLang = normalizeLang(data.language); if (geoLang) { setLang(geoLang); } if (data.country_code) { const normalizedCountry = normalizeCountry(data.country_code); if (window.COUNTRIES?.find(c => c.code === normalizedCountry)) { setCountry(normalizedCountry); } } }) .catch(() => {}); // backend padł — zostajemy z navigator.language }, []); // Gdy user ręcznie zmieni język — wyłącz auto-detect (zapamiętaj jego wybór na zawsze) const setLangManual = (newLang) => { const normalized = normalizeLang(newLang) || "pl"; setLang(normalized); setAutoDetected(false); writeLS("kr_auto_detected", false); }; const setCountryManual = (newCountry) => { const normalized = normalizeCountry(newCountry) || "pl"; setCountry(normalized); setAutoDetected(false); writeLS("kr_auto_detected", false); }; // Ustaw atrybut lang na dla SEO i screen readers useEffectApp(() => { if (lang) document.documentElement.setAttribute("lang", lang); }, [lang]); // Increment visit counter once per browser session useEffectApp(() => { if (!sessionStorage.getItem("kr_session_id")) { sessionStorage.setItem("kr_session_id", Date.now().toString()); const v = readLS("kr_visits", 47238) + 1; writeLS("kr_visits", v); setVisits(v); } // Slow live drift — random visits coming in to feel alive const id = setInterval(() => { setVisits(v => { const next = v + (Math.random() < 0.4 ? 1 : 0); if (next !== v) writeLS("kr_visits", next); return next; }); }, 8000); return () => clearInterval(id); }, []); useEffectApp(() => { writeLS("kr_route", route); }, [route]); useEffectApp(() => { writeLS("kr_country", country); }, [country]); useEffectApp(() => { writeLS("kr_lang", lang); }, [lang]); useEffectApp(() => { writeLS("kr_tiles", tiles); }, [tiles]); // Apply theme to useEffectApp(() => { document.documentElement.setAttribute("data-theme", t.theme); document.documentElement.setAttribute("data-mode", t.mode); }, [t.theme, t.mode]); // Skeleton on first paint useEffectApp(() => { const id = setTimeout(() => setLoading(false), 350); return () => clearTimeout(id); }, []); // Scroll to top on route change useEffectApp(() => { window.scrollTo({ top: 0, behavior: "instant" }); }, [JSON.stringify(route)]); useEffectApp(() => { const currentRouteName = typeof route === "string" ? route : route?.name || "home"; const currentRouteData = typeof route === "object" ? route : null; const safeLang = normalizeLang(lang) || "en"; const nextUrl = buildStateUrl(window.location.origin, safeLang, currentRouteName, currentRouteData); const nextRelative = nextUrl.replace(window.location.origin, ""); const currentRelative = `${window.location.pathname}${window.location.search}`; if (nextRelative !== currentRelative) { window.history.replaceState(null, "", nextRelative); } applySeo({ routeName: currentRouteName, routeData: currentRouteData, lang: safeLang }); if (typeof window.gtag === "function" && currentRouteName !== "admin") { window.gtag("event", "page_view", { page_title: document.title, page_location: window.location.href, page_path: `${window.location.pathname}${window.location.search}`, language: safeLang, }); } }, [route, lang]); // Cmd/Ctrl-K opens search useEffectApp(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); setSearch(true); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const navigate = (target) => { setRoute(target); }; const routeName = typeof route === "string" ? route : (route?.name || "home"); const routeData = typeof route === "object" ? route : null; return ( <>
setTweak("mode", m)} theme={t.theme} setTheme={(th) => setTweak("theme", th)} openSearch={() => setSearch(true)} /> {t.showTicker && }
{loading ? : ( <> {routeName === "home" && } {routeName === "mortgage" && } {routeName === "cash" && } {routeName === "banks" && } {routeName === "markets" && } {routeName === "programs" && } {routeName === "articles" && } {routeName === "article" && } {routeName === "faq" && } {routeName === "contact" && } {routeName === "privacy" && } {routeName === "terms" && } {routeName === "cookies" && } {routeName === "admin" && } )}