// Landing page primitives + sections
// ─────────── Supabase : inscriptions à la bêta ───────────
// La clé "anon" est PUBLIQUE par conception : elle peut figurer dans le code.
// La sécurité vient de la règle (RLS) configurée dans Supabase, qui n'autorise
// QUE l'insertion (aucune lecture des inscriptions par le public).
// → Voir INSTALLATION-supabase.md pour créer la table et récupérer ces 2 valeurs.
const SUPABASE_URL = 'https://lmhhrccmgebztioesmik.supabase.co';
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxtaGhyY2NtZ2VienRpb2VzbWlrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjUzMjIyOTgsImV4cCI6MjA4MDg5ODI5OH0.epfoBIsZJHLqj96dYE7AvImK_EgjMW9PFtvLk4VwlDc';
// Nom EXACT de la table Supabase (respectez la casse / les majuscules).
const SUPABASE_TABLE = 'inscription_beta';
async function saveBetaSignup(data) {
const res = await fetch(SUPABASE_URL + '/rest/v1/' + SUPABASE_TABLE, {
method: 'POST',
headers: {
'apikey': SUPABASE_ANON_KEY,
'Authorization': 'Bearer ' + SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Prefer': 'return=minimal',
},
body: JSON.stringify(data),
});
if (!res.ok) {
const detail = await res.text().catch(() => '');
throw new Error('Supabase ' + res.status + ', ' + detail);
}
}
// Inscription à la newsletter Brevo (via la fonction Supabase « brevo-subscribe »).
// Best-effort : si ça échoue, l'inscription bêta reste valide (on n'interrompt pas l'utilisateur).
async function subscribeNewsletter({ email, prenom, nom, ville, role }) {
try {
await fetch(SUPABASE_URL + '/functions/v1/brevo-subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + SUPABASE_ANON_KEY,
'apikey': SUPABASE_ANON_KEY,
},
body: JSON.stringify({ email, prenom: prenom || '', nom: nom || '', ville: ville || '', role: role || null, consent: true }),
});
} catch (e) {
console.warn('[newsletter] inscription Brevo échouée (non bloquant)', e);
}
}
const ROLES = {
pilote: { emoji: '🏡', tint: '#dcefe7', short: 'Porteurs de lieu', name: "Pilote d'impact", verb: 'Coordonnez un lieu durable', persona: "Porteur·se d'un tiers-lieu, écolieu, ferme, association, incubateur…", pillText: "Coordonner un lieu durable et visible", desc: 'Tiers-lieu, écolieu, ferme, association, incubateur. Vous publiez des quêtes, accueillez des Bâtisseurs, certifiez les preuves d\'impact.', perks: ['Pilotez et financez vos projets avec des outils numériques intégrés', 'Rendez vos impacts mesurables et traçables, preuves à l\'appui', 'Transformez vos actions en données probantes pour ancrer votre modèle'], cta: 'Développer mon lieu', accent: '#018262', image: window.__resources.pilote },
batisseur: { emoji: '🌿', tint: '#fdf3e7', short: 'Citoyens', name: "Bâtisseur d'impact", verb: "Passez à l'action", persona: "Membre, particulier, étudiant, digital nomad, entrepreneur…", pillText: "Passer à l'action avec des avantages concrets", desc: 'Membre, particulier, étudiant, digital nomad, entrepreneur. Vous rejoignez des quêtes concrètes, contribuez, gagnez des graines à utiliser dans le réseau.', perks: ['Passez de l\'éco-anxiété à l\'éco-action, un pas à la fois', 'Soyez reconnu et récompensé : chaque action est valorisée', 'Avancez à votre rythme, porté par une communauté qui avance avec vous'], cta: 'Trouver ma quête', accent: '#c8732a', image: window.__resources.batisseur },
semeur: { emoji: '🌾', tint: '#e8f4f9', short: 'Financeurs', name: "Semeur d'impact", verb: 'Soutenez des projets durables', persona: "Financeur public/privé, investisseur, fondation, collectivité…", pillText: "Soutenir des projets durables certifiés", desc: 'Financeur public/privé, fondation, investisseur, collectivité. Vous financez des projets contre des preuves d\'impact certifiées.', perks: ['Identifiez les initiatives véritablement transformatrices', 'Assurez-vous d\'impacts mesurables, durables et transparents', 'Chaque acte investi est tracé, mesuré, et devient une graine'], cta: 'Soutenir des projets', accent: '#3a6e8c', image: window.__resources.semeur },
};
const Section = ({ id, eyebrow, title, sub, children, dark, narrow, padded = true }) => (
{eyebrow && (
{eyebrow}
)}
{title && (
{title}
)}
{sub && (
{sub}
)}
{children}
);
// ─────────────────── Hero ───────────────────
const Hero = ({ role, setRole, palette, persona, onChoose }) => {
const r = ROLES[role];
const [mode, setMode] = React.useState('signup'); // 'signup' | 'login'
const accent = palette === 'terracotta' ? '#c8732a' : palette === 'sky' ? '#3a6e8c' : '#018262';
React.useEffect(() => {
if (mode === 'login') {
document.getElementById('hero')?.scrollIntoView({ behavior: 'smooth' });
}
}, [mode]);
return (
setMode('login')} persona={persona} onChoose={onChoose}/>
Écosystème Vivant Autonome & Décentralisé
Imaginons un avenir durable et réalisons-le ensemble.
Du pixel à la terre, du rêve au lieu, de l'action à l'impact. Plongez dans un avenir solarpunk et rejoignez le mouvement qui transforme la transition écologique en économie régénérative.
document.getElementById('ecosystem').scrollIntoView({ behavior: 'smooth' })} style={{
padding: '16px 28px', background: accent, color: '#e8f7f3', border: 'none', borderRadius: 12,
fontFamily: "'Satoshi',sans-serif", fontSize: 15, fontWeight: 700, letterSpacing: '.02em', cursor: 'pointer',
boxShadow: '0 8px 24px ' + accent + '50', transition: 'transform .18s, box-shadow .18s',
}} onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 12px 28px ' + accent + '70'; }}
onMouseLeave={e => { e.currentTarget.style.transform = ''; e.currentTarget.style.boxShadow = '0 8px 24px ' + accent + '50'; }}>
En savoir plus ↓
);
};
const NAV_LINKS = [['#ecosystem', 'Solutions'], ['#roles', 'Profils'], ['#foundations', 'Piliers'], ['#cycle', 'Boucle'], ['#deva', 'Deva'], ['#association', "L'asso"], ['#agir', 'Nous soutenir'], ['#cta', 'Nous suivre']];
const NavBar = ({ accent, onLogin, persona, onChoose }) => {
const [scrolled, setScrolled] = React.useState(false);
const [overDark, setOverDark] = React.useState(false);
const [menuOpen, setMenuOpen] = React.useState(false);
const pr = persona && ROLES[persona];
React.useEffect(() => {
const onScroll = () => {
setScrolled(window.scrollY > 24);
setOverDark(document.documentElement.classList.contains('over-prologue'));
};
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
// Also watch the html class for changes the prologue sets after mount
const mo = new MutationObserver(() => setOverDark(document.documentElement.classList.contains('over-prologue')));
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => { window.removeEventListener('scroll', onScroll); mo.disconnect(); };
}, []);
// Close menu on scroll or escape
React.useEffect(() => {
if (!menuOpen) return;
const onKey = (e) => { if (e.key === 'Escape') setMenuOpen(false); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [menuOpen]);
const linkColor = overDark ? '#f5fbf8' : '#0d2b22';
const logoFill = overDark ? '#f5fbf8' : '#018262';
const burgerColor = overDark ? '#f5fbf8' : '#0d2b22';
return (
{NAV_LINKS.map(([h, l]) => (
{l}
))}
{pr && (
{ const el = document.getElementById('roles'); if (el) el.scrollIntoView({ behavior: 'smooth' }); }}
title="Changer de profil"
style={{
display: 'inline-flex', alignItems: 'center', gap: 7,
background: 'transparent', border: 'none', cursor: 'pointer', padding: 0,
fontFamily: "'Satoshi',sans-serif", fontSize: 12.5, fontWeight: 700,
color: overDark ? '#f5fbf8' : pr.accent,
}}>
{pr.emoji}
Vue {pr.short.toLowerCase()}
onChoose && onChoose(null)}
aria-label="Revenir à la vue générale"
title="Revenir à la vue générale"
style={{
width: 20, height: 20, borderRadius: '50%', flexShrink: 0,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
background: overDark ? 'rgba(13,43,34,.25)' : '#fff',
border: 'none', cursor: 'pointer', padding: 0,
color: overDark ? '#f5fbf8' : pr.accent, fontSize: 13, lineHeight: 1,
}}>×
)}
Se connecter
setMenuOpen(o => !o)}
aria-label={menuOpen ? 'Fermer le menu' : 'Ouvrir le menu'}
aria-expanded={menuOpen}
className="nav-burger"
style={{
display: 'none',
width: 40, height: 40, borderRadius: 10,
background: menuOpen ? 'rgba(1,130,98,.12)' : 'transparent',
border: '1px solid ' + (menuOpen ? 'rgba(1,130,98,.3)' : 'transparent'),
cursor: 'pointer', padding: 0,
alignItems: 'center', justifyContent: 'center',
transition: 'background .2s, border-color .2s',
}}>
{/* Mobile slide-down menu */}
{NAV_LINKS.map(([h, l]) => (
setMenuOpen(false)} style={{
padding: '14px 4px',
fontFamily: "'Satoshi', sans-serif", fontSize: 16, fontWeight: 600,
color: '#0d2b22',
borderBottom: '1px solid rgba(46,102,66,.08)',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}>
{l}
→
))}
{ setMenuOpen(false); onLogin(); }} style={{
marginTop: 18, padding: '14px 18px',
background: accent, color: '#e8f7f3', border: 'none',
borderRadius: 12,
fontFamily: "'Satoshi', sans-serif", fontSize: 14, fontWeight: 700,
cursor: 'pointer',
boxShadow: '0 8px 20px ' + accent + '40',
}}>Se connecter
);
};
const Logo = ({ width = 100, fill = '#018262' }) => (
);
const HeroVisual = ({ role, setRole, mode, setMode }) => {
const r = ROLES[role];
return (
Bienvenue dans l'écosystème
{mode === 'login' ? (
setMode('signup')}/>
) : (
setMode('login')}/>
)}
);
};
const SignupContent = ({ role, setRole, r, onLogin }) => {
const [chosen, setChosen] = React.useState(false);
const [sent, setSent] = React.useState(false);
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState(null);
const inputStyle = {
width: '100%', padding: '14px 16px',
background: '#fff', border: '1px solid rgba(46,102,66,.16)', borderRadius: 12,
fontFamily: "'Satoshi',sans-serif", fontSize: 14, color: '#0d2b22',
outline: 'none', transition: 'border-color .15s, box-shadow .15s',
boxSizing: 'border-box',
};
const focusIn = e => { e.currentTarget.style.borderColor = r.accent; e.currentTarget.style.boxShadow = '0 0 0 3px ' + r.accent + '22'; };
const focusOut = e => { e.currentTarget.style.borderColor = 'rgba(46,102,66,.16)'; e.currentTarget.style.boxShadow = 'none'; };
// ─── FORM VIEW (a profile has been chosen) ───
if (chosen) {
return (
<>
Rejoignez la bêta :
{/* Chosen profile recap + changer */}
{r.emoji}
{!sent && (
{ setChosen(false); setSent(false); }} style={{ background: 'none', border: 'none', padding: 0, color: r.accent, fontWeight: 700, cursor: 'pointer', fontFamily: "'Satoshi',sans-serif", fontSize: 12, flexShrink: 0 }}>Changer
)}
{sent ? (
🌱
Merci pour votre inscription !
Votre demande d'accès {r.name} est bien enregistrée. Nous vous contacterons dès l'ouverture de la bêta, en octobre 2026, et vous serez convié·e à notre événement de lancement .
) : (
)}
Déjà membre ? Se connecter
>
);
}
// ─── SELECTION VIEW (pick a profile) ───
return (
<>
S'inscrire à la bêta
Notre prototype ouvre en octobre 2026 . Inscrivez-vous pour le tester en avant-première, être convié·e à notre événement de lancement et nous aider à le faire grandir avec vos retours.
{Object.entries(ROLES).map(([id, x]) => {
const isSel = role === id;
return (
{ setRole(id); setChosen(true); setSent(false); }} style={{
display: 'flex', alignItems: 'flex-start', gap: 14, padding: '16px 18px',
border: '1px solid ' + (isSel ? x.accent : 'rgba(46,102,66,.14)'),
background: isSel ? 'rgba(1,130,98,.04)' : '#fff',
borderRadius: 14, marginBottom: 10, cursor: 'pointer',
boxShadow: isSel ? '0 8px 24px ' + x.accent + '30' : 'none',
transition: 'all .25s',
}}>
{x.emoji}
{x.short}
{x.name}
{x.persona}
{x.pillText}
);
})}
Déjà membre ? Se connecter
>
);
};
const LoginForm = ({ onBack }) => {
const inputStyle = {
width: '100%', padding: '14px 16px',
background: '#fff', border: '1px solid rgba(46,102,66,.16)', borderRadius: 12,
fontFamily: "'Satoshi',sans-serif", fontSize: 14, color: '#0d2b22',
outline: 'none', transition: 'border-color .15s, box-shadow .15s',
boxSizing: 'border-box',
};
return (
<>
Se connecter
Bon retour parmi nous.
Pas encore de compte ? S'inscrire à la bêta
>
);
};
window.Hero = Hero;
window.Section = Section;
window.ROLES = ROLES;
window.Logo = Logo;