// SALVO — Part 1: Utilities, Loader, Cursor, Nav, Hero, Stats, Problem const { useState, useEffect, useRef, useCallback, useMemo } = React; // ===== HOOKS ===== const useInView = (options = {}) => { const ref = useRef(null); const [inView, setInView] = useState(false); useEffect(() => { const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { setInView(true); observer.disconnect(); } }, { threshold: 0.12, ...options }); if (ref.current) observer.observe(ref.current); return () => observer.disconnect(); }, []); return [ref, inView]; }; const useReducedMotion = () => { const [reduced, setReduced] = useState(false); useEffect(() => { const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); setReduced(mq.matches); const handler = (e) => setReduced(e.matches); mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); }, []); return reduced; }; // ===== ANIMATED COUNTER ===== const AnimatedCounter = ({ from = 0, to, duration = 1500, suffix = '', prefix = '', format }) => { const [value, setValue] = useState(from); const [ref, inView] = useInView(); const reduced = useReducedMotion(); useEffect(() => { if (!inView) return; if (reduced) { setValue(to); return; } const start = performance.now(); const animate = (now) => { const p = Math.min((now - start) / duration, 1); const ease = p === 1 ? 1 : 1 - Math.pow(2, -10 * p); setValue(from + (to - from) * ease); if (p < 1) requestAnimationFrame(animate); }; requestAnimationFrame(animate); }, [inView, reduced]); const display = format ? format(value) : (Number.isInteger(to) ? Math.round(value) : value.toFixed(1)); return {prefix}{display}{suffix}; }; // ===== REVEAL ON SCROLL ===== const RevealOnScroll = ({ children, delay = 0, y = 20, className = '' }) => { const [ref, inView] = useInView(); const reduced = useReducedMotion(); const style = reduced ? {} : { opacity: inView ? 1 : 0, transform: inView ? 'translateY(0)' : `translateY(${y}px)`, transition: `opacity 0.6s cubic-bezier(0.16,1,0.3,1) ${delay}ms, transform 0.6s cubic-bezier(0.16,1,0.3,1) ${delay}ms`, }; return
{children}
; }; // ===== SALVO BUTTON ===== const SalvoButton = ({ children, variant = 'primary', onClick, href, style = {} }) => { const base = { display: 'inline-flex', alignItems: 'center', gap: '8px', fontFamily: "'Syne', sans-serif", fontWeight: 700, fontSize: '11px', letterSpacing: '0.12em', textTransform: 'uppercase', padding: '12px 28px', cursor: 'pointer', border: 'none', textDecoration: 'none', transition: 'all 0.2s cubic-bezier(0.16,1,0.3,1)', position: 'relative', overflow: 'hidden', }; const primary = { ...base, background: 'var(--accent)', color: 'var(--bg)', ...style, }; const ghost = { ...base, background: 'transparent', color: 'var(--white)', border: '1px solid var(--border)', ...style, }; const s = variant === 'primary' ? primary : ghost; const El = href ? 'a' : 'button'; return ( { if (variant === 'primary') { e.currentTarget.style.background = 'var(--accent-dim)'; e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 8px 32px rgba(255,107,53,0.25)'; } else { e.currentTarget.style.color = 'var(--accent)'; e.currentTarget.style.borderColor = 'var(--accent)'; e.currentTarget.style.transform = 'translateY(-2px)'; } }} onMouseLeave={e => { e.currentTarget.style.background = variant === 'primary' ? 'var(--accent)' : 'transparent'; e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = 'none'; if (variant !== 'primary') { e.currentTarget.style.color = 'var(--white)'; e.currentTarget.style.borderColor = 'var(--border)'; } }} >{children} ); }; // ===== CUSTOM CURSOR ===== const CustomCursor = () => { const dotRef = useRef(null); const ringRef = useRef(null); const pos = useRef({ x: 0, y: 0 }); const ring = useRef({ x: 0, y: 0 }); useEffect(() => { const dot = dotRef.current; const ringEl = ringRef.current; if (!dot || !ringEl) return; const onMove = (e) => { pos.current = { x: e.clientX, y: e.clientY }; dot.style.transform = `translate(${e.clientX - 4}px, ${e.clientY - 4}px)`; }; let raf; const animate = () => { ring.current.x += (pos.current.x - ring.current.x) * 0.12; ring.current.y += (pos.current.y - ring.current.y) * 0.12; ringEl.style.transform = `translate(${ring.current.x - 16}px, ${ring.current.y - 16}px)`; raf = requestAnimationFrame(animate); }; animate(); const onEnter = () => { dot.style.width = '32px'; dot.style.height = '32px'; dot.style.marginTop = '-12px'; dot.style.marginLeft = '-12px'; }; const onLeave = () => { dot.style.width = '8px'; dot.style.height = '8px'; dot.style.marginTop = '0'; dot.style.marginLeft = '0'; }; document.addEventListener('mousemove', onMove); document.querySelectorAll('a, button, [role=button]').forEach(el => { el.addEventListener('mouseenter', onEnter); el.addEventListener('mouseleave', onLeave); }); return () => { document.removeEventListener('mousemove', onMove); cancelAnimationFrame(raf); }; }, []); return ( <>
); }; // ===== LOADER ===== const Loader = ({ onDone }) => { const [phase, setPhase] = useState('letters'); // letters | line | tagline | fadeout const [letters, setLetters] = useState([false,false,false,false,false]); const [lineWidth, setLineWidth] = useState(0); const [typed, setTyped] = useState(''); const [opacity, setOpacity] = useState(1); const reduced = useReducedMotion(); const tagline = 'FIRE WHEN READY.'; useEffect(() => { if (reduced) { onDone(); return; } const word = 'SALVO'; // Stagger letters word.split('').forEach((_, i) => { setTimeout(() => setLetters(prev => { const n=[...prev]; n[i]=true; return n; }), i * 80 + 100); }); // Draw line setTimeout(() => { setPhase('line'); let w = 0; const lineAnim = setInterval(() => { w += 4; setLineWidth(Math.min(w, 100)); if (w >= 100) clearInterval(lineAnim); }, 16); }, 700); // Typewriter tagline setTimeout(() => { setPhase('tagline'); let i = 0; const typeAnim = setInterval(() => { i++; setTyped(tagline.slice(0, i)); if (i >= tagline.length) clearInterval(typeAnim); }, 30); }, 1100); // Fade out setTimeout(() => { setPhase('fadeout'); setOpacity(0); setTimeout(onDone, 500); }, 1900); }, []); return (
{'SALVO'.split('').map((l, i) => ( {l} ))}
{typed}
Click to skip
); }; // ===== NAV ===== const Nav = ({ onReserve }) => { const [scrolled, setScrolled] = useState(false); const [mobileOpen, setMobileOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 80); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); const scrollTo = (id) => { const el = document.getElementById(id); if (el) window.scrollTo({ top: el.offsetTop - 80, behavior: 'smooth' }); setMobileOpen(false); }; const links = [ { label: 'Science', id: 'science' }, { label: 'Produit', id: 'product' }, { label: 'Athlètes', id: 'athletes' }, { label: 'Pré-commande', id: 'preorder' }, ]; return ( <> {/* Mobile overlay */} {mobileOpen && (
{links.map(l => ( ))} { onReserve(); setMobileOpen(false); }}>Réserver mon pack
)} ); }; // ===== PARTICLE CANVAS ===== const ParticleCanvas = () => { const canvasRef = useRef(null); const reduced = useReducedMotion(); useEffect(() => { if (reduced) return; const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const resize = () => { canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; }; resize(); window.addEventListener('resize', resize); const N = 90; const particles = Array.from({ length: N }, () => ({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, vx: (Math.random() - 0.5) * 0.25, vy: (Math.random() - 0.5) * 0.25, r: Math.random() * 1.2 + 0.5, op: Math.random() * 0.12 + 0.03, })); let raf; const draw = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { p.x = (p.x + p.vx + canvas.width) % canvas.width; p.y = (p.y + p.vy + canvas.height) % canvas.height; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fillStyle = `rgba(255,107,53,${p.op})`; ctx.fill(); }); raf = requestAnimationFrame(draw); }; draw(); return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', resize); }; }, [reduced]); return ; }; // ===== HERO SECTION ===== const HeroSection = ({ onReserve }) => { const [visible, setVisible] = useState({ badge: false, letters: [false,false,false,false,false], sub: false, tagline: false, cta: false, stats: false }); const reduced = useReducedMotion(); useEffect(() => { if (reduced) { setVisible({ badge: true, letters: [true,true,true,true,true], sub: true, tagline: true, cta: true, stats: true }); return; } setTimeout(() => setVisible(v => ({ ...v, badge: true })), 300); 'SALVO'.split('').forEach((_, i) => { setTimeout(() => setVisible(v => { const l = [...v.letters]; l[i] = true; return { ...v, letters: l }; }), 500 + i * 70); }); setTimeout(() => setVisible(v => ({ ...v, sub: true })), 900); setTimeout(() => setVisible(v => ({ ...v, tagline: true })), 1100); setTimeout(() => setVisible(v => ({ ...v, cta: true })), 1400); setTimeout(() => setVisible(v => ({ ...v, stats: true })), 1700); }, []); const scrollTo = (id) => { const el = document.getElementById(id); if (el) window.scrollTo({ top: el.offsetTop - 80, behavior: 'smooth' }); }; const stats = [ { val: '90s', label: 'Onset time' }, { val: '900mg', label: 'Sodium' }, { val: '1/7', label: 'Athlètes ciblés' }, { val: '30ml', label: 'Format shot' }, ]; return (
{/* Radial glow */}
{/* Badge */}
Pré-commande — Q4 2026
{/* Brand hero */}

{'SALVO'.split('').map((l, i) => ( {l} ))}

{/* Subtitle */}
Fire When Ready.
{/* Tagline */}

Le premier shot anti-crampe européen.
90 secondes. Double action. Pour le 1 sur 7.

{/* CTA row */}
Réserver mon pack scrollTo('science')} style={{ padding: '14px 28px', fontSize: 12 }}> Comprendre la science →
{/* Stats strip */}
{stats.map((s, i) => (
{s.val}
{s.label}
))}
{/* Scroll indicator */}
Scroll
); }; // ===== STATS BAR ===== const StatsBar = () => { const cards = [ { value: 15, suffix: '%', label: 'Des athlètes sont des heavy sweaters' }, { value: 900, suffix: 'mg', label: 'Sodium par dose' }, { value: 90, suffix: 's', label: "Délai d'action TRP" }, { value: 0, suffix: '', label: 'Concurrent européen direct', special: 'ZÉRO' }, ]; return (
{cards.map((c, i) => { const [ref, inView] = useInView(); return (
e.currentTarget.style.background = 'var(--accent-glow)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'} >
{c.special ? c.special : ( inView ? : 0{c.suffix} )}
{c.label}
); })}
); }; // ===== PROBLEM SECTION ===== const ProblemSection = () => { const [ref, inView] = useInView(); return (
{/* Left: visual placeholder */}
{/* Duotone photo placeholder */}
{/* Diagonal stripes */}
Athlète en effort — photo à venir
Traitement duotone orange / bleu nuit
{/* Orange overlay effect */}
{/* Orange accent line */}
{/* Right: copy */}
Le problème

Vous transpirez plus.
Vous crampez plus.

Vous avez tout essayé : pastilles de sel, boissons iso, magnésium, étirements préventifs. Vous crampez quand même. Au km 35, dans le dernier round, dans la dernière côte.

Vous êtes le 1 sur 7. Et le marché vous a oublié.

{/* Pull quote */}

"67% des athlètes d'endurance crampent. Pour 1 sur 7, c'est récurrent et invalidant."

— Schwellnus, Br J Sports Med
); }; // Export all to window Object.assign(window, { useInView, useReducedMotion, AnimatedCounter, RevealOnScroll, SalvoButton, CustomCursor, Loader, Nav, ParticleCanvas, HeroSection, StatsBar, ProblemSection, });