// Shared primitives — used across all pages // Exposes: Logo, Brand, Topbar, MenuOverlay, FloatCta, Footer, Ph (placeholder), useReveal, useI18n, useTheme, Tweaks const { useState, useEffect, useRef, useContext, createContext, useCallback, useMemo } = React; /* ---------------- i18n ---------------- */ const I18nContext = createContext(null); function I18nProvider({ children }) { const [lang, setLang] = useState(() => { try { return localStorage.getItem('kdc-lang') || 'en'; } catch { return 'en'; } }); useEffect(() => { try { localStorage.setItem('kdc-lang', lang); } catch {} document.documentElement.lang = lang; }, [lang]); const value = useMemo(() => ({ lang, setLang, t: window.KDC_I18N[lang], }), [lang]); return React.createElement(I18nContext.Provider, { value }, children); } function useI18n() { return useContext(I18nContext); } /* ---------------- theme (tweaks) ---------------- */ const ThemeContext = createContext(null); function ThemeProvider({ children }) { const [theme, setTheme] = useState(() => { try { return localStorage.getItem('kdc-theme') || 'light'; } catch { return 'light'; } }); const [typePair, setTypePair] = useState(() => { try { return localStorage.getItem('kdc-type') || 'poppins'; } catch { return 'poppins'; } }); const [menuStyle, setMenuStyle] = useState(() => { try { return localStorage.getItem('kdc-menu') || 'carousel'; } catch { return 'carousel'; } }); useEffect(() => { document.documentElement.dataset.theme = theme; try { localStorage.setItem('kdc-theme', theme); } catch {} }, [theme]); useEffect(() => { const pairs = { poppins: { display: '"Poppins", sans-serif', sans: '"Poppins", system-ui, sans-serif' }, fraunces: { display: '"Fraunces", serif', sans: '"Inter", system-ui, sans-serif' }, playfair: { display: '"Playfair Display", serif', sans: '"Manrope", system-ui, sans-serif' }, }; const p = pairs[typePair] || pairs.poppins; document.documentElement.style.setProperty('--font-display', p.display); document.documentElement.style.setProperty('--font-sans', p.sans); try { localStorage.setItem('kdc-type', typePair); } catch {} }, [typePair]); useEffect(() => { try { localStorage.setItem('kdc-menu', menuStyle); } catch {} }, [menuStyle]); const value = useMemo(() => ({ theme, setTheme, typePair, setTypePair, menuStyle, setMenuStyle, }), [theme, typePair, menuStyle]); return React.createElement(ThemeContext.Provider, { value }, children); } function useTheme() { return useContext(ThemeContext); } /* ---------------- reveal on scroll ---------------- */ function useReveal() { useEffect(() => { const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -60px 0px' }); document.querySelectorAll('.reveal, .reveal-stagger').forEach((el) => io.observe(el)); return () => io.disconnect(); }); } /* ---------------- Logo mark ---------------- */ function Logo({ size = 36 }) { return ( KDC Construction logo ); } /* ---------------- Brand mark ---------------- */ function Brand({ to = 'index.html' }) { const { t } = useI18n(); return ( KDC Construction FL {t.brand.tagline} ); } /* ---------------- Language Switch ---------------- */ function LangSwitch() { const { lang, setLang } = useI18n(); return (
); } /* ---------------- Topbar ---------------- */ function Topbar({ onMenuOpen, transparentOnHero = false }) { const { t } = useI18n(); const [scrolled, setScrolled] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 60); window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); return () => window.removeEventListener('scroll', onScroll); }, []); const cls = ['topbar']; if (transparentOnHero && !scrolled) cls.push('on-hero'); if (scrolled) cls.push('scrolled'); return (
{t.nav.quote}
); } /* ---------------- Menu Overlay (Carousel / Grid / Index) ---------------- */ function MenuOverlay({ open, onClose }) { const { t, lang } = useI18n(); const { menuStyle } = useTheme(); const scrollerRef = useRef(null); useEffect(() => { document.documentElement.style.overflow = open ? 'hidden' : ''; return () => { document.documentElement.style.overflow = ''; }; }, [open]); useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); const items = t.menu.items; const hrefs = ['index.html', 'services.html', 'projects.html', 'about.html', 'contact.html', 'quote.html']; const scroll = (dir) => { const el = scrollerRef.current; if (!el) return; const cardW = el.querySelector('.menu-card')?.offsetWidth || 400; el.scrollBy({ left: dir * (cardW + 24), behavior: 'smooth' }); }; return (
{menuStyle === 'carousel' && ( <>
{items.map((it, i) => (
{it.n} {it.tag}
{it.t}
{it.d}
))}
{lang === 'en' ? 'Drag · scroll · click' : 'Arrastra · desliza · haz clic'}
)} {menuStyle === 'grid' && (
{items.map((it, i) => ( { e.currentTarget.style.background = 'var(--kdc-navy-900)'; e.currentTarget.style.color = 'var(--bone-50)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--bg)'; e.currentTarget.style.color = 'var(--ink)'; }} >
{it.n}{it.tag}
{it.t}
{it.d}
))}
)} {menuStyle === 'list' && (
{items.map((it, i) => ( { e.currentTarget.style.paddingLeft = '24px'; }} onMouseLeave={(e) => { e.currentTarget.style.paddingLeft = '0'; }} > {it.n} {it.t} {it.tag} → ))}
)}
); } function MenuCardArt({ idx }) { // Abstract geometric art per card const palettes = [ { a: '#17335F', b: '#E8E2D7' }, { a: '#0F2341', b: '#F3EFE8' }, { a: '#2A5691', b: '#FAF8F4' }, { a: '#1F4178', b: '#E8E2D7' }, { a: '#0F2341', b: '#D8D1C3' }, { a: '#17335F', b: '#F3EFE8' }, ]; const p = palettes[idx % palettes.length]; return ( {idx === 0 && <> } {idx === 1 && <> {[0,1,2,3,4,5,6].map(i => ( ))} } {idx === 2 && <> } {idx === 3 && <> } {idx === 4 && <> } {idx === 5 && <> } ); } /* ---------------- Placeholder image ---------------- */ function Ph({ label, ratio = '16/10', tone = 'default', style }) { const bgs = { default: {}, dark: { background: 'var(--kdc-navy-900)', color: 'var(--bone-300)' }, navy: { background: 'var(--kdc-navy-700)', color: 'var(--bone-50)' }, }; return (
{label}
); } /* ---------------- Float CTA (mobile call) ---------------- */ function FloatCta() { const { t } = useI18n(); const [show, setShow] = useState(false); useEffect(() => { const onScroll = () => setShow(window.scrollY > 600); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); return ( {t.nav.quote} → ); } /* ---------------- Footer with embedded form ---------------- */ function Footer() { const { t, lang } = useI18n(); const MAKE_WEBHOOK = 'https://hook.eu2.make.com/aknc2civ2ty87i55w5gep1gh6sr5y3sr'; const [sent, setSent] = useState(false); const [sending, setSending] = useState(false); const [vals, setVals] = useState({ name: '', email: '', msg: '', privacy: false }); const submit = (e) => { e.preventDefault(); setSending(true); fetch(MAKE_WEBHOOK, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ formType: 'Quick Message', name: vals.name, email: vals.email, message: vals.msg, }), }).finally(() => { setSending(false); setSent(true); setTimeout(() => { setSent(false); setVals({ name: '', email: '', msg: '', privacy: false }); }, 3500); }); }; return (