/* global React */
// ---------- Icons (inline SVG) ----------
const Icon = ({ name, size = 24, stroke = 2 }) => {
const paths = {
building: <> >,
home: <> >,
sparkles: <> >,
broom: <> >,
shield: <> >,
window: <> >,
phone: <> >,
mail: <> >,
location: <> >,
clock: <> >,
check: <> >,
arrow: <> >,
star: <> >,
users: <> >,
award: <> >,
leaf: <> >,
heart: <> >,
zap: <> >,
chat: <> >,
menu: <> >,
close: <> >,
target: <> >,
spray: <> >,
facebook: <> >,
instagram: <> >,
google: <> >,
'chevron-up': ,
};
return (
{paths[name] || null}
);
};
// ---------- SME Logo (real brand mark) ----------
const SMELogo = ({ size = 'md', light = false }) => {
const iconW = size === 'xl' ? 100 : size === 'lg' ? 84 : size === 'sm' ? 52 : 72;
const nameSize = size === 'xl' ? '1.35rem' : size === 'lg' ? '1.15rem' : size === 'sm' ? '0.85rem' : '1.05rem';
const tagSize = size === 'xl' ? '0.68rem' : size === 'lg' ? '0.62rem' : size === 'sm' ? '0.52rem' : '0.58rem';
const nameColor = light ? '#ffffff' : 'var(--sme-grey-900)';
const tagColor = light ? 'rgba(255,255,255,0.65)' : 'var(--sme-grey-400)';
const logoSrc = (typeof window !== 'undefined' && window.smeThemeUrl)
? window.smeThemeUrl + '/images/logo-sme.png?v=12'
: '/wp-content/themes/sme-nettoyage-react/images/logo-sme.png?v=12';
return (
NETTOYAGE
La propreté, notre engagement
);
};
// ---------- Scroll-triggered fade-in (rect-based for reliability) ----------
const Reveal = ({ children, delay = 0, y = 24, as: As = 'div', style = {}, ...rest }) => {
const ref = React.useRef(null);
const [visible, setVisible] = React.useState(false);
React.useEffect(() => {
const el = ref.current;
if (!el) return;
const check = () => {
const rect = el.getBoundingClientRect();
return rect.top < window.innerHeight - 40 && rect.bottom > 0;
};
if (check()) { setVisible(true); return; }
const onScroll = () => { if (check()) { setVisible(true); cleanup(); } };
// Safety fallback: reveal anyway after 1.2s so content can never stay hidden.
const fallback = setTimeout(() => { setVisible(true); cleanup(); }, 1200);
const cleanup = () => {
window.removeEventListener('scroll', onScroll);
clearTimeout(fallback);
};
window.addEventListener('scroll', onScroll, { passive: true });
return cleanup;
}, []);
return (
{children}
);
};
// ---------- Animated counter (rect-based) ----------
const Counter = ({ end, suffix = '', prefix = '', duration = 1600 }) => {
const ref = React.useRef(null);
const [val, setVal] = React.useState(0);
const startedRef = React.useRef(false);
React.useEffect(() => {
const el = ref.current;
if (!el) return;
let raf;
const startAnim = () => {
if (startedRef.current) return;
startedRef.current = true;
const start = performance.now();
const tick = (now) => {
const t = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - t, 3);
setVal(Math.round(end * eased));
if (t < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
};
const check = () => {
const rect = el.getBoundingClientRect();
return rect.top < window.innerHeight - 40 && rect.bottom > 0;
};
if (check()) { startAnim(); return () => cancelAnimationFrame(raf); }
const onScroll = () => { if (check()) { startAnim(); cleanup(); } };
const fallback = setTimeout(startAnim, 1500);
const cleanup = () => {
window.removeEventListener('scroll', onScroll);
clearTimeout(fallback);
};
window.addEventListener('scroll', onScroll, { passive: true });
return () => { cleanup(); cancelAnimationFrame(raf); };
}, [end, duration]);
return {prefix}{val.toLocaleString('fr-FR')}{suffix} ;
};
// ---------- Header (utility topbar + main bar with mega-menu) ----------
const SUB_SERVICES = [
{ id: 'entreprises', label: "Nettoyage d'Entreprises", icon: 'building', desc: 'Bureaux, sièges, espaces de travail.' },
{ id: 'batiments', label: 'Nettoyage de Bâtiments', icon: 'home', desc: 'Copropriétés, parties communes, halls.' },
{ id: 'vitres', label: 'Nettoyage de Vitres', icon: 'window', desc: 'Vitrines, façades, baies vitrées.' },
{ id: 'industriel', label: 'Nettoyage Industriel', icon: 'shield', desc: 'Sites de production, entrepôts, ateliers.' },
{ id: 'jardins', label: 'Jardins & Parcs', icon: 'leaf', desc: 'Entretien d\'espaces verts et extérieurs.' },
];
const Header = ({ currentPage, navigate }) => {
const [scrolled, setScrolled] = React.useState(false);
const [mobileOpen, setMobileOpen] = React.useState(false);
const [megaOpen, setMegaOpen] = React.useState(false);
const [mobileServicesOpen, setMobileServicesOpen] = React.useState(false);
const closeTimer = React.useRef(null);
React.useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 12);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
// Close mega-menu on Escape
React.useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') setMegaOpen(false); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
const pages = [
{ id: 'home', label: 'Accueil' },
{ id: 'about', label: 'À propos' },
{ id: 'services', label: 'Services', hasMega: true },
{ id: 'faq', label: 'FAQ' },
{ id: 'contact', label: 'Contact' },
];
const openMega = () => {
if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; }
setMegaOpen(true);
};
const scheduleClose = () => {
if (closeTimer.current) clearTimeout(closeTimer.current);
closeTimer.current = setTimeout(() => setMegaOpen(false), 180);
};
const goService = (subId) => {
setMegaOpen(false);
setMobileOpen(false);
setMobileServicesOpen(false);
navigate('service-' + subId);
};
const navItem = (p) => {
const active = currentPage === p.id;
if (p.hasMega) {
return (
{ setMegaOpen(!megaOpen); }}
aria-expanded={megaOpen}
aria-haspopup="true"
style={{
background: 'none', border: 'none',
fontFamily: 'Poppins, sans-serif',
fontWeight: active ? 600 : 500,
fontSize: '0.95rem',
color: active || megaOpen ? 'var(--sme-blue)' : 'var(--sme-grey-900)',
padding: '10px 4px',
position: 'relative',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
transition: 'color 0.2s',
}}>
{p.label}
);
}
return (
{ navigate(p.id); setMobileOpen(false); }}
style={{
background: 'none',
border: 'none',
fontFamily: 'Poppins, sans-serif',
fontWeight: active ? 600 : 500,
fontSize: '0.95rem',
color: active ? 'var(--sme-blue)' : 'var(--sme-grey-900)',
padding: '10px 4px',
position: 'relative',
cursor: 'pointer',
transition: 'color 0.2s',
}}>
{p.label}
);
};
return (
<>
{/* TOP UTILITY BAR */}
{/* MAIN HEADER */}
>
);
};
// ---------- Footer ----------
const Footer = ({ navigate }) => {
return (
{/* Brand block footer */}
NETTOYAGE
La propreté, notre engagement
Entreprise de nettoyage professionnel pour particuliers et professionnels.
Des espaces propres, sains et accueillants — avec rigueur et confiance.
Navigation
{['home', 'about', 'services', 'faq', 'contact'].map((p, i) => (
navigate(p)} style={{
background: 'none', border: 'none', padding: 0,
color: 'rgba(255,255,255,0.7)', cursor: 'pointer',
fontFamily: 'inherit', fontSize: '0.93rem',
}}>
{['Accueil', 'À propos', 'Services', 'FAQ', 'Contact'][i]}
))}
Services
{[
{ id: 'service-entreprises', label: "Nettoyage d'Entreprises" },
{ id: 'service-batiments', label: 'Nettoyage de Bâtiments' },
{ id: 'service-vitres', label: 'Nettoyage de Vitres' },
{ id: 'service-industriel', label: 'Nettoyage Industriel' },
{ id: 'service-jardins', label: 'Jardins & Parcs' },
].map(s => (
navigate(s.id)} style={{
background: 'none', border: 'none', padding: 0,
color: 'rgba(255,255,255,0.7)', cursor: 'pointer',
fontFamily: 'inherit', fontSize: '0.93rem',
textAlign: 'left',
transition: 'color 0.18s',
}}
onMouseEnter={e => e.currentTarget.style.color = 'white'}
onMouseLeave={e => e.currentTarget.style.color = 'rgba(255,255,255,0.7)'}>
{s.label}
))}
© 2026 SME Nettoyage — Tous droits réservés.
Site réalisé par{' '}
Digital Média Mobile
navigate('mentions-legales')} style={{
background: 'none', border: 'none', padding: 0, cursor: 'pointer',
color: 'rgba(255,255,255,0.55)', fontFamily: 'inherit', fontSize: 'inherit',
textDecoration: 'underline', textUnderlineOffset: 3,
}}>Mentions légales
navigate('politique-confidentialite')} style={{
background: 'none', border: 'none', padding: 0, cursor: 'pointer',
color: 'rgba(255,255,255,0.55)', fontFamily: 'inherit', fontSize: 'inherit',
textDecoration: 'underline', textUnderlineOffset: 3,
}}>Politique de confidentialité
);
};
// ---------- Reusable: section header ----------
const SectionHeader = ({ eyebrow, title, intro, center = false, eyebrowGreen = false }) => (
{eyebrow}
{title}
{intro &&
{intro}
}
);
// ---------- Back to top button ----------
const BackToTop = () => {
const [visible, setVisible] = React.useState(false);
React.useEffect(() => {
const onScroll = () => setVisible(window.scrollY > 400);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
if (!visible) return null;
return (
<>
window.scrollTo({ top: 0, behavior: 'smooth' })}
aria-label="Retour en haut de la page"
>
>
);
};
// ---------- Cookie consent banner ----------
const CookieConsent = ({ navigate }) => {
const STORAGE_KEY = 'sme_cookie_consent';
const [visible, setVisible] = React.useState(false);
React.useEffect(() => {
if (!localStorage.getItem(STORAGE_KEY)) setVisible(true);
}, []);
const accept = () => { localStorage.setItem(STORAGE_KEY, 'accepted'); setVisible(false); };
const refuse = () => { localStorage.setItem(STORAGE_KEY, 'refused'); setVisible(false); };
if (!visible) return null;
return (
<>
🍪 Ce site utilise des cookies
Nous utilisons des cookies techniques essentiels au bon fonctionnement du site.
Aucun cookie publicitaire n'est déposé sans votre accord.{' '}
{navigate && (
{ refuse(); navigate('politique-confidentialite'); }} style={{
background: 'none', border: 'none', padding: 0,
color: 'var(--sme-blue)', cursor: 'pointer',
fontFamily: 'inherit', fontSize: 'inherit',
fontWeight: 500, textDecoration: 'underline',
textUnderlineOffset: '2px',
}}>
En savoir plus
)}
{ e.currentTarget.style.borderColor = 'var(--sme-grey-400)'; e.currentTarget.style.color = 'var(--sme-grey-900)'; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--sme-grey-200)'; e.currentTarget.style.color = 'var(--sme-grey-700)'; }}
>
Tout refuser
{ e.currentTarget.style.background = 'var(--sme-blue-dark)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'var(--sme-blue)'; }}
>
Tout accepter
>
);
};
// Export to window for cross-file babel script access
Object.assign(window, { Icon, SMELogo, Header, Footer, SectionHeader, Reveal, Counter, BackToTop, CookieConsent });