// Quick City — Shared React components const { useState, useEffect } = React; /* ───────── i18n hook ───────── */ function useT() { const [lang, setLang] = useState(() => (window.QCI18N ? window.QCI18N.getLang() : 'de')); useEffect(() => { const handler = (e) => setLang(e.detail.lang); window.addEventListener('qc-lang-change', handler); return () => window.removeEventListener('qc-lang-change', handler); }, []); // Translator function — falls back to provided default, then to the key const t = (key, fallback) => { if (!window.QCI18N) return fallback !== undefined ? fallback : key; const v = window.QCI18N.t(key); if (v === key && fallback !== undefined) return fallback; return v; }; return { t, lang, setLang: (l) => window.QCI18N && window.QCI18N.setLang(l) }; } function LangSwitcher() { const { lang, setLang } = useT(); const langs = ['de', 'en', 'tr']; return (
{langs.map(l => ( ))}
); } function getRoute() { // Clean URLs via History API: read the pathname (e.g. /lns/ausbau). let path = decodeURIComponent(location.pathname || '/'); // If served as /app.html (fallback), strip it. path = path.replace(/^\/?app\.html\/?/i, '/'); let query = location.search ? location.search.slice(1) : ''; // Legacy support: old #/lns/ausbau links still work. if ((path === '' || path === '/') && location.hash.length > 1) { const h = location.hash.slice(1); const qi = h.indexOf('?'); if (qi !== -1) { path = h.slice(0, qi); query = h.slice(qi + 1); } else { path = h; } } // Query embedded in path (e.g. /projekte?brand=lns) if (path.indexOf('?') !== -1) { const i = path.indexOf('?'); query = path.slice(i + 1); path = path.slice(0, i); } const params = new URLSearchParams(query); return { path: path.replace(/^\/+|\/+$/g, '') || '', params }; } // Navigate without full page reload (History API). function qcNavigate(href) { history.pushState({}, '', href); window.dispatchEvent(new Event('qc-route')); } // Global click interceptor — set up once. Turns internal into // SPA navigation, and handles in-page #anchors (#bewerben etc.) with a smooth scroll. if (typeof window !== 'undefined' && !window.__qcRouterInit) { window.__qcRouterInit = true; document.addEventListener('click', function (e) { if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; const a = e.target.closest ? e.target.closest('a') : null; if (!a) return; const href = a.getAttribute('href'); if (!href) return; if (a.target === '_blank' || a.hasAttribute('download')) return; // In-page anchor if (href.charAt(0) === '#') { if (href.length > 1) { const el = document.getElementById(href.slice(1)); if (el) { e.preventDefault(); const y = el.getBoundingClientRect().top + window.pageYOffset - 80; window.scrollTo({ top: y, behavior: 'smooth' }); } } return; } // Internal absolute path → SPA nav if (href.charAt(0) === '/' && !href.startsWith('//')) { e.preventDefault(); qcNavigate(href); } }); } function useRoute() { const [route, setRoute] = useState(getRoute()); useEffect(() => { const onChange = () => setRoute(getRoute()); window.addEventListener('popstate', onChange); window.addEventListener('qc-route', onChange); window.addEventListener('hashchange', onChange); return () => { window.removeEventListener('popstate', onChange); window.removeEventListener('qc-route', onChange); window.removeEventListener('hashchange', onChange); }; }, []); // Scroll to top whenever the path changes (after React has rendered the new page) useEffect(() => { window.scrollTo(0, 0); }, [route.path]); return route; } function detectBrand(route) { if (route.path.startsWith('lns')) return 'lns'; if (route.path.startsWith('ifm')) return 'ifm'; if (route.path === 'projekte' && route.params.get('brand') === 'lns') return 'lns'; return 'ifm'; } function Nav({ brand, currentHash }) { const { t } = useT(); const data = window.QC_DATA; const b = data.brands[brand]; const items = data.nav[brand]; const [mobileOpen, setMobileOpen] = useState(false); const [openSub, setOpenSub] = useState(null); // Lock body scroll when mobile menu is open useEffect(() => { document.body.style.overflow = mobileOpen ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [mobileOpen]); // Close mobile menu when route changes useEffect(() => { setMobileOpen(false); setOpenSub(null); }, [currentHash]); // Nav i18n keys mirror data.js order. We translate by label match. const navKeyByLabel = { 'Unternehmen': 'nav.company', 'Über uns': 'nav.about', 'Unsere Unternehmen': 'nav.companies', 'Unsere Technik': 'nav.tech', 'Unsere Werte': 'nav.values', 'Zertifikate': 'nav.certs', 'Leistungen': 'nav.services', 'Projekte': 'nav.projects', 'Team': 'nav.team', 'Karriere': 'nav.career', 'Kontakt & Standort': 'nav.contact', 'Übersicht': 'nav.career.overview', 'Stellenangebote': 'nav.career.jobs', 'Winterdienst': 'svc.winterdienst.title', 'Garten- & Landschaftsbau': 'svc.garten.title', 'Grünflächenpflege': 'svc.gruen.title', 'Baumdienste': 'svc.baum.title', 'Reinigungsservice': 'svc.reinigung.title', 'Tiefbau & Kabelverlegung': 'svc.tiefbau.title', 'Telekommunikation & Netzausbau': 'svc.ausbau.title', 'Straßen-, Wege- & Flächenbau': 'svc.bau.title', 'Baustellensicherung & Logistik': 'svc.baustellenpruefung.title', 'Störungsdienst & Wartung': 'svc.stoerungsdienst.title' }; const tnav = (label) => navKeyByLabel[label] ? t(navKeyByLabel[label], label) : label; return ( ); } function Footer({ brand }) { const { t } = useT(); const data = window.QC_DATA; const b = data.brands[brand]; const services = brand === 'ifm' ? data.ifmServices : data.lnsServices; const about = brand === 'ifm' ? data.nav.ifm[0].children : data.nav.lns[0].children; const navKeyByLabel = { 'Über uns': 'nav.about', 'Unsere Unternehmen': 'nav.companies', 'Unsere Technik': 'nav.tech', 'Unsere Werte': 'nav.values', 'Zertifikate': 'nav.certs', 'Team': 'nav.team', 'Karriere': 'nav.career', 'Kontakt & Standort': 'nav.contact' }; const tnav = (label) => navKeyByLabel[label] ? t(navKeyByLabel[label], label) : label; return ( ); } function Layout({ brand, children }) { const b = window.QC_DATA.brands[brand]; const route = useRoute(); // Apply brand colors via inline style on body useEffect(() => { document.body.style.setProperty('--accent', b.accent); document.body.style.setProperty('--switch-color', b.switch); // Swap favicon to match the active brand (red IFM / orange LNS logo) var icon = document.querySelector('link[rel="icon"]'); if (!icon) { icon = document.createElement('link'); icon.rel = 'icon'; document.head.appendChild(icon); } icon.type = 'image/jpeg'; icon.href = brand === 'lns' ? 'assets/logo-lns.jpg' : 'assets/logo-ifm.jpg'; }, [brand]); return ( <>