/* ============================================================ MVH Mobile Detailing — shared component library Loaded as a global babel script. React is global. Exposes components + data on window. ============================================================ */ /* ---------- Brand constants ---------- */ const MVH = { phoneDisplay: '07311 796805', tel: '+447311796805', wa: '447311796805', email: 'j.m.stephens346@gmail.com', logo: MVH_THEME + '/assets/mvh-logo.png', }; const waLink = (msg) => `https://wa.me/${MVH.wa}?text=${encodeURIComponent(msg || "Hi MVH, I'd like to book a detail.")}`; /* Towns that have a dedicated SEO page */ const LOC_PAGES = ['cannock','hednesford','rugeley','stafford','lichfield','burntwood','sutton-coldfield','burton-upon-trent']; const townHref = (name) => { const k = name.toLowerCase().replace(/ /g, '-'); return LOC_PAGES.includes(k) ? `mobile-car-detailing-${k}/` : '/services/'; }; /* ---------- Navigation links ---------- */ const NAV_LINKS = [ { label: 'Services', href: '/services/' }, { label: 'Ceramic', href: '/ceramic-coating/' }, { label: 'Diagnostics', href: '/diagnostics/' }, { label: 'Gallery', href: '/gallery/' }, { label: 'About', href: '/about/' }, { label: 'Contact', href: '/contact/' }, ]; /* ---------- Service areas ---------- */ const AREAS = [ 'Hednesford', 'Cannock', 'Burntwood', 'Rugeley', 'Lichfield', 'Stafford', 'Sutton Coldfield', 'Featherstone', 'Huntington', 'Pye Green', 'Burton upon Trent', ]; /* ---------- Service tiers ---------- */ const SERVICES = [ { key: 'bronze', name: 'Bronze', tagline: 'Maintenance Detail', href: '/service-bronze/', cover: MVH_THEME + '/assets/cover-bronze.avif', blurb: 'Keep an already-cared-for vehicle pristine. Monthly bookings only.', time: '1 – 1.5 hrs', priceReg: '£45 – £55', priceLrg: '£55 – £65', includes: ['Safe wash & dry', 'Wheels cleaned', 'Tyre dressing', 'Vacuum', 'Dashboard wipe', 'Windows cleaned'], }, { key: 'silver', name: 'Silver', tagline: 'Full Valet', href: '/service-silver/', cover: MVH_THEME + '/assets/cover-silver.avif', blurb: 'The best all-round package. Inside and out, restored to fresh.', time: '2 – 3 hrs', priceReg: '£70 – £120', priceLrg: '£100 – £150', popular: true, includes: ['Safe wash & dry', 'Wheels cleaned', 'Tyre dressing', 'Deep vacuum', 'Dashboard wipe', 'Windows cleaned', 'Door shuts cleaned', 'Seat & carpet spot clean', 'Plastic trim cleaned', 'Sealant protection'], }, { key: 'gold', name: 'Gold', tagline: 'Enhancement Detail', href: '/service-gold/', cover: MVH_THEME + '/assets/cover-gold.png', blurb: 'Machine polish enhancement that brings back true depth and gloss.', time: '4 – 6 hrs', priceReg: '£150 – £300', priceLrg: '£220 – £400', includes: ['Everything in Silver', 'Clay mitt / bar treatment', 'Machine polish enhancement', 'Paint gloss boost', 'Wax sealant protection'], }, { key: 'platinum', name: 'Platinum', tagline: 'Showroom Detail', href: '/service-platinum/', cover: MVH_THEME + '/assets/cover-platinum.avif', blurb: 'Our flagship. Multi-stage correction and protection, end to end.', time: 'Full day +', priceReg: '£350 +', priceLrg: '£450 +', includes: ['Multi-stage decontamination', 'Full 2-stage machine polish', 'Interior deep clean', 'Leather clean & condition', 'Engine bay detail', 'Long-lasting paint protection', 'Fabric roof treatment (if applicable)'], }, ]; /* ---------- Diamond flagship tier ---------- */ const DIAMOND_SERVICE = { key: 'diamond', name: 'Diamond', tagline: 'Ultimate Detail', href: '/service-diamond/', cover: MVH_THEME + '/assets/cover-diamond.jpg', blurb: 'The ultimate paint correction, ceramic protection and total vehicle enhancement — MVH Detailing\'s most exclusive package.', time: 'Full day +', priceSmall: '£500 – £550', priceLarge: '£600 – £630', includes: [ 'Multi-stage decontamination', 'Full 2-stage machine polish', 'Interior deep clean', 'Leather clean & condition', 'Engine bay detail', 'Long-lasting paint protection', 'Fabric roof treatment (if applicable)', '2 year paintwork ceramic coating included', '2 year wheel face ceramic coating included', ], }; /* ---------- Interior Reset standalone service ---------- */ const INTERIOR_RESET = { key: 'interior-reset', name: 'Interior Reset', tagline: 'Interior Reset Detail', href: '/service-interior-reset/', cover: MVH_THEME + '/assets/cover-interior.avif', blurb: 'A comprehensive interior restoration service designed to refresh, sanitise and rejuvenate your vehicle\'s cabin.', time: '2 – 4 hrs', priceMed: '£100 – £120', priceLrg: '£120 – £140', priceReg: '£100 – £120', includes: [ 'Full interior vacuum', 'Deep clean of carpets and mats', 'Dashboard, centre console and trim cleaned', 'Door cards cleaned and dressed', 'Air vents thoroughly cleaned', 'Cup holders and storage compartments cleaned', 'Interior glass cleaned streak-free', 'Plastic and vinyl surfaces protected', 'Light stain removal', 'Boot area vacuumed and cleaned', ], upgrades: [ 'Fabric seat shampoo and extraction', 'Leather clean and conditioning', 'Pet hair removal', 'Heavy stain removal', 'Child seat deep clean', ], }; const ADDONS = [ { name: 'Ceramic coating — paintwork', price: '£150 – £250' }, { name: 'Window coating', price: '£100 – £150' }, { name: 'Wheel face ceramic coating', price: '£100 – £170' }, { name: 'Pet hair removal', price: '£20 – £80' }, { name: 'Mould removal', price: '£80 – £250+' }, { name: 'Engine bay detail', price: '£40' }, { name: 'Headlight restoration', price: '£60 – £100' }, { name: 'Leather conditioning', price: '£30 – £35' }, { name: 'Child seat clean', price: '£30' }, { name: 'Soft top protection', price: '£50 – £60' }, { name: 'Scratch reduction', price: '£50 – £300' }, ]; /* ---------- Reviews ---------- Real figures from the MVH Google Business Profile. The six invented "reviews" that shipped with the original design have been removed — the section links to Google instead of showing made-up testimonials. */ const MVHG = (typeof window !== 'undefined' && window.MVH_REVIEWS) ? window.MVH_REVIEWS : {}; /* Contact/booking form key (Web3Forms). Set in WordPress: Settings > MVH Google Reviews > Contact form access key. */ const FORM_KEY_FALLBACK = '2771599c-04cc-4eb5-a9cb-d1d5685429a1'; // Web3Forms access key (public by design — safe in page code) const FORM_KEY = (typeof window !== 'undefined' && window.MVH_FORMKEY) ? window.MVH_FORMKEY : FORM_KEY_FALLBACK; const REVIEW_STATS = { rating: MVHG.rating || '5.0', count: MVHG.count || 11 }; const GBP_URL = MVHG.url || 'https://search.google.com/local/reviews?placeid=ChIJ9dSKjtMnVo4RTJcwN5AZ7mo'; const REVIEWS = Array.isArray(MVHG.items) ? MVHG.items : []; /* ============================================================ ICONS ============================================================ */ function Icon({ name, size = 22, color = 'currentColor', stroke = 1.7, style }) { const p = { fill: 'none', stroke: color, strokeWidth: stroke, strokeLinecap: 'round', strokeLinejoin: 'round' }; const paths = { phone: , whatsapp: , star: , check: , arrow: , arrowUpRight: , plus: , menu: , close: , pin: , clock: , shield: , sparkle: , car: , droplet: , mail: , cart: , award: , }; return {paths[name]}; } function Stars({ n = 5, size = 16 }) { return {Array.from({ length: n }).map((_, i) => )}; } /* ============================================================ SCROLL REVEAL ============================================================ */ function Reveal({ children, delay = 0, as = 'div', className = '', style }) { const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { setTimeout(() => el.classList.add('in'), delay); io.unobserve(el); } }); }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }); io.observe(el); return () => io.disconnect(); }, [delay]); const Tag = as; return {children}; } /* ============================================================ PRIMITIVES ============================================================ */ function Eyebrow({ children, center }) { return
{children}
; } function Button({ children, variant = 'gold', size, href, onClick, block, icon, style, target }) { const cls = `btn btn--${variant}${size ? ' btn--' + size : ''}${block ? ' btn--block' : ''}`; const inner = <>{children}{icon && }; if (href) return {inner}; return ; } /* ============================================================ NAV ============================================================ */ function Nav({ active }) { const [scrolled, setScrolled] = React.useState(false); const [open, setOpen] = React.useState(false); React.useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 24); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); React.useEffect(() => { document.body.style.overflow = open ? 'hidden' : ''; }, [open]); return ( <>
MVH Mobile Detailing
Book Now
MVH
); } /* ============================================================ STICKY ACTION BAR + DESKTOP FLOATERS ============================================================ */ function StickyActions() { /* No point offering "Book Now" to someone already on the booking page — hide it there and let Call + WhatsApp share the bar evenly. */ const onBookingPage = (() => { try { return /^\/booking\/?$/.test(window.location.pathname); } catch (err) { return false; } })(); return ( <>
Call WhatsApp {!onBookingPage && Book Now}
); } /* ============================================================ FOOTER ============================================================ */ function Footer() { return ( ); } /* ============================================================ FAQ ============================================================ */ function FAQ({ items }) { const [open, setOpen] = React.useState(0); return (
{items.map((it, i) => (
{it.a}
))}
); } /* ============================================================ REVIEWS SECTION ============================================================ */ function ReviewCard({ r }) { return (
{r.initial}
{r.url ? {r.name} : {r.name}}
{r.date}

"{r.text}"

{r.vehicle &&
{r.vehicle}
}
); } function ReviewsSection() { return (
Verified Google Reviews

Trusted across
Staffordshire

{REVIEW_STATS.rating}
Google Rating
Based on {REVIEW_STATS.count} reviews
{REVIEWS.length > 0 && (
{REVIEWS.map((r, i) => )}
)}
); } /* ============================================================ SERVICE AREAS SECTION ============================================================ */ function AreasSection() { return (
Mobile · We Come To You

Serving Staffordshire

Fully self-sufficient mobile detailing — we bring our own power and water. Tap your town for local pricing & availability.

{AREAS.map(a => ( {a} ))}
); } /* ============================================================ CTA BAND ============================================================ */ function CTABand({ title = 'Ready for showroom results?', sub = 'Book your detail in under a minute, or message us on WhatsApp for a tailored quote.' }) { return (
Enhance · Protect · Maintain

{title}

{sub}

); } /* ============================================================ BEFORE / AFTER SLIDER ============================================================ */ function BeforeAfter({ id, beforeLabel = 'Before', afterLabel = 'After', caption, height = 460 }) { const [pos, setPos] = React.useState(52); const ref = React.useRef(null); const dragging = React.useRef(false); const move = (clientX) => { const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); let p = ((clientX - r.left) / r.width) * 100; p = Math.max(2, Math.min(98, p)); setPos(p); }; React.useEffect(() => { const up = () => (dragging.current = false); const mv = (e) => { if (dragging.current) move(e.touches ? e.touches[0].clientX : e.clientX); }; window.addEventListener('mouseup', up); window.addEventListener('touchend', up); window.addEventListener('mousemove', mv); window.addEventListener('touchmove', mv, { passive: false }); return () => { window.removeEventListener('mouseup', up); window.removeEventListener('touchend', up); window.removeEventListener('mousemove', mv); window.removeEventListener('touchmove', mv); }; }, []); return (
{ dragging.current = true; move(e.clientX); }} onTouchStart={(e) => { dragging.current = true; move(e.touches[0].clientX); }}> {/* After (full) */} {afterLabel} {/* Before (clipped) */}
{beforeLabel}
{/* handle */}
{caption &&
{caption}
}
); } /* ============================================================ SERVICE CARD ============================================================ */ function ServiceCard({ s, index }) { return (
{s.cover ? {s.name : }
{s.popular && Most Popular}
{('0' + (index + 1))}
{s.name}
{s.tagline}

{s.blurb}

From
{s.priceReg.split('–')[0].trim()}
Details
); } /* ---------- JSON-LD schema injector ---------- */ function injectSchema(obj) { const s = document.createElement('script'); s.type = 'application/ld+json'; s.textContent = JSON.stringify(obj); document.head.appendChild(s); } /* ---------- expose globally ---------- */ Object.assign(window, { MVH, waLink, NAV_LINKS, AREAS, SERVICES, ADDONS, REVIEWS, REVIEW_STATS, GBP_URL, FORM_KEY, DIAMOND_SERVICE, INTERIOR_RESET, Icon, Stars, Reveal, Eyebrow, Button, Nav, StickyActions, Footer, FAQ, townHref, LOC_PAGES, ReviewCard, ReviewsSection, AreasSection, CTABand, injectSchema, BeforeAfter, ServiceCard, });