/* CITTA BUILDERS — shared React components used across all five pages. Loaded after the Cohere DS bundle; exports onto window.Site so each page's own babel script (separate scope) can use them. */ /* Single source of truth for footer social links — update these three URLs to go live. */ const SOCIAL_LINKS = { linkedin: 'https://www.linkedin.com/in/citta-builders-365b54424', instagram: 'https://www.instagram.com/cittabuilders/', facebook: 'https://www.facebook.com/cittabuilders360', }; const NAV_LINKS = [ { href: 'index.html', label: 'Home', i18n: 'nav.home' }, { href: 'about.html', label: 'About', i18n: 'nav.about' }, { href: 'services.html', label: 'Services', i18n: 'nav.services' }, { href: 'portfolio.html', label: 'Portfolio', i18n: 'nav.portfolio' }, { href: 'contact.html', label: 'Contact', i18n: 'nav.contact' }, ]; /* ============ Language switcher — instant, local, dictionary-based (shared/i18n.js). No network call, no external widget, no polling: choosing a language is a synchronous DOM text swap via CITTA_I18N, so there is no delay and nothing to get "stuck". Persisted in localStorage so the choice survives page navigation across the whole site. ============ */ const LANGUAGES = [ { code: 'en', label: 'English' }, { code: 'sw', label: 'Kiswahili' }, { code: 'fr', label: 'Français' }, { code: 'ar', label: 'العربية' }, { code: 'zh-CN', label: '中文' }, { code: 'pt', label: 'Português' }, ]; function getCurrentLang() { return (window.CITTA_I18N && window.CITTA_I18N.getLang()) || 'en'; } function LanguageSwitcher({ variant, onLight }) { const [open, setOpen] = React.useState(false); const [current, setCurrent] = React.useState('en'); React.useEffect(() => { setCurrent(getCurrentLang()); const onChange = (e) => setCurrent(e.detail.lang); document.addEventListener('citta-lang-changed', onChange); return () => document.removeEventListener('citta-lang-changed', onChange); }, []); const choose = (code) => { setOpen(false); if (window.CITTA_I18N) window.CITTA_I18N.setLang(code); setCurrent(code); }; return (
{open && (
setOpen(false)}> {LANGUAGES.map((l) => ( ))}
)}
); } /* ============ Device-mode switch — removed: real responsive breakpoints (mobile/desktop) now handle this automatically, so the manual Desktop/Mobile toggle is no longer needed. ============ */ function Nav({ active, alwaysLight = false }) { const [scrolled, setScrolled] = React.useState(alwaysLight); const [ready, setReady] = React.useState(false); const [hovered, setHovered] = React.useState(null); const [menuOpen, setMenuOpen] = React.useState(false); const [indicator, setIndicator] = React.useState({ left: 0, width: 0, ready: false }); const linksRef = React.useRef(null); const linkRefs = React.useRef({}); React.useEffect(() => { if (alwaysLight) return; const onScroll = () => setScrolled(window.scrollY > 40); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, [alwaysLight]); React.useEffect(() => { const t = setTimeout(() => setReady(true), 50); return () => clearTimeout(t); }, []); const measure = React.useCallback((label) => { const el = linkRefs.current[label]; const wrap = linksRef.current; if (!el || !wrap) return; const elRect = el.getBoundingClientRect(); const wrapRect = wrap.getBoundingClientRect(); setIndicator({ left: elRect.left - wrapRect.left, width: elRect.width, ready: true }); }, []); React.useEffect(() => { measure(hovered || active); const onResize = () => measure(hovered || active); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, [hovered, active, measure, scrolled]); const { Button } = window.CohereDesignSystem_4a3e6b; const onLight = scrolled || alwaysLight; const logoSrc = onLight ? 'images/logo-black.png' : 'images/logo-white.png'; return (
CITTA BUILDERS
{NAV_LINKS.map((l) => ( setMenuOpen(false)} data-i18n={l.i18n}>{l.label} ))}
setMenuOpen(false)} data-i18n="nav.cta">Schedule a Consultation
); } function Footer() { const ref = React.useRef(null); const [shown, setShown] = React.useState(false); const [subscribed, setSubscribed] = React.useState(false); const [subError, setSubError] = React.useState(null); const [subSending, setSubSending] = React.useState(false); React.useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver( (entries) => { entries.forEach((e) => { if (e.isIntersecting) { setShown(true); io.unobserve(el); } }); }, { threshold: 0.1 } ); io.observe(el); return () => io.disconnect(); }, []); return ( ); } /* Runs lucide.createIcons after mount — call once per page from the root component. */ function useLucide(deps = []) { React.useEffect(() => { if (window.lucide) window.lucide.createIcons(); }, deps); } function PageHero({ eyebrow, title, sub, surface = 'primary', children, tall = false, bgImage, i18nEyebrow, i18nTitle, i18nSub }) { return (
{bgImage ? (
) : (
)}
{eyebrow && {eyebrow}}

{title}

{sub &&

{sub}

} {children}
); } function SectionHead({ eyebrow, title, body, action, i18nEyebrow, i18nTitle, i18nBody }) { return (
{eyebrow && {eyebrow}}

{title}

{body &&

{body}

}
{action}
); } function ImgSlot({ ratio = '4-3', label = 'Image placeholder', src, alt, fit }) { if (src) { return (
{alt
); } return (
{label}
); } function StatGrid({ stats, onDark = false }) { return (
{stats.map((s) => (
{s.value}
{s.label}
))}
); } /* ============ LedgerStats — unique count-up scoreboard for the "Why Choose" band ============ */ function LedgerStat({ value, label, index, i18n, i18nValue }) { const ref = React.useRef(null); const [display, setDisplay] = React.useState(null); const match = /^(\d+)(.*)$/.exec(value); React.useEffect(() => { const el = ref.current; if (!el) return; if (!match) { setDisplay(value); return; } const target = parseInt(match[1], 10); const suffix = match[2]; const io = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; io.unobserve(el); const duration = 1300; const start = performance.now(); const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setDisplay(Math.round(eased * target) + suffix); if (t < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }); }, { threshold: 0.15, rootMargin: '0px 0px -10% 0px' }); io.observe(el); return () => io.disconnect(); }, [value]); return (
{display === null ? (match ? '0' + match[2] : value) : display}
{label}
); } function LedgerStats({ stats }) { return (
{stats.map((s, i) => )}
); } function Timeline({ items }) { return (
{items.map((it, i) => (
{it.index || String(i + 1).padStart(2, '0')}

{it.title}

{it.body}

))}
); } function ProcessGrid({ steps }) { const railRef = React.useRef(null); const [active, setActive] = React.useState(0); React.useEffect(() => { const rail = railRef.current; if (!rail) return; const cards = Array.from(rail.children); const io = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting && entry.intersectionRatio > 0.6) { setActive(cards.indexOf(entry.target)); } }); }, { root: rail, threshold: [0.6] } ); cards.forEach((c) => io.observe(c)); return () => io.disconnect(); }, [steps]); return (
{steps.map((s, i) => (
{String(i + 1).padStart(2, '0')}

{s.title}

{s.body}

))}
{steps.map((s, i) => )}
Swipe to see all {steps.length} steps
); } function Testimonials({ items }) { return (
{items.map((t, i) => (

“{t.quote}”

{t.name}{t.role}
))}
); } function IndustryChips({ items }) { const { Chip } = window.CohereDesignSystem_4a3e6b; return (
{items.map((i) => {i})}
); } function CtaBand({ eyebrow, title, sub, surface = 'green', i18nEyebrow, i18nTitle, i18nSub, i18nCta }) { const { Button } = window.CohereDesignSystem_4a3e6b; const ref = React.useRef(null); const [shown, setShown] = React.useState(false); React.useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver( (entries) => { entries.forEach((e) => { if (e.isIntersecting) { setShown(true); io.unobserve(el); } }); }, { threshold: 0.25 } ); io.observe(el); return () => io.disconnect(); }, []); return (
{eyebrow && ( {eyebrow} )}

{title}

{sub &&

{sub}

}
); } Object.assign(window, { Site: { Nav, Footer, useLucide, PageHero, SectionHead, ImgSlot, StatGrid, LedgerStats, Timeline, ProcessGrid, Testimonials, IndustryChips, CtaBand }, });