/* =========================================================================== CX-Feedback — shared sections and primitives --------------------------------------------------------------------------- British English. No em dashes. No banned vocabulary. Voice rules in Docs/02-brand-voice.md are non-negotiable. =========================================================================== */ const { useState, useEffect, useRef, useId } = React; /* --------------------------------------------------------------------------- Utility: scroll-triggered reveal --------------------------------------------------------------------------- */ function useReveal() { useEffect(() => { const els = document.querySelectorAll('.reveal'); if (!('IntersectionObserver' in window)) { els.forEach((e) => e.classList.add('is-visible')); return; } const io = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('is-visible'); io.unobserve(entry.target); } }); }, { rootMargin: '0px 0px -8% 0px', threshold: 0.08 } ); els.forEach((el) => io.observe(el)); return () => io.disconnect(); }, []); } /* --------------------------------------------------------------------------- Brand wordmark (inline SVG; swap for a real logo when one exists) --------------------------------------------------------------------------- */ const Wordmark = ({ className = '', variant = 'colour' }) => { const src = variant === 'white' ? 'assets/cx_feedback_logo_no_strap_light_cropped.png' : 'assets/cx_feedback_logo_no_strap_rgb_cropped.png'; return ( CX-Feedback Logo ); }; /* --------------------------------------------------------------------------- Buttons & Links (Disabled dynamically if not pointing to the homepage) --------------------------------------------------------------------------- */ const isNotHomepage = (href) => { if (!href) return false; const h = href.trim(); if (h === '#' || h === '' || h.startsWith('#')) return false; if (h === 'index.html' || h === '/' || h.includes('index.html')) return false; return true; }; const BtnPrimary = ({ href = 'Book-a-call.html', children, className = '', icon = null, fullWidth = false, ...rest }) => { const disabled = isNotHomepage(href); if (disabled) { return ( e.preventDefault()} className={`btn inline-flex items-center justify-center gap-2 bg-slate-100 border border-slate-200 text-slate-400 font-semibold rounded-lg px-6 py-3 pointer-events-none opacity-60 cursor-not-allowed ${fullWidth ? 'w-full' : ''} ${className}`} {...rest} > {children} {icon} ); } return ( {children} {icon} ); }; const BtnSecondary = ({ href = '#', children, className = '', icon = null, ...rest }) => { const disabled = isNotHomepage(href); if (disabled) { return ( e.preventDefault()} className={`btn inline-flex items-center justify-center gap-2 bg-slate-100 border border-slate-200 text-slate-400 font-semibold rounded-lg px-6 py-3 pointer-events-none opacity-60 cursor-not-allowed ${className}`} {...rest} > {icon} {children} ); } return ( {icon} {children} ); }; const TextLinkCTA = ({ href, children }) => { const disabled = isNotHomepage(href); if (disabled) { return ( {children} ); } return ( {children} ); }; /* --------------------------------------------------------------------------- Calm image placeholder (replace one-for-one with real photography) --------------------------------------------------------------------------- */ const ImagePlaceholder = ({ alt, ratio = '4 / 3', tone = 'slate', className = '' }) => { const tones = { slate: 'from-slate-100 to-slate-200 text-slate-500', blue: 'from-blue-50 to-blue-100 text-blue-700', warm: 'from-amber-50 to-orange-100 text-amber-800', dark: 'from-slate-800 to-slate-900 text-slate-300', }; return (
); }; /* =========================================================================== Sticky header =========================================================================== */ function Header() { const [scrolled, setScrolled] = useState(false); const [mobileOpen, setMobileOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 4); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); useEffect(() => { if (!mobileOpen) return; const onKey = (e) => e.key === 'Escape' && setMobileOpen(false); window.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [mobileOpen]); const nav = [ { label: 'Product', href: 'Product-surveys.html' }, { label: 'Pricing', href: 'Pricing.html' }, { label: 'Customers', href: 'Customers.html' }, { label: 'Knowledge', href: 'Knowledge.html' }, { label: 'Why us', href: 'Why-us.html' }, { label: 'About', href: 'About.html' }, ]; return (
e.preventDefault()} className="hidden lg:inline-flex text-[15px] text-slate-400 font-medium px-2 pointer-events-none opacity-50 cursor-not-allowed" > Log in e.preventDefault()} className="hidden lg:inline-flex btn items-center justify-center bg-slate-100 border border-slate-200 text-slate-400 font-semibold rounded-lg px-5 py-2.5 pointer-events-none opacity-60 cursor-not-allowed" > Book a call
setMobileOpen(false)} />
); } /* =========================================================================== Footer =========================================================================== */ const FooterLink = ({ href, children }) => { const disabled = isNotHomepage(href); if (disabled) { return ( e.preventDefault()} className="text-slate-500 font-medium pointer-events-none opacity-60 cursor-not-allowed" > {children} ); } return ( {children} ); }; function Footer() { return ( ); } /* =========================================================================== Reusable: Page hero (used on every subpage) =========================================================================== */ function PageHero({ eyebrow, h1, sub, primary = { label: 'Book a thirty-minute call', href: 'Book-a-call.html' }, secondary, bg = '#FFFFFF' }) { return (
{eyebrow &&

{eyebrow}

}

{h1}

{sub && (

{sub}

)}
{primary && {primary.label}} {secondary && }>{secondary.label}}
); } /* =========================================================================== Reusable: Section heading =========================================================================== */ function SectionHeading({ eyebrow, h2, intro, centred = false, id }) { return (
{eyebrow &&

{eyebrow}

} {h2 &&

{h2}

} {intro &&

{intro}

}
); } /* =========================================================================== Reusable: Big quote card =========================================================================== */ function QuoteCard({ quote, name, role, org, tag, accent = false, link, logo }) { const disabled = isNotHomepage(link); return (
{tag && ( {tag} )}
“{quote}”
{logo && (
{`${org}
)}
{name}
{role}, {org}
{link && ( disabled ? ( Read the full story → ) : ( Read the full story → ) )}
); } /* =========================================================================== Reusable: Stat tile =========================================================================== */ function StatTile({ value, label, source, href }) { const disabled = isNotHomepage(href); const Tag = (href && !disabled) ? 'a' : 'div'; return (
{value}
{label}
{source && (
{source} {href && !disabled && } {href && disabled && (Coming soon)}
)}
); } /* =========================================================================== Reusable: Pain/answer card =========================================================================== */ function PainAnswerCard({ pain, answer, Icon }) { return (
{Icon && ( )}

{pain}

{answer}

); } /* =========================================================================== Reusable: Feature card =========================================================================== */ function FeatureCard({ Icon, title, body, href, linkLabel = 'Learn more' }) { return (
{Icon && ( )}

{title}

{body}

{href &&
{linkLabel}
}
); } /* =========================================================================== Reusable: Lead capture form (Book a call / Contact) =========================================================================== */ function LeadForm({ buttonLabel = 'Send my details', compact = false }) { const [submitted, setSubmitted] = useState(false); const onSubmit = (e) => { e.preventDefault(); setSubmitted(true); }; if (submitted) { return (

Thanks. We will be in touch.

We have your details. Expect a reply from the sales team the same working day with a few times that suit you. If it is urgent, call us on {' '}0141 810 2599.

); } return (

By submitting you agree to our privacy policy. We will only use your details to reply about CX-Feedback. No nurture spam.

); } const Field = ({ label, name, type = 'text', required = false, hint, defaultValue }) => { const id = useId(); return (
{hint &&

{hint}

}
); }; const TextareaField = ({ label, name, rows = 4, required = false }) => { const id = useId(); return (