diff --git a/apps/web/src/components/ui/lazy-component/index.ts b/apps/web/src/components/ui/lazy-component/index.ts index e57e79d73..80d8cbc1c 100644 --- a/apps/web/src/components/ui/lazy-component/index.ts +++ b/apps/web/src/components/ui/lazy-component/index.ts @@ -53,4 +53,5 @@ export { LazyDistribution, LazyEducation, LazySupport, + LazyLanding, } from './lazyExports'; diff --git a/apps/web/src/components/ui/lazy-component/lazyExports.ts b/apps/web/src/components/ui/lazy-component/lazyExports.ts index a0c3d4399..708aaad25 100644 --- a/apps/web/src/components/ui/lazy-component/lazyExports.ts +++ b/apps/web/src/components/ui/lazy-component/lazyExports.ts @@ -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', +); diff --git a/apps/web/src/features/landing/pages/LandingPage.tsx b/apps/web/src/features/landing/pages/LandingPage.tsx new file mode 100644 index 000000000..60b8acbc9 --- /dev/null +++ b/apps/web/src/features/landing/pages/LandingPage.tsx @@ -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 ( + + {children} + + ); +} + +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 ( +
+ {/* ═══ ATMOSPHERE — Ink wash background ═══ */} +