/* 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: <>>,
};
return (
);
};
// ---------- SME Logo (real brand mark) ----------
const SMELogo = ({ size = 'md', light = false, vertical = false }) => {
const scale = size === 'xl' ? 1.8 : size === 'lg' ? 1.3 : size === 'sm' ? 0.7 : 1;
const subColor = light ? 'rgba(255,255,255,0.95)' : '#0891B2';
const tagColor = light ? 'rgba(255,255,255,0.55)' : '#94A3B8';
// Real brand logo (JPEG, white background). On dark surfaces we wrap it
// in a small white card so the white background reads as intentional.
const markSize = 64 * scale;
const mark = (
);
if (vertical) {
return (
{mark}
NETTOYAGE
La propreté, notre engagement
);
}
return (
{mark}
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('services');
// Defer hash so navigate's scroll-to-top finishes first
setTimeout(() => {
const el = document.getElementById('service-' + subId);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 350);
};
const navItem = (p) => {
const active = currentPage === p.id;
if (p.hasMega) {
return (
);
}
return (
);
};
return (
<>
{/* TOP UTILITY BAR */}
{/* MAIN HEADER */}
>
);
};
// ---------- Footer ----------
const Footer = ({ navigate }) => (
);
// ---------- Reusable: section header ----------
const SectionHeader = ({ eyebrow, title, intro, center = false, eyebrowGreen = false }) => (
{eyebrow}
{title}
{intro &&
{intro}
}
);
// Export to window for cross-file babel script access
Object.assign(window, { Icon, SMELogo, Header, Footer, SectionHeader, Reveal, Counter });