/* Genesis DMA — shared primitives, hooks, icons */ const { useState, useEffect, useRef, useMemo, useCallback, memo } = React; /* --- Hash router --- */ function useHashRoute() { const getPath = () => { const h = window.location.hash || '#/'; return h.replace(/^#/, '') || '/'; }; const [path, setPath] = useState(getPath); useEffect(() => { const onHash = () => { setPath(getPath()); window.scrollTo({ top: 0, behavior: 'auto' }); }; window.addEventListener('hashchange', onHash); if (!window.location.hash) window.location.hash = '#/'; return () => window.removeEventListener('hashchange', onHash); }, []); return path; } /* --- SEO hook: title, meta description, canonical, og/twitter, JSON-LD --- */ function setMeta(selector, attr, value) { let el = document.head.querySelector(selector); if (!el) { el = document.createElement('meta'); const [, name] = selector.match(/\[([^=]+)="([^"]+)"\]/) || []; // fallback: parse selector crudely const m = selector.match(/\[([^=]+)="([^"]+)"\]/); if (m) el.setAttribute(m[1], m[2]); document.head.appendChild(el); } el.setAttribute(attr, value); } function setLink(rel, href) { let el = document.head.querySelector(`link[rel="${rel}"]`); if (!el) { el = document.createElement('link'); el.setAttribute('rel', rel); document.head.appendChild(el); } el.setAttribute('href', href); } function useSEO({ title, description, path, jsonLd, ogImage }) { useEffect(() => { const canonical = SITE.url + path; document.title = title; setMeta('meta[name="description"]', 'content', description); setLink('canonical', canonical); setMeta('meta[property="og:title"]', 'content', title); setMeta('meta[property="og:description"]', 'content', description); setMeta('meta[property="og:url"]', 'content', canonical); setMeta('meta[property="og:image"]', 'content', ogImage || SITE.defaultOg); setMeta('meta[name="twitter:title"]', 'content', title); setMeta('meta[name="twitter:description"]', 'content', description); setMeta('meta[name="twitter:image"]', 'content', ogImage || SITE.defaultOg); // Clean up old JSON-LD blocks that we manage document.head.querySelectorAll('script[data-gdma-ld]').forEach(n => n.remove()); const blocks = Array.isArray(jsonLd) ? jsonLd : (jsonLd ? [jsonLd] : []); blocks.forEach(obj => { const s = document.createElement('script'); s.type = 'application/ld+json'; s.setAttribute('data-gdma-ld', '1'); s.textContent = JSON.stringify(obj); document.head.appendChild(s); }); }, [title, description, path, JSON.stringify(jsonLd)]); } /* --- Single shared IntersectionObserver for reveals --- */ let sharedRevealObserver = null; function getRevealObserver() { if (sharedRevealObserver) return sharedRevealObserver; sharedRevealObserver = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('is-visible'); sharedRevealObserver.unobserve(e.target); } }); }, { rootMargin: '0px 0px -10% 0px', threshold: 0.05 }); return sharedRevealObserver; } function Reveal({ as: Tag = 'div', className = '', children, delay = 0, ...rest }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; if (delay) el.style.transitionDelay = `${delay}ms`; getRevealObserver().observe(el); return () => getRevealObserver().unobserve(el); }, [delay]); return {children}; } /* --- Icons (1.5px stroke) --- */ const Ico = { Arrow: (p) => , Plus: (p) => , Bolt: (p) => , Search: (p) => , Flow: (p) => , Chip: (p) => , Pen: (p) => , Chart: (p) => , Check: (p) => , Pin: (p) => , Mail: (p) => , Phone: (p) => , }; /* --- Primary / Ghost buttons --- */ function BtnPrimary({ href, children, onClick, ...r }) { return {children}; } function BtnGhost({ href, children, onClick, ...r }) { return {children}; } /* --- Breadcrumb --- */ function Breadcrumb({ items }) { return ( {items.map((it, i) => ( {i > 0 && /} {i < items.length - 1 ? {it.label} : {it.label}} ))} ); } /* --- FAQ --- */ function FAQ({ items }) { return ( {items.map(([q, a], i) => ( {q} {a} ))} ); } /* --- Section wrapper --- */ function Section({ id, label, title, intro, children, className = '' }) { return ( {(label || title || intro) && ( {label && {label}} {title && {title}} {intro && {intro}} )} {children} ); } /* --- Counter (count-up on intersect) --- */ function Counter({ to, prefix = '', suffix = '', duration = 1500 }) { const ref = useRef(null); const [val, setVal] = useState(0); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { const start = performance.now(); const step = (t) => { const p = Math.min(1, (t - start) / duration); const eased = 1 - Math.pow(1 - p, 3); setVal(Math.round(to * eased)); if (p < 1) requestAnimationFrame(step); }; requestAnimationFrame(step); io.disconnect(); } }, { threshold: 0.3 }); io.observe(el); return () => io.disconnect(); }, [to, duration]); return {prefix}{val.toLocaleString()}{suffix}; } Object.assign(window, { useHashRoute, useSEO, Reveal, Ico, BtnPrimary, BtnGhost, Breadcrumb, FAQ, Section, Counter, });
{a}