feat: add pre-launch landing page at /launch
Sumi-e ink wash aesthetic landing page with: - Hero section with Talas branding and email capture - Three value proposition cards (Open Hardware, Ethical Platform, Community) - Condenser microphone product teaser - Veza platform feature grid - Bottom CTA with email subscription (POST /api/v1/newsletter/subscribe) - Framer Motion scroll-triggered animations - Fully responsive, accessible, public route (no auth required) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
72fb90d70f
commit
86a8d1c357
4 changed files with 657 additions and 0 deletions
|
|
@ -53,4 +53,5 @@ export {
|
|||
LazyDistribution,
|
||||
LazyEducation,
|
||||
LazySupport,
|
||||
LazyLanding,
|
||||
} from './lazyExports';
|
||||
|
|
|
|||
|
|
@ -351,3 +351,9 @@ export const LazySupport = createLazyComponent(
|
|||
undefined,
|
||||
'Support',
|
||||
);
|
||||
// Pre-launch landing page
|
||||
export const LazyLanding = createLazyComponent(
|
||||
() => import('@/features/landing/pages/LandingPage'),
|
||||
undefined,
|
||||
'Landing',
|
||||
);
|
||||
|
|
|
|||
648
apps/web/src/features/landing/pages/LandingPage.tsx
Normal file
648
apps/web/src/features/landing/pages/LandingPage.tsx
Normal file
|
|
@ -0,0 +1,648 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import {
|
||||
Mic,
|
||||
Shield,
|
||||
Users,
|
||||
ArrowRight,
|
||||
Check,
|
||||
Wrench,
|
||||
Eye,
|
||||
Lock,
|
||||
Music,
|
||||
Globe,
|
||||
Heart,
|
||||
} from 'lucide-react';
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
TALAS LANDING PAGE — Pre-launch
|
||||
Aesthetic: Sumi-e ink wash (墨の濃淡) — scroll unfurling
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
const inkReveal = {
|
||||
hidden: { opacity: 0, y: 20, filter: 'blur(8px)' },
|
||||
visible: (i: number) => ({
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
filter: 'blur(0px)',
|
||||
transition: { delay: i * 0.12, duration: 0.7, ease: [0.25, 0.1, 0.25, 1] },
|
||||
}),
|
||||
};
|
||||
|
||||
const brushStroke = {
|
||||
hidden: { scaleX: 0, originX: 0 },
|
||||
visible: {
|
||||
scaleX: 1,
|
||||
transition: { duration: 0.8, ease: [0.33, 1, 0.68, 1] },
|
||||
},
|
||||
};
|
||||
|
||||
function Section({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
||||
const ref = useRef(null);
|
||||
const inView = useInView(ref, { once: true, margin: '-60px' });
|
||||
return (
|
||||
<motion.section
|
||||
ref={ref}
|
||||
initial="hidden"
|
||||
animate={inView ? 'visible' : 'hidden'}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LandingPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
const handleSubscribe = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!email || !email.includes('@')) return;
|
||||
setStatus('loading');
|
||||
try {
|
||||
const res = await fetch('/api/v1/newsletter/subscribe', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
throw new Error(data?.error?.message || 'Subscription failed');
|
||||
}
|
||||
setStatus('success');
|
||||
setEmail('');
|
||||
} catch (err) {
|
||||
setStatus('error');
|
||||
setErrorMsg(err instanceof Error ? err.message : 'An error occurred');
|
||||
setTimeout(() => setStatus('idle'), 4000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--sumi-bg-void)] text-[var(--sumi-text-primary)] overflow-x-hidden">
|
||||
{/* ═══ ATMOSPHERE — Ink wash background ═══ */}
|
||||
<div className="fixed inset-0 pointer-events-none" aria-hidden="true">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(ellipse at 15% 20%, rgba(184,58,30, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 85% 60%, rgba(184,134,11, 0.03) 0%, transparent 45%),
|
||||
radial-gradient(ellipse at 50% 90%, rgba(79,104,64, 0.02) 0%, transparent 40%)
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
{/* Ink drip streaks */}
|
||||
<div className="absolute top-0 left-[10%] w-px h-[60%] bg-gradient-to-b from-[var(--sumi-border-strong)] to-transparent opacity-20" />
|
||||
<div className="absolute top-0 left-[70%] w-px h-[35%] bg-gradient-to-b from-[var(--sumi-border-default)] to-transparent opacity-15" />
|
||||
<div className="absolute top-0 left-[42%] w-px h-[80%] bg-gradient-to-b from-[var(--sumi-border-faint)] to-transparent opacity-10" />
|
||||
<div className="absolute top-0 right-[22%] w-px h-[45%] bg-gradient-to-b from-[var(--sumi-border-default)] to-transparent opacity-12" />
|
||||
|
||||
{/* Ghost kanji */}
|
||||
<span className="ghost-kanji left-[5%] top-[8%] text-[220px] opacity-[0.015]">音</span>
|
||||
<span className="ghost-kanji right-[8%] top-[35%] text-[160px] opacity-[0.012]">響</span>
|
||||
<span className="ghost-kanji left-[15%] bottom-[15%] text-[200px] opacity-[0.01]">創</span>
|
||||
<span className="ghost-kanji right-[20%] bottom-[40%] text-[140px] opacity-[0.018]">真</span>
|
||||
</div>
|
||||
|
||||
{/* ═══ NAVIGATION ═══ */}
|
||||
<nav className="fixed top-0 inset-x-0 z-50 border-b border-[var(--sumi-border-faint)] bg-[var(--sumi-bg-void)]/80 backdrop-blur-xl">
|
||||
<div className="max-w-[1200px] mx-auto px-6 h-14 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded-sm bg-[var(--sumi-accent)] flex items-center justify-center hanko-seal" style={{ transform: 'rotate(-2deg)' }}>
|
||||
<span className="text-[var(--sumi-bg-base)] font-[var(--sumi-font-heading)] text-sm font-semibold relative z-10">T</span>
|
||||
</div>
|
||||
<span
|
||||
className="text-lg tracking-[0.2em] text-[var(--sumi-text-primary)]"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 200 }}
|
||||
>
|
||||
TALAS
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<a
|
||||
href="#product"
|
||||
className="text-xs tracking-[0.15em] text-[var(--sumi-text-tertiary)] hover:text-[var(--sumi-text-primary)] transition-colors"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}
|
||||
>
|
||||
PRODUIT
|
||||
</a>
|
||||
<a
|
||||
href="#platform"
|
||||
className="text-xs tracking-[0.15em] text-[var(--sumi-text-tertiary)] hover:text-[var(--sumi-text-primary)] transition-colors"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}
|
||||
>
|
||||
PLATEFORME
|
||||
</a>
|
||||
<Link
|
||||
to="/login"
|
||||
className="text-xs tracking-[0.1em] px-4 py-1.5 border border-[var(--sumi-border-strong)] rounded-sm text-[var(--sumi-text-secondary)] hover:text-[var(--sumi-text-primary)] hover:border-[var(--sumi-accent)]/40 transition-all"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}
|
||||
>
|
||||
CONNEXION
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* ═══ HERO ═══ */}
|
||||
<header className="relative z-10 min-h-screen flex flex-col items-center justify-center px-6 pt-14">
|
||||
<motion.div
|
||||
className="max-w-[800px] mx-auto text-center"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{/* Hanko seal */}
|
||||
<motion.div variants={inkReveal} custom={0} className="flex justify-center mb-10">
|
||||
<div className="h-20 w-20 rounded-sm bg-[var(--sumi-accent)] flex items-center justify-center hanko-seal">
|
||||
<span
|
||||
className="text-[var(--sumi-bg-base)] text-3xl relative z-10"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 600 }}
|
||||
>
|
||||
T
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Title */}
|
||||
<motion.h1
|
||||
variants={inkReveal}
|
||||
custom={1}
|
||||
className="text-5xl sm:text-7xl tracking-[0.15em] mb-3"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 200 }}
|
||||
>
|
||||
TALAS
|
||||
</motion.h1>
|
||||
|
||||
<motion.div variants={brushStroke} className="w-24 h-px bg-gradient-to-r from-transparent via-[var(--sumi-kin)]/40 to-transparent mx-auto mb-8" />
|
||||
|
||||
<motion.p
|
||||
variants={inkReveal}
|
||||
custom={2}
|
||||
className="text-base sm:text-lg text-[var(--sumi-text-secondary)] leading-relaxed max-w-[550px] mx-auto mb-4"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}
|
||||
>
|
||||
Matériel audio professionnel — ouvert, réparable, transparent.
|
||||
<br />
|
||||
Plateforme musicale éthique — sans tracking, sans algorithme.
|
||||
</motion.p>
|
||||
|
||||
<motion.p
|
||||
variants={inkReveal}
|
||||
custom={3}
|
||||
className="text-xs tracking-[0.25em] text-[var(--sumi-text-tertiary)] uppercase mb-12"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}
|
||||
>
|
||||
Lancement bientôt — Rejoins les premiers
|
||||
</motion.p>
|
||||
|
||||
{/* Email capture — hero */}
|
||||
<motion.form
|
||||
variants={inkReveal}
|
||||
custom={4}
|
||||
onSubmit={handleSubscribe}
|
||||
className="flex flex-col sm:flex-row items-center gap-3 max-w-[460px] mx-auto"
|
||||
>
|
||||
{status === 'success' ? (
|
||||
<div className="flex items-center gap-2 text-[var(--sumi-sage)] text-sm py-3">
|
||||
<Check size={16} />
|
||||
<span style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}>
|
||||
Inscription confirmée. À bientôt.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="ton@email.com"
|
||||
required
|
||||
aria-label="Adresse email"
|
||||
className="flex-1 w-full sm:w-auto px-4 py-2.5 bg-[var(--sumi-surface-card)] border border-[var(--sumi-border-default)] rounded-sm text-sm text-[var(--sumi-text-primary)] placeholder:text-[var(--sumi-text-disabled)] focus:outline-none focus:border-[var(--sumi-accent)]/50 focus:shadow-[var(--sumi-shadow-glow)] transition-all"
|
||||
style={{ fontFamily: 'var(--sumi-font-body)' }}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === 'loading'}
|
||||
className="w-full sm:w-auto px-6 py-2.5 bg-[var(--sumi-accent)] text-[var(--sumi-text-inverse)] rounded-sm text-sm tracking-[0.1em] hover:bg-[var(--sumi-accent-hover)] disabled:opacity-50 transition-colors flex items-center justify-center gap-2"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 400 }}
|
||||
>
|
||||
{status === 'loading' ? (
|
||||
<span className="h-4 w-4 border border-current border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
REJOINDRE
|
||||
<ArrowRight size={14} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</motion.form>
|
||||
|
||||
{status === 'error' && (
|
||||
<p className="text-xs text-[var(--sumi-vermillion)] mt-2">{errorMsg}</p>
|
||||
)}
|
||||
|
||||
{/* Scroll hint */}
|
||||
<motion.div
|
||||
variants={inkReveal}
|
||||
custom={6}
|
||||
className="absolute bottom-12 left-1/2 -translate-x-1/2"
|
||||
>
|
||||
<div className="w-px h-12 bg-gradient-to-b from-[var(--sumi-border-strong)] to-transparent mx-auto mb-2" />
|
||||
<span className="text-[10px] tracking-[0.3em] text-[var(--sumi-text-disabled)]" style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}>
|
||||
DÉCOUVRIR
|
||||
</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</header>
|
||||
|
||||
{/* ═══ VALUES — Three pillars ═══ */}
|
||||
<Section className="relative z-10 py-32 px-6">
|
||||
<div className="max-w-[1000px] mx-auto">
|
||||
<motion.div variants={inkReveal} custom={0} className="text-center mb-20">
|
||||
<span className="text-[10px] tracking-[0.3em] text-[var(--sumi-kin)] uppercase" style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}>
|
||||
三つの柱
|
||||
</span>
|
||||
<h2
|
||||
className="text-3xl sm:text-4xl mt-3 tracking-[0.05em]"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 200 }}
|
||||
>
|
||||
Trois engagements
|
||||
</h2>
|
||||
<div className="w-16 h-px bg-gradient-to-r from-transparent via-[var(--sumi-kin)]/30 to-transparent mx-auto mt-6" />
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
icon: Wrench,
|
||||
kanji: '開',
|
||||
title: 'Hardware Ouvert',
|
||||
desc: 'Schémas publiés sous licence CERN-OHL. Tu peux construire, réparer et améliorer chaque composant. Pas d\'obsolescence programmée.',
|
||||
accent: 'var(--sumi-accent)',
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
kanji: '守',
|
||||
title: 'Plateforme Éthique',
|
||||
desc: 'Zéro tracking comportemental. Zéro algorithme de manipulation. Flux chronologique. Données privées. Code open-source (AGPL-3.0).',
|
||||
accent: 'var(--sumi-sage)',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
kanji: '結',
|
||||
title: 'Communauté Artiste',
|
||||
desc: 'Streaming, marketplace, chat en temps réel, playlists collaboratives. Rémunération transparente. Les artistes contrôlent leur musique.',
|
||||
accent: 'var(--sumi-kin)',
|
||||
},
|
||||
].map((card, i) => (
|
||||
<motion.div
|
||||
key={card.title}
|
||||
variants={inkReveal}
|
||||
custom={i + 1}
|
||||
className="ink-card p-8 bg-[var(--sumi-surface-card)]/60 backdrop-blur-sm border border-[var(--sumi-border-faint)] group hover:border-[var(--sumi-border-strong)] transition-all duration-500"
|
||||
>
|
||||
{/* Ghost kanji in card */}
|
||||
<span
|
||||
className="absolute -right-2 -top-4 text-[100px] opacity-[0.025] pointer-events-none select-none"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 200 }}
|
||||
>
|
||||
{card.kanji}
|
||||
</span>
|
||||
|
||||
<div
|
||||
className="w-10 h-10 rounded-sm flex items-center justify-center mb-6 transition-all duration-500"
|
||||
style={{ backgroundColor: `color-mix(in srgb, ${card.accent} 12%, transparent)` }}
|
||||
>
|
||||
<card.icon size={18} style={{ color: card.accent }} strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<h3
|
||||
className="text-lg mb-3 tracking-[0.03em]"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 400 }}
|
||||
>
|
||||
{card.title}
|
||||
</h3>
|
||||
|
||||
<p
|
||||
className="text-sm text-[var(--sumi-text-secondary)] leading-relaxed"
|
||||
style={{ fontFamily: 'var(--sumi-font-body)', fontWeight: 400 }}
|
||||
>
|
||||
{card.desc}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ═══ PRODUCT TEASER — Condenser microphone ═══ */}
|
||||
<Section className="relative z-10 py-32 px-6" id="product">
|
||||
<div className="max-w-[1000px] mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
|
||||
{/* Microphone illustration — abstract */}
|
||||
<motion.div
|
||||
variants={inkReveal}
|
||||
custom={0}
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<div className="relative w-[280px] h-[380px]">
|
||||
{/* Ink wash backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 rounded-sm"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(ellipse at 50% 40%, rgba(184,58,30, 0.06) 0%, transparent 60%),
|
||||
linear-gradient(180deg, var(--sumi-surface-card) 0%, transparent 100%)
|
||||
`,
|
||||
border: '1px solid var(--sumi-border-faint)',
|
||||
}}
|
||||
/>
|
||||
{/* Microphone silhouette */}
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<Mic size={80} strokeWidth={0.8} className="text-[var(--sumi-text-tertiary)] mb-6" />
|
||||
<div className="w-px h-20 bg-gradient-to-b from-[var(--sumi-text-tertiary)]/30 to-transparent" />
|
||||
<div className="w-12 h-1 rounded-full bg-[var(--sumi-text-tertiary)]/15 mt-1" />
|
||||
</div>
|
||||
{/* Gold accent corner */}
|
||||
<div className="absolute top-4 right-4 w-8 h-px bg-[var(--sumi-kin)]/30" />
|
||||
<div className="absolute top-4 right-4 w-px h-8 bg-[var(--sumi-kin)]/30" />
|
||||
{/* Seal */}
|
||||
<div className="absolute bottom-6 right-6 h-10 w-10 rounded-sm bg-[var(--sumi-accent)]/10 border border-[var(--sumi-accent)]/20 flex items-center justify-center">
|
||||
<span className="text-[var(--sumi-accent)] text-xs" style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 500 }}>T</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Product details */}
|
||||
<div>
|
||||
<motion.span
|
||||
variants={inkReveal}
|
||||
custom={0}
|
||||
className="text-[10px] tracking-[0.3em] text-[var(--sumi-kin)] uppercase"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}
|
||||
>
|
||||
Premier produit
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
variants={inkReveal}
|
||||
custom={1}
|
||||
className="text-3xl sm:text-4xl mt-3 mb-2 tracking-[0.03em]"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 200 }}
|
||||
>
|
||||
Microphone Condensateur
|
||||
</motion.h2>
|
||||
|
||||
<motion.div variants={brushStroke} className="w-20 h-px bg-gradient-to-r from-[var(--sumi-accent)]/40 to-transparent mb-8" />
|
||||
|
||||
<motion.p
|
||||
variants={inkReveal}
|
||||
custom={2}
|
||||
className="text-sm text-[var(--sumi-text-secondary)] leading-relaxed mb-8"
|
||||
style={{ fontFamily: 'var(--sumi-font-body)' }}
|
||||
>
|
||||
Large diaphragme. Préampli OPA1642. Corps aluminium usiné.
|
||||
Schémas publiés, composants standards, guide de réparation inclus dans la boîte.
|
||||
30 à 40% moins cher que les concurrents — sans compromis sur la qualité.
|
||||
</motion.p>
|
||||
|
||||
<motion.ul variants={inkReveal} custom={3} className="space-y-3 mb-10">
|
||||
{[
|
||||
{ icon: Eye, text: 'Schémas KiCAD publiés — CERN-OHL-W' },
|
||||
{ icon: Wrench, text: 'Réparable — pas de colle, composants standards' },
|
||||
{ icon: Globe, text: 'Fabriqué en France — sourcing documenté' },
|
||||
{ icon: Lock, text: '120-180 € — transparence totale des coûts' },
|
||||
].map((item) => (
|
||||
<li key={item.text} className="flex items-start gap-3 text-sm text-[var(--sumi-text-secondary)]">
|
||||
<item.icon size={15} className="text-[var(--sumi-accent)] mt-0.5 shrink-0" strokeWidth={1.5} />
|
||||
<span style={{ fontFamily: 'var(--sumi-font-body)' }}>{item.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</motion.ul>
|
||||
|
||||
<motion.div variants={inkReveal} custom={4}>
|
||||
<a
|
||||
href="#notify"
|
||||
className="inline-flex items-center gap-2 text-xs tracking-[0.15em] text-[var(--sumi-accent)] hover:text-[var(--sumi-accent-hover)] transition-colors"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 400 }}
|
||||
>
|
||||
ÊTRE NOTIFIÉ DU LANCEMENT
|
||||
<ArrowRight size={13} />
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ═══ PLATFORM TEASER — Veza ═══ */}
|
||||
<Section className="relative z-10 py-32 px-6" id="platform">
|
||||
<div className="max-w-[1000px] mx-auto text-center">
|
||||
<motion.span
|
||||
variants={inkReveal}
|
||||
custom={0}
|
||||
className="text-[10px] tracking-[0.3em] text-[var(--sumi-kin)] uppercase"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}
|
||||
>
|
||||
La plateforme
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
variants={inkReveal}
|
||||
custom={1}
|
||||
className="text-3xl sm:text-4xl mt-3 mb-2 tracking-[0.15em]"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 200 }}
|
||||
>
|
||||
VEZA
|
||||
</motion.h2>
|
||||
|
||||
<motion.div variants={brushStroke} className="w-16 h-px bg-gradient-to-r from-transparent via-[var(--sumi-kin)]/30 to-transparent mx-auto mb-6" />
|
||||
|
||||
<motion.p
|
||||
variants={inkReveal}
|
||||
custom={2}
|
||||
className="text-xs tracking-[0.2em] text-[var(--sumi-text-tertiary)] mb-12"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}
|
||||
>
|
||||
墨 STREAMING — DU MICRO À L'AUDITEUR
|
||||
</motion.p>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-6 max-w-[700px] mx-auto mb-16">
|
||||
{[
|
||||
{ icon: Music, label: 'Streaming HLS' },
|
||||
{ icon: Users, label: 'Communauté' },
|
||||
{ icon: Heart, label: 'Marketplace' },
|
||||
{ icon: Lock, label: 'Vie privée' },
|
||||
{ icon: Globe, label: 'Open source' },
|
||||
{ icon: Shield, label: 'Zéro tracking' },
|
||||
].map((feat, i) => (
|
||||
<motion.div
|
||||
key={feat.label}
|
||||
variants={inkReveal}
|
||||
custom={i * 0.5 + 3}
|
||||
className="flex flex-col items-center gap-3 py-6 px-4 rounded-sm border border-[var(--sumi-border-faint)] bg-[var(--sumi-surface-card)]/30 hover:border-[var(--sumi-border-default)] transition-all duration-500"
|
||||
>
|
||||
<feat.icon size={20} strokeWidth={1.2} className="text-[var(--sumi-text-tertiary)]" />
|
||||
<span
|
||||
className="text-xs tracking-[0.1em] text-[var(--sumi-text-secondary)]"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}
|
||||
>
|
||||
{feat.label}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.p
|
||||
variants={inkReveal}
|
||||
custom={6}
|
||||
className="text-sm text-[var(--sumi-text-tertiary)] max-w-[500px] mx-auto leading-relaxed"
|
||||
style={{ fontFamily: 'var(--sumi-font-body)' }}
|
||||
>
|
||||
435 000 lignes de code. Audit de sécurité externe. 34 suites de tests.
|
||||
Backend Go + Stream server Rust + Frontend React.
|
||||
Auto-hébergé. Pas de cloud. Pas de VC.
|
||||
</motion.p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ═══ EMAIL CAPTURE — Bottom CTA ═══ */}
|
||||
<Section className="relative z-10 py-32 px-6" id="notify">
|
||||
<div className="max-w-[600px] mx-auto text-center">
|
||||
<motion.span
|
||||
variants={inkReveal}
|
||||
custom={0}
|
||||
className="ghost-kanji text-[120px] opacity-[0.03] absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
待
|
||||
</motion.span>
|
||||
|
||||
<motion.h2
|
||||
variants={inkReveal}
|
||||
custom={0}
|
||||
className="text-2xl sm:text-3xl mb-3 tracking-[0.05em]"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 200 }}
|
||||
>
|
||||
Rejoins les premiers
|
||||
</motion.h2>
|
||||
|
||||
<motion.div variants={brushStroke} className="w-16 h-px bg-gradient-to-r from-transparent via-[var(--sumi-kin)]/30 to-transparent mx-auto mb-6" />
|
||||
|
||||
<motion.p
|
||||
variants={inkReveal}
|
||||
custom={1}
|
||||
className="text-sm text-[var(--sumi-text-secondary)] mb-10 leading-relaxed"
|
||||
style={{ fontFamily: 'var(--sumi-font-body)' }}
|
||||
>
|
||||
Inscris-toi pour être notifié du lancement.
|
||||
Pas de spam — un seul email le jour J.
|
||||
</motion.p>
|
||||
|
||||
<motion.form
|
||||
variants={inkReveal}
|
||||
custom={2}
|
||||
onSubmit={handleSubscribe}
|
||||
className="flex flex-col sm:flex-row items-center gap-3 max-w-[460px] mx-auto"
|
||||
>
|
||||
{status === 'success' ? (
|
||||
<div className="flex items-center gap-2 text-[var(--sumi-sage)] text-sm py-3">
|
||||
<Check size={16} />
|
||||
<span style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}>
|
||||
C'est noté. Merci.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="ton@email.com"
|
||||
required
|
||||
aria-label="Adresse email pour notification de lancement"
|
||||
className="flex-1 w-full sm:w-auto px-4 py-2.5 bg-[var(--sumi-surface-card)] border border-[var(--sumi-border-default)] rounded-sm text-sm text-[var(--sumi-text-primary)] placeholder:text-[var(--sumi-text-disabled)] focus:outline-none focus:border-[var(--sumi-accent)]/50 focus:shadow-[var(--sumi-shadow-glow)] transition-all"
|
||||
style={{ fontFamily: 'var(--sumi-font-body)' }}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === 'loading'}
|
||||
className="w-full sm:w-auto px-6 py-2.5 bg-[var(--sumi-accent)] text-[var(--sumi-text-inverse)] rounded-sm text-sm tracking-[0.1em] hover:bg-[var(--sumi-accent-hover)] disabled:opacity-50 transition-colors flex items-center justify-center gap-2"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 400 }}
|
||||
>
|
||||
{status === 'loading' ? (
|
||||
<span className="h-4 w-4 border border-current border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
NOTIFIER MOI
|
||||
<ArrowRight size={14} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</motion.form>
|
||||
|
||||
{status === 'error' && (
|
||||
<p className="text-xs text-[var(--sumi-vermillion)] mt-2">{errorMsg}</p>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ═══ FOOTER ═══ */}
|
||||
<footer className="relative z-10 border-t border-[var(--sumi-border-faint)] py-16 px-6">
|
||||
<div className="max-w-[1000px] mx-auto">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-8">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-7 w-7 rounded-sm bg-[var(--sumi-accent)] flex items-center justify-center hanko-seal" style={{ transform: 'rotate(-2deg)' }}>
|
||||
<span className="text-[var(--sumi-bg-base)] text-xs font-semibold relative z-10" style={{ fontFamily: 'var(--sumi-font-heading)' }}>T</span>
|
||||
</div>
|
||||
<span className="text-sm tracking-[0.2em]" style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 200 }}>
|
||||
TALAS
|
||||
</span>
|
||||
<span className="text-[10px] text-[var(--sumi-text-disabled)] tracking-[0.1em]" style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}>
|
||||
× VEZA
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<nav className="flex items-center gap-8" aria-label="Footer navigation">
|
||||
{[
|
||||
{ label: 'Open Source', href: '#' },
|
||||
{ label: 'Confidentialité', href: '#' },
|
||||
{ label: 'Contact', href: 'mailto:contact@talas.fr' },
|
||||
].map((link) => (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className="text-xs text-[var(--sumi-text-tertiary)] hover:text-[var(--sumi-text-secondary)] transition-colors tracking-[0.05em]"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Bottom line */}
|
||||
<div className="mt-12 pt-8 border-t border-[var(--sumi-border-faint)] text-center">
|
||||
<p
|
||||
className="text-[10px] text-[var(--sumi-text-disabled)] tracking-[0.15em]"
|
||||
style={{ fontFamily: 'var(--sumi-font-heading)', fontWeight: 300 }}
|
||||
>
|
||||
TECHNOLOGIE AUDIO ÉTHIQUE — FAIT EN FRANCE — {new Date().getFullYear()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -51,6 +51,7 @@ import {
|
|||
LazyDistribution,
|
||||
LazyEducation,
|
||||
LazySupport,
|
||||
LazyLanding,
|
||||
} from '@/components/ui/LazyComponent';
|
||||
import { PublicRoute } from './PublicRoute';
|
||||
import { ProtectedLayoutRoute } from './ProtectedLayoutRoute';
|
||||
|
|
@ -87,6 +88,7 @@ export function getPublicRoutes(): RouteEntry[] {
|
|||
|
||||
export function getPublicStandaloneRoutes(): RouteEntry[] {
|
||||
return [
|
||||
{ path: '/launch', element: <ErrorBoundary><LazyLanding /></ErrorBoundary> },
|
||||
{ path: '/design-system', element: <ErrorBoundary><LazyDesignSystemDemo /></ErrorBoundary> },
|
||||
{ path: '/u/:username', element: <ErrorBoundary><LazyUserProfile /></ErrorBoundary> },
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue