// Landing, CTA + Footer + App
const BackToTop = () => {
const [show, setShow] = React.useState(false);
React.useEffect(() => {
const onScroll = () => setShow(window.scrollY > 600);
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
{
const el = document.getElementById('inscription');
if (el) {
const y = el.getBoundingClientRect().top + window.scrollY - 90;
window.scrollTo({ top: Math.max(0, y), behavior: 'smooth' });
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}}
aria-label="Revenir à l'inscription"
style={{
position: 'fixed', bottom: 28, right: 28, zIndex: 80,
width: 52, height: 52, borderRadius: '50%',
background: '#018262', color: '#e8f7f3', border: 'none',
boxShadow: '0 12px 28px rgba(13,43,34,.3), 0 4px 10px rgba(1,130,98,.25)',
cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 22, lineHeight: 1,
opacity: show ? 1 : 0,
transform: show ? 'translateY(0)' : 'translateY(12px)',
pointerEvents: show ? 'auto' : 'none',
transition: 'opacity .25s ease, transform .25s ease, background .2s',
}}
onMouseEnter={e => { if (show) e.currentTarget.style.background = '#016b52'; }}
onMouseLeave={e => { e.currentTarget.style.background = '#018262'; }}>
);
};
const SocialBtn = ({ href, label, children }) => (
{ e.currentTarget.style.background = 'rgba(232,247,243,.2)'; e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.borderColor = 'rgba(207,238,231,.4)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(232,247,243,.1)'; e.currentTarget.style.transform = ''; e.currentTarget.style.borderColor = 'rgba(207,238,231,.22)'; }}>
{children}
);
const NEWSLETTER_BY_ROLE = {
default: {
eyebrow: 'Quatre fois par an',
titleA: 'Quatre fois par an,',
titleB: 'au rythme des saisons.',
pitch: "Une lettre de saison qui raconte ce qui pousse dans l'écosystème : les nouveaux lieux, les quêtes en cours, ce qu'on a appris, ce qui se sème. Lent, soigné, sans tracker.",
btn: "🌱 S'inscrire à la newsletter",
socialTitle: 'Suivez-nous,',
socialItalic: 'écrivez-nous.',
},
pilote: {
eyebrow: 'Pour votre lieu',
titleA: 'Pour votre lieu,',
titleB: 'un rendez-vous saisonnier.',
pitch: "Recevez chaque saison les nouveaux modules, fiches techniques et retours d'expérience d'autres lieux. De quoi nourrir vos chantiers et faire grandir votre projet sans bruit.",
btn: "🏡 Recevoir la lettre des lieux",
socialTitle: 'Rejoignez les autres pilotes,',
socialItalic: 'partagez vos réussites.',
},
batisseur: {
eyebrow: 'Pour passer à l\'action',
titleA: 'Vos quêtes de saison,',
titleB: 'directement dans votre boîte.',
pitch: "Recevez les quêtes ouvertes près de chez vous, les nouveaux lieux à découvrir et les bons plans graines. Un email par saison, sans tracker.",
btn: "🌿 Recevoir mes quêtes",
socialTitle: 'Vibrez avec la communauté,',
socialItalic: 'suivez les chantiers du réseau.',
},
semeur: {
eyebrow: 'Pour suivre l\'impact',
titleA: 'Le rapport saisonnier,',
titleB: "pour mesurer l'impact.",
pitch: "Recevez chaque saison les chiffres du réseau : graines en circulation, quêtes accomplies, preuves validées, Vadité des lieux. De quoi orienter vos financements en toute connaissance.",
btn: "🌾 Recevoir le rapport d'impact",
socialTitle: 'Suivez les projets que vous soutenez,',
socialItalic: 'partagez avec votre réseau.',
},
};
// ─────────── Newsletter : inscription Brevo via fonction Supabase ───────────
// La clé API Brevo reste SECRÈTE côté serveur (variable BREVO_API_KEY dans la
// fonction Edge « brevo-subscribe »). Le site n'appelle que cette fonction.
// → Voir INSTALLATION-newsletter-brevo.md.
const NL_SUPABASE_URL = 'https://lmhhrccmgebztioesmik.supabase.co';
const NL_SUPABASE_ANON = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxtaGhyY2NtZ2VienRpb2VzbWlrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjUzMjIyOTgsImV4cCI6MjA4MDg5ODI5OH0.epfoBIsZJHLqj96dYE7AvImK_EgjMW9PFtvLk4VwlDc';
const NL_ENDPOINT = NL_SUPABASE_URL + '/functions/v1/brevo-subscribe';
const CTASection = ({ role }) => {
const copy = (role && NEWSLETTER_BY_ROLE[role]) || NEWSLETTER_BY_ROLE.default;
const [prenom, setPrenom] = React.useState('');
const [email, setEmail] = React.useState('');
const [consent, setConsent] = React.useState(false);
const [status, setStatus] = React.useState('idle'); // idle | loading | ok | error
const [errMsg, setErrMsg] = React.useState('');
const submit = async (e) => {
if (e) e.preventDefault();
if (status === 'loading') return;
const mail = email.trim();
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(mail)) {
setStatus('error'); setErrMsg('Merci d’indiquer un email valide.');
return;
}
if (!consent) {
setStatus('error'); setErrMsg('Merci de cocher la case pour accepter de recevoir la newsletter.');
return;
}
setStatus('loading'); setErrMsg('');
try {
const res = await fetch(NL_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + NL_SUPABASE_ANON,
'apikey': NL_SUPABASE_ANON,
},
body: JSON.stringify({ email: mail, prenom: prenom.trim(), role: role || null, consent: true }),
});
if (!res.ok) {
let msg = 'Une erreur est survenue. Réessayez dans un instant.';
let raw = '';
try { raw = await res.text(); const j = JSON.parse(raw); if (j && j.error) msg = j.error; } catch {}
console.error('[newsletter] HTTP', res.status, 'depuis', NL_ENDPOINT, '→', raw);
setStatus('error'); setErrMsg(msg);
return;
}
setStatus('ok'); setPrenom(''); setEmail(''); setConsent(false);
} catch {
setStatus('error'); setErrMsg('Connexion impossible. Vérifiez votre réseau.');
}
};
return (
💭 ➡️ 🏡
{copy.titleA}
{copy.titleB}
{copy.pitch}
{status === 'ok' ? (
🌱
Merci, c’est noté !
Vous êtes bien inscrit·e à la newsletter. À très vite dans votre boîte mail.
) : (
)}
{status !== 'ok' && (
{ setConsent(e.target.checked); if (e.target.checked && status === 'error') setStatus('idle'); }} style={{
width: 18, height: 18, marginTop: 1, flexShrink: 0, accentColor: '#7ec9b0', cursor: 'pointer',
}}/>
J’accepte de recevoir la newsletter d’EVAD par email et que mon adresse soit utilisée à cette fin. Je peux me désinscrire à tout moment.
)}
{status === 'error' && (
{errMsg}
)}
Pas de spam, pas de tracker, désinscription en un clic.
{/* ─── Réseaux sociaux + contact ─── */}
✓ Association d'intérêt général
✓ Licence Creative Commons
✓ Données souveraines
);
};
const Footer = () => (
{/* Atmospheric glow */}
Écosystème Vivant Autonome & Décentralisé
Imaginons un avenir durable et réalisons-le ensemble.
{[
{ t: 'Découvrir', l: [
['Les profils', '#roles'],
['Les solutions', '#ecosystem'],
['Les piliers', '#foundations'],
['La spirale VADE', '#cycle'],
['Vadance & Vadité', '#vadite'],
['Deva', '#deva'],
] },
{ t: "L'association", l: [
["L'objet", '#association'],
['Devenir membre', 'https://www.helloasso.com/associations/evad-connect/adhesions/devenir-membre-2026'],
['Conseil Régénératif', 'https://compte.evad.org/apps/forms/s/3YxDgfMCdPGf8gPyN7bmdM4w'],
['Nous contacter', 'mailto:contact@evad.org'],
] },
{ t: 'Agir', l: [
['Faire un don défiscalisé', 'https://www.helloasso.com/associations/evad-connect/formulaires/1'],
['Soutenir en mécène', "mailto:contact@evad.org?subject=Devenir%20mécène%20d'EVAD"],
['Coopérer avec nous', 'https://compte.evad.org/apps/forms/s/MFdHsmKzPo9PSK3KjWo4czWn'],
] },
{ t: 'Suivre', l: [
['Newsletter', '#cta'],
['Nous suivre', '#suivre'],
] },
].map(c => (
))}
{/* Empreinte numérique du site · esprit Vadité */}
🌍 Empreinte de ce site
Nous mesurons aussi notre propre impact numérique.
{[
{ lbl: 'Création du site', sub: 'conception + développement · ponctuel', e: '≈ 1,5 kWh', c: '≈ 0,5 kg CO₂e' },
{ lbl: 'Chaque visite du site', sub: '~1,5 Mo transférés', e: '≈ 1,3 Wh', c: '≈ 0,5 g CO₂e' },
].map(m => (
{m.lbl}
{m.sub}
⚡ {m.e}
🌫️ {m.c}
))}
Estimations à titre indicatif (ordre de grandeur), dans une démarche de sobriété numérique en amélioration continue.
{/* Social row */}
);
// ─────────────────── Bandeau cookies (strictement nécessaires) ───────────────────
const CookieNotice = () => {
const [show, setShow] = React.useState(false);
React.useEffect(() => {
try { if (localStorage.getItem('evad.cookies.ack') !== '1') setShow(true); } catch { setShow(true); }
}, []);
const dismiss = () => {
try { localStorage.setItem('evad.cookies.ack', '1'); } catch {}
setShow(false);
};
if (!show) return null;
return (
🍪
Ce site n'utilise que des cookies strictement nécessaires à son
fonctionnement. Aucun traceur publicitaire ni statistique : rien à accepter ou refuser.{' '}
En savoir plus
{ e.currentTarget.style.background = '#a8e6cf'; }}
onMouseLeave={e => { e.currentTarget.style.background = '#7ec9b0'; }}>
J'ai compris
);
};
// ─────────────────── App ───────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"heroRole": "pilote",
"density": "comfortable",
"palette": "forest",
"teamLayout": "directory"
}/*EDITMODE-END*/;
const App = () => {
const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);
const [role, setRole] = React.useState(tweaks.heroRole);
// Persona = explicitly chosen via Deva chat onboarding. Null when reset.
const [persona, setPersona] = React.useState(() => {
try { const v = localStorage.getItem('evad.deva.persona'); return (v && v !== 'null') ? v : null; } catch { return null; }
});
React.useEffect(() => { setRole(tweaks.heroRole); }, [tweaks.heroRole]);
// Personnalisation du site depuis le prologue (choix de profil)
const choosePersona = (id) => {
if (id) {
try { localStorage.setItem('evad.deva.persona', id); } catch {}
setPersona(id);
setRole(id);
setTweak('heroRole', id);
} else {
try { localStorage.removeItem('evad.deva.persona'); } catch {}
setPersona(null);
}
};
// density CSS var
const secPad = tweaks.density === 'compact' ? '64px' : tweaks.density === 'airy' ? '128px' : '96px';
// observe-in animation
React.useEffect(() => {
const els = document.querySelectorAll('[data-fade]');
const io = new IntersectionObserver((entries) => {
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('in'); });
}, { threshold: 0.12 });
els.forEach(el => io.observe(el));
return () => io.disconnect();
}, []);
return (
{ setRole(r); setTweak('heroRole', r); }} palette={tweaks.palette} persona={persona} onChoose={choosePersona}/>
{ setRole(r); setTweak('heroRole', r); }}
onPersonaChange={setPersona}
/>
setTweak('heroRole', v)}
options={[
{ value: 'pilote', label: '🏡 Pilote' },
{ value: 'batisseur', label: '🌿 Bâtisseur' },
{ value: 'semeur', label: '🌾 Semeur' },
]}
/>
setTweak('palette', v)}
options={[
{ value: 'forest', label: 'Forest' },
{ value: 'terracotta', label: 'Terracotta' },
{ value: 'sky', label: 'Sky' },
]}
/>
setTweak('density', v)}
options={[
{ value: 'compact', label: 'Compact' },
{ value: 'comfortable', label: 'Confort' },
{ value: 'airy', label: 'Aéré' },
]}
/>
setTweak('teamLayout', v)}
options={[
{ value: 'directory', label: 'Annuaire' },
{ value: 'cards', label: 'Cartes' },
]}
/>
);
};
ReactDOM.createRoot(document.getElementById('root')).render( );