// 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 (
);
}
/* ---------------- 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 (
);
}
/* ---------------- 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) => (
))}
{lang === 'en' ? 'Drag · scroll · click' : 'Arrastra · desliza · haz clic'}
>
)}
{menuStyle === 'grid' && (
)}
{menuStyle === 'list' && (
)}
);
}
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 (
);
}
/* ---------------- 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 (
);
}
/* ---------------- Tweaks Panel ---------------- */
function Tweaks() {
const [active, setActive] = useState(false);
const [visible, setVisible] = useState(false);
const { theme, setTheme, typePair, setTypePair, menuStyle, setMenuStyle } = useTheme();
useEffect(() => {
const onMsg = (e) => {
if (e.data?.type === '__activate_edit_mode') setVisible(true);
if (e.data?.type === '__deactivate_edit_mode') setVisible(false);
};
window.addEventListener('message', onMsg);
window.parent?.postMessage({ type: '__edit_mode_available' }, '*');
return () => window.removeEventListener('message', onMsg);
}, []);
if (!visible) return null;
return (
<>
Tweaks
{['carousel', 'grid', 'list'].map(s => (
))}
{[['poppins','Poppins'],['fraunces','Fraunces'],['playfair','Playfair']].map(([k,l]) => (
))}
{['light', 'dark'].map(s => (
))}
>
);
}
/* ---------------- App Shell ---------------- */
function ShellUI({ children, transparentTop = false }) {
const [menuOpen, setMenuOpen] = useState(false);
useReveal();
return (
<>
setMenuOpen(true)} transparentOnHero={transparentTop}/>
setMenuOpen(false)}/>
{children}
{typeof ConsentUI !== 'undefined' && }
>
);
}
function Shell({ children, transparentTop = false }) {
// Backwards-compatible: used by Home which doesn't wrap with AppRoot.
// If providers are already present above, we don't add them twice.
const existing = useContext(I18nContext);
if (existing) return {children};
return (
{children}
);
}
function AppRoot({ children }) {
return (
{children}
);
}
/* ---------------- Page Head (shared) ---------------- */
function PageHead({ eyebrow, title, sub }) {
return (
);
}
Object.assign(window, {
Logo, Brand, LangSwitch, Topbar, MenuOverlay, Ph, FloatCta, Footer,
Tweaks, Shell, AppRoot, PageHead, I18nProvider, ThemeProvider, useI18n, useTheme, useReveal,
});