/* global React */
const { useEffect, useRef, useState } = React;
// ===== Logo =====
function ChicLogo({ size = "md", color, style }) {
const cls = `chic-logo chic-logo--${size}`;
return (
Centre
Hospitalier
International de
Calavi (CHIC)
);
}
// ===== Pills =====
function StatusPill({ status, dense }) {
const open = status === "open";
return (
{dense ? (open ? "Ouvert" : "Bientôt") : (open ? "Disponible" : "Bientôt")}
);
}
// ===== Animated number counter (counts up when visible) =====
function Counter({ to = 0, duration = 1400, suffix = "", className = "", style }) {
const [val, setVal] = useState(0);
const ref = useRef(null);
const started = useRef(false);
useEffect(() => {
const node = ref.current;
if (!node) return;
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting && !started.current) {
started.current = true;
const start = performance.now();
const tick = t => {
const p = Math.min(1, (t - start) / duration);
// ease-out cubic
const eased = 1 - Math.pow(1 - p, 3);
setVal(Math.round(eased * to));
if (p < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
});
}, { threshold: 0.4 });
obs.observe(node);
return () => obs.disconnect();
}, [to, duration]);
return {val.toLocaleString("fr-FR")}{suffix};
}
// ===== Stat block =====
function Stat({ value, label, frac, prefix = "", suffix = "" }) {
// value may include a fraction "openSpec/totalSpec" — split visually
if (typeof value === "string" && value.includes("/")) {
const [a, b] = value.split("/");
return (
);
}
return (
{prefix}{suffix}
{frac && /{frac}}
{label}
);
}
// ===== Reveal on scroll =====
function FadeUp({ children, delay = 0, as: As = "div", className = "", style }) {
const [shown, setShown] = useState(false);
const ref = useRef(null);
useEffect(() => {
const node = ref.current;
if (!node) return;
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
setTimeout(() => setShown(true), delay);
obs.disconnect();
}
});
}, { threshold: 0.1, rootMargin: "0px 0px -40px 0px" });
obs.observe(node);
return () => obs.disconnect();
}, [delay]);
return (
{children}
);
}
// ===== Crumbs =====
function Crumbs({ trail }) {
return (
{trail.map((t, i) => (
{i > 0 && /}
{i === trail.length - 1
? {t.label}
: {t.label}}
))}
);
}
// ===== Section header (eyebrow + title + lead + optional right) =====
function SectionHeader({ eyebrow, title, lead, right }) {
return (
{eyebrow &&
{eyebrow}
}
{title &&
{title}
}
{lead &&
{lead}
}
{right &&
{right}
}
);
}
Object.assign(window, {
ChicLogo, StatusPill, Counter, Stat, FadeUp, Crumbs, SectionHeader,
});