All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m54s
The landing hardcoded 9/19/49 (monthly) and 7/15/39 (annual-equivalent) while /pricing renders admin-configurable prices — the two surfaces drifted apart whenever prices changed. The landing now fetches the same live endpoint (no-store) with the previous values as load/failure fallback, and shows the French '9 €' format like /pricing (was '€9').
505 lines
28 KiB
TypeScript
505 lines
28 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import {
|
|
FileText,
|
|
Layers,
|
|
MousePointer2,
|
|
Download,
|
|
Globe,
|
|
Cpu,
|
|
CheckCircle2,
|
|
ArrowRight,
|
|
Layout,
|
|
PieChart,
|
|
FileSpreadsheet,
|
|
Presentation,
|
|
MessageSquare,
|
|
ShieldCheck,
|
|
Zap,
|
|
Menu,
|
|
X,
|
|
Languages,
|
|
Monitor,
|
|
Eye,
|
|
Activity,
|
|
} from "lucide-react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import { useI18n } from "@/lib/i18n";
|
|
import { API_BASE } from "@/lib/config";
|
|
import { LanguageSwitcher } from "@/components/ui/language-switcher";
|
|
|
|
const Logo = () => (
|
|
<div className="flex items-center gap-4 group cursor-pointer">
|
|
<div className="w-10 h-10 bg-brand-dark rounded-xl flex items-center justify-center text-white font-bold text-2xl shadow-xl transition-all duration-500 group-hover:rotate-[15deg] group-hover:scale-110">
|
|
W
|
|
</div>
|
|
<span className="text-2xl font-bold tracking-tighter text-brand-dark dark:text-white">
|
|
Wordly<span className="text-brand-accent">.art</span>
|
|
</span>
|
|
</div>
|
|
);
|
|
|
|
const Navbar = () => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const { t } = useI18n();
|
|
|
|
return (
|
|
<nav className="fixed top-0 left-0 right-0 z-50 bg-brand-bg/80 dark:bg-[#0a0a0a]/80 backdrop-blur-md border-b border-black/5 dark:border-white/5">
|
|
<div className="max-w-7xl mx-auto px-6 h-20 flex justify-between items-center">
|
|
<Link href="/">
|
|
<Logo />
|
|
</Link>
|
|
|
|
<div className="hidden md:flex items-center space-x-10">
|
|
<a href="#features" className="text-brand-dark/60 dark:text-white/60 hover:text-brand-dark dark:hover:text-white transition-colors text-[11px] font-bold uppercase tracking-widest">{t('landing.nav.why')}</a>
|
|
<a href="#formats" className="text-brand-dark/60 dark:text-white/60 hover:text-brand-dark dark:hover:text-white transition-colors text-[11px] font-bold uppercase tracking-widest">{t('landing.nav.formats')}</a>
|
|
<a href="#pricing" className="text-brand-dark/60 dark:text-white/60 hover:text-brand-dark dark:hover:text-white transition-colors text-[11px] font-bold uppercase tracking-widest">{t('landing.nav.pricing')}</a>
|
|
<div className="h-4 w-[1px] bg-black/10 dark:bg-white/10"></div>
|
|
<LanguageSwitcher className="h-9 w-auto gap-1.5 rounded-full border-black/10 dark:border-white/10 bg-transparent dark:bg-transparent px-4 text-[11px] font-bold uppercase tracking-widest text-brand-dark/60 dark:text-white/60 hover:text-brand-dark dark:hover:text-white shadow-none" />
|
|
<Link href="/auth/login" className="text-brand-dark/60 dark:text-white/60 hover:text-brand-dark dark:hover:text-white transition-colors text-[11px] font-bold uppercase tracking-widest">{t('landing.nav.login')}</Link>
|
|
<Link href="/auth/register" className="premium-button py-2.5 px-6 leading-none">{t('landing.nav.startFree')}</Link>
|
|
</div>
|
|
|
|
<div className="md:hidden">
|
|
<button onClick={() => setIsOpen(!isOpen)} className="text-brand-dark dark:text-white p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors">
|
|
{isOpen ? <X size={24} /> : <Menu size={24} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0, height: 0 }}
|
|
animate={{ opacity: 1, height: "auto" }}
|
|
exit={{ opacity: 0, height: 0 }}
|
|
className="md:hidden bg-white dark:bg-[#141414] border-b border-black/5 dark:border-white/5 overflow-hidden"
|
|
>
|
|
<div className="px-6 pt-4 pb-10 space-y-4 text-center">
|
|
<div className="flex justify-center">
|
|
<LanguageSwitcher className="w-full h-11 rounded-2xl border-2 border-black/5 dark:border-white/10 bg-transparent dark:bg-transparent font-bold text-brand-dark dark:text-white shadow-none" />
|
|
</div>
|
|
<a href="#features" onClick={() => setIsOpen(false)} className="block py-3 text-lg font-bold text-brand-dark dark:text-white">{t('landing.nav.why')}</a>
|
|
<a href="#formats" onClick={() => setIsOpen(false)} className="block py-3 text-lg font-bold text-brand-dark dark:text-white">{t('landing.nav.formats')}</a>
|
|
<a href="#pricing" onClick={() => setIsOpen(false)} className="block py-3 text-lg font-bold text-brand-dark dark:text-white">{t('landing.nav.pricing')}</a>
|
|
<div className="pt-6 grid grid-cols-2 gap-4">
|
|
<Link href="/auth/login" onClick={() => setIsOpen(false)} className="flex items-center justify-center py-4 text-brand-dark/60 dark:text-white/60 font-bold border-2 border-black/5 dark:border-white/10 rounded-2xl">{t('landing.nav.login')}</Link>
|
|
<Link href="/auth/register" onClick={() => setIsOpen(false)} className="flex items-center justify-center py-4 bg-brand-dark dark:bg-brand-accent text-white dark:text-brand-dark rounded-2xl font-bold">{t('landing.nav.startFree')}</Link>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</nav>
|
|
);
|
|
};
|
|
|
|
const Hero = () => {
|
|
const { t } = useI18n();
|
|
|
|
return (
|
|
<section className="relative pt-48 pb-32 lg:pt-64 lg:pb-48 px-4 text-center">
|
|
<div className="max-w-5xl mx-auto relative z-10">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
|
>
|
|
<div className="inline-flex items-center gap-2 px-4 py-1.5 mb-10 text-[10px] font-bold tracking-[0.3em] text-brand-dark/60 dark:text-white/60 uppercase bg-transparent border border-black/10 dark:border-white/10 rounded-full">
|
|
<Languages size={10} /> {t('landing.hero.tag')}
|
|
</div>
|
|
<h1 className="text-6xl md:text-8xl mb-10 text-brand-dark dark:text-white font-serif font-medium tracking-tight leading-[1.1]">
|
|
{t('landing.hero.titleLine1')}<br />
|
|
<span className="block mt-4 italic">{t('landing.hero.titleLine2')}</span>
|
|
</h1>
|
|
<p className="text-lg md:text-xl text-brand-dark/50 dark:text-white/50 mb-14 max-w-2xl mx-auto leading-relaxed font-light">
|
|
{t('landing.hero.description')}
|
|
</p>
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-24">
|
|
<Link href="/auth/register" className="premium-button px-12 py-5 text-base inline-block text-center">
|
|
{t('landing.hero.ctaMain')}
|
|
</Link>
|
|
<a href="#pricing" className="px-12 py-5 rounded-xl border border-black/10 dark:border-white/10 font-bold text-base hover:bg-black/5 dark:hover:bg-white/5 transition-all inline-block text-brand-dark dark:text-white">
|
|
{t('landing.hero.ctaSec')}
|
|
</a>
|
|
</div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.98, y: 30 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1], delay: 0.2 }}
|
|
className="relative max-w-5xl mx-auto"
|
|
>
|
|
<div className="editorial-card p-4 lg:p-6 overflow-hidden !bg-brand-muted/30 dark:!bg-[#1f1f1f]/30">
|
|
<div className="aspect-[16/9] lg:aspect-[21/9] bg-white dark:bg-[#141414] rounded-2xl relative overflow-hidden shadow-sm border border-black/5 dark:border-white/5">
|
|
<img
|
|
src="https://images.unsplash.com/photo-1499951360447-b19be8fe80f5?auto=format&fit=crop&q=80&w=2000"
|
|
alt="Workspace"
|
|
className="w-full h-full object-cover filter saturate-[0.8] grayscale-[0.1]"
|
|
referrerPolicy="no-referrer"
|
|
/>
|
|
<div className="absolute inset-0 bg-brand-dark/5 dark:bg-black/20" />
|
|
|
|
<div className="absolute top-8 right-8 w-64 bg-white/90 dark:bg-[#1a1a1a]/90 backdrop-blur-xl border border-black/5 dark:border-white/10 p-6 rounded-2xl shadow-2xl text-left hidden md:block">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="w-8 h-8 rounded-full bg-brand-accent/20 flex items-center justify-center text-brand-accent">
|
|
<Zap size={16} />
|
|
</div>
|
|
<span className="text-[10px] font-bold uppercase tracking-widest text-brand-dark dark:text-white">Context Engine</span>
|
|
</div>
|
|
<p className="text-xs text-brand-dark/70 dark:text-white/70 leading-relaxed font-medium">{t('landing.hero.contextEngine')}</p>
|
|
</div>
|
|
|
|
<div className="absolute bottom-8 left-8 w-64 bg-brand-dark dark:bg-[#0a0a0a] text-white p-6 rounded-2xl shadow-2xl text-left hidden md:block">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<Activity size={16} className="text-brand-accent" />
|
|
<span className="text-[10px] font-bold uppercase tracking-widest text-brand-accent">{t('landing.hero.liveAnalysis')}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{[1, 2, 3].map(i => (
|
|
<div key={i} className="w-6 h-6 rounded-full border-2 border-brand-dark dark:border-[#0a0a0a] bg-brand-muted dark:bg-[#1f1f1f] text-[8px] flex items-center justify-center font-bold text-brand-dark dark:text-white">JD</div>
|
|
))}
|
|
<span className="text-[10px] ml-2 text-white/60">+12 {t('landing.hero.termsDetected')}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const Steps = () => {
|
|
const { t } = useI18n();
|
|
const steps = [
|
|
{ num: t('landing.steps.step1.num'), title: t('landing.steps.step1.title'), desc: t('landing.steps.step1.desc'), icon: <MousePointer2 size={24} /> },
|
|
{ num: t('landing.steps.step2.num'), title: t('landing.steps.step2.title'), desc: t('landing.steps.step2.desc'), icon: <Cpu size={24} /> },
|
|
{ num: t('landing.steps.step3.num'), title: t('landing.steps.step3.title'), desc: t('landing.steps.step3.desc'), icon: <Download size={24} /> },
|
|
];
|
|
|
|
return (
|
|
<section className="py-32 px-4 border-y border-black/5 dark:border-white/5">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="mb-24">
|
|
<span className="accent-pill mb-6 block w-fit">{t('landing.steps.process')}</span>
|
|
<h2 className="text-4xl md:text-5xl text-brand-dark dark:text-white font-serif font-medium tracking-tight mb-6">{t('landing.steps.title')}</h2>
|
|
<p className="text-brand-dark/60 dark:text-white/60 text-lg max-w-xl font-light">{t('landing.steps.subtitle')}</p>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-3 gap-16">
|
|
{steps.map((st, i) => (
|
|
<div key={i} className="group">
|
|
<div className="w-12 h-12 rounded-xl bg-brand-muted dark:bg-[#1f1f1f] flex items-center justify-center text-brand-dark dark:text-white mb-8 group-hover:bg-brand-dark dark:group-hover:bg-brand-accent group-hover:text-white dark:group-hover:text-brand-dark transition-all duration-500">
|
|
{st.icon}
|
|
</div>
|
|
<h3 className="text-xl font-serif font-medium text-brand-dark dark:text-white mb-4">{st.title}</h3>
|
|
<p className="text-brand-dark/60 dark:text-white/60 leading-relaxed font-light">{st.desc}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const FeaturesContext = () => {
|
|
const { t } = useI18n();
|
|
return (
|
|
<section id="features" className="py-32 px-4 bg-brand-muted/30 dark:bg-[#1f1f1f]/30">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="grid lg:grid-cols-12 gap-20 items-end mb-24">
|
|
<div className="lg:col-span-12">
|
|
<span className="accent-pill mb-6 block w-fit font-bold uppercase tracking-widest">{t('landing.features.tag')}</span>
|
|
<h2 className="text-4xl md:text-5xl text-brand-dark dark:text-white font-serif font-medium tracking-tight leading-none">
|
|
{t('landing.features.title')}
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-3 gap-8">
|
|
{[
|
|
{ icon: <Monitor className="text-brand-dark/60 dark:text-white/60" />, title: t('landing.features.context.title'), desc: t('landing.features.context.desc') },
|
|
{ icon: <Layers className="text-brand-dark/60 dark:text-white/60" />, title: t('landing.features.glossary.title'), desc: t('landing.features.glossary.desc') },
|
|
{ icon: <Eye className="text-brand-dark/60 dark:text-white/60" />, title: t('landing.features.vision.title'), desc: t('landing.features.vision.desc') },
|
|
].map((item, i) => (
|
|
<div key={i} className="editorial-card p-10 flex flex-col items-start gap-8">
|
|
<div className="w-10 h-10 border border-black/5 dark:border-white/10 rounded-full flex items-center justify-center">
|
|
{item.icon}
|
|
</div>
|
|
<div>
|
|
<h4 className="text-xl font-serif font-medium mb-4 text-brand-dark dark:text-white">{item.title}</h4>
|
|
<p className="text-brand-dark/60 dark:text-white/60 leading-relaxed font-light text-sm">{item.desc}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const LayoutFeatures = () => {
|
|
const { t } = useI18n();
|
|
const points = [
|
|
{ title: t('landing.layout.p1.title'), desc: t('landing.layout.p1.desc'), icon: <Layers size={22} /> },
|
|
{ title: t('landing.layout.p2.title'), desc: t('landing.layout.p2.desc'), icon: <Layout size={22} /> },
|
|
{ title: t('landing.layout.p3.title'), desc: t('landing.layout.p3.desc'), icon: <PieChart size={22} /> },
|
|
{ title: t('landing.layout.p4.title'), desc: t('landing.layout.p4.desc'), icon: <Zap size={22} /> },
|
|
{ title: t('landing.layout.p5.title'), desc: t('landing.layout.p5.desc'), icon: <MessageSquare size={22} /> },
|
|
{ title: t('landing.layout.p6.title'), desc: t('landing.layout.p6.desc'), icon: <Globe size={22} /> },
|
|
];
|
|
|
|
return (
|
|
<section className="py-32 px-4 bg-brand-dark dark:bg-[#0a0a0a] text-white">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="text-center mb-32">
|
|
<h2 className="text-4xl md:text-6xl mb-10 leading-tight font-serif font-medium tracking-tight">
|
|
{t('landing.layout.title')}<br /> <span className="italic">{t('landing.layout.title2')}</span>
|
|
</h2>
|
|
<p className="text-white/40 max-w-2xl mx-auto text-xl font-light tracking-widest">
|
|
{t('landing.layout.subtitle')}
|
|
</p>
|
|
</div>
|
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-white/5 border border-white/10 rounded-3xl overflow-hidden shadow-2xl">
|
|
{points.map((p, i) => (
|
|
<div key={i} className="group p-14 bg-brand-dark dark:bg-[#0a0a0a] hover:bg-white/[0.02] transition-all duration-700">
|
|
<div className="w-12 h-12 border border-white/10 rounded-xl flex items-center justify-center text-brand-accent mb-10 group-hover:scale-110 group-hover:border-white/20 transition-all">
|
|
{p.icon}
|
|
</div>
|
|
<h4 className="text-xl font-serif font-medium mb-6 leading-none">{p.title}</h4>
|
|
<p className="text-white/30 text-sm leading-relaxed font-light">{p.desc}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const FormatsSection = () => {
|
|
const { t } = useI18n();
|
|
const formats = [
|
|
{ id: ".DOCX", name: t('landing.formats.word.name'), icon: <FileText className="text-brand-dark/60 dark:text-white/60" />, items: [t('landing.formats.word.i1'), t('landing.formats.word.i2'), t('landing.formats.word.i3'), t('landing.formats.word.i4')] },
|
|
{ id: ".XLSX", name: t('landing.formats.excel.name'), icon: <FileSpreadsheet className="text-brand-dark/60 dark:text-white/60" />, items: [t('landing.formats.excel.i1'), t('landing.formats.excel.i2'), t('landing.formats.excel.i3'), t('landing.formats.excel.i4')] },
|
|
{ id: ".PPTX", name: t('landing.formats.pptx.name'), icon: <Presentation className="text-brand-dark/60 dark:text-white/60" />, items: [t('landing.formats.pptx.i1'), t('landing.formats.pptx.i2'), t('landing.formats.pptx.i3'), t('landing.formats.pptx.i4')] },
|
|
{ id: ".PDF", name: t('landing.formats.pdf.name'), icon: <FileText className="text-brand-dark/60 dark:text-white/60" />, items: [t('landing.formats.pdf.i1'), t('landing.formats.pdf.i2'), t('landing.formats.pdf.i3'), t('landing.formats.pdf.i4')] },
|
|
];
|
|
|
|
return (
|
|
<section id="formats" className="py-40 px-4">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="mb-24">
|
|
<span className="accent-pill mb-6 block w-fit">{t('landing.formats.pill')}</span>
|
|
<h2 className="text-4xl md:text-5xl text-brand-dark dark:text-white mb-6 leading-tight font-serif font-medium tracking-tight">{t('landing.formats.title')} <span className="italic">{t('landing.formats.title2')}</span></h2>
|
|
<p className="text-brand-dark/60 dark:text-white/60 max-w-xl text-lg font-light">{t('landing.formats.subtitle')}</p>
|
|
</div>
|
|
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
|
|
{formats.map((f, i) => (
|
|
<div key={i} className="editorial-card p-10 group">
|
|
<div className="flex justify-between items-start mb-10">
|
|
<div className="w-12 h-12 bg-brand-muted dark:bg-[#1f1f1f] rounded-xl flex items-center justify-center text-xl group-hover:bg-brand-dark dark:group-hover:bg-brand-accent group-hover:text-white dark:group-hover:text-brand-dark transition-all">
|
|
{f.icon}
|
|
</div>
|
|
<span className="text-[9px] font-bold text-brand-dark/50 dark:text-white/50 uppercase tracking-[0.2em]">{f.id}</span>
|
|
</div>
|
|
<h3 className="text-xl font-serif font-medium mb-6 text-brand-dark dark:text-white tracking-tight">{f.name}</h3>
|
|
<ul className="space-y-4">
|
|
{f.items.map((item, j) => (
|
|
<li key={j} className="flex items-start gap-3 text-xs text-brand-dark/60 dark:text-white/60 font-medium">
|
|
<div className="w-1 h-1 bg-brand-accent rounded-full mt-1.5 shrink-0 opacity-40"></div> {item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const PricingSection = () => {
|
|
const { t } = useI18n();
|
|
const [isAnnual, setIsAnnual] = useState(true);
|
|
// Prices come from the same live endpoint as /pricing (admin-configurable);
|
|
// the hardcoded values below are only a fallback while it loads or fails.
|
|
const [livePrices, setLivePrices] = useState<Record<string, { monthly: number; annualMonthly: number }>>({});
|
|
|
|
useEffect(() => {
|
|
fetch(`${API_BASE}/api/v1/auth/plans`, { cache: 'no-store' })
|
|
.then(r => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
.then(json => {
|
|
const d = json.data ?? json;
|
|
const map: Record<string, { monthly: number; annualMonthly: number }> = {};
|
|
for (const plan of d.plans ?? []) {
|
|
if (typeof plan.price_monthly === 'number' && plan.price_monthly > 0) {
|
|
map[plan.id] = {
|
|
monthly: plan.price_monthly,
|
|
annualMonthly: plan.price_yearly > 0 ? plan.price_yearly / 12 : plan.price_monthly,
|
|
};
|
|
}
|
|
}
|
|
setLivePrices(map);
|
|
})
|
|
.catch(() => { /* keep marketing defaults */ });
|
|
}, []);
|
|
|
|
const priceFor = (id: string, fallbackMonthly: number, fallbackAnnual: number): string => {
|
|
const live = livePrices[id];
|
|
const value = live
|
|
? (isAnnual ? live.annualMonthly : live.monthly)
|
|
: (isAnnual ? fallbackAnnual : fallbackMonthly);
|
|
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
};
|
|
|
|
const plans = [
|
|
{ id: 'starter', name: t('landing.pricing.starter.name'), desc: t('landing.pricing.starter.desc'), monthlyPrice: 9, annualPrice: 7, features: [t('landing.pricing.starter.f1'), t('landing.pricing.starter.f2'), t('landing.pricing.starter.f3'), t('landing.pricing.starter.f4')], cta: t('landing.pricing.starter.cta'), popular: false },
|
|
{ id: 'pro', name: t('landing.pricing.pro.name'), desc: t('landing.pricing.pro.desc'), monthlyPrice: 19, annualPrice: 15, features: [t('landing.pricing.pro.f1'), t('landing.pricing.pro.f2'), t('landing.pricing.pro.f3'), t('landing.pricing.pro.f4'), t('landing.pricing.pro.f5'), t('landing.pricing.pro.f6')], cta: t('landing.pricing.pro.cta'), popular: true },
|
|
{ id: 'business', name: t('landing.pricing.business.name'), desc: t('landing.pricing.business.desc'), monthlyPrice: 49, annualPrice: 39, features: [t('landing.pricing.business.f1'), t('landing.pricing.business.f2'), t('landing.pricing.business.f3'), t('landing.pricing.business.f4'), t('landing.pricing.business.f5'), t('landing.pricing.business.f6')], cta: t('landing.pricing.business.cta'), popular: false },
|
|
];
|
|
|
|
return (
|
|
<section id="pricing" className="py-40 px-4 bg-brand-bg dark:bg-[#0a0a0a]">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="text-center mb-24">
|
|
<h2 className="text-4xl md:text-5xl mb-8 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">{t('landing.pricing.title')}</h2>
|
|
<p className="text-brand-dark/60 dark:text-white/60 text-xl font-light max-w-2xl mx-auto">{t('landing.pricing.subtitle')}</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-center gap-12 mb-32">
|
|
<button onClick={() => setIsAnnual(false)} className={`text-sm font-bold uppercase tracking-widest transition-all ${!isAnnual ? 'text-brand-dark dark:text-white underline underline-offset-8' : 'text-brand-dark/55 dark:text-white/55 hover:text-brand-dark dark:hover:text-white'}`}>
|
|
{t('landing.pricing.monthly')}
|
|
</button>
|
|
<button onClick={() => setIsAnnual(true)} className={`text-sm font-bold uppercase tracking-widest transition-all relative ${isAnnual ? 'text-brand-dark dark:text-white underline underline-offset-8' : 'text-brand-dark/55 dark:text-white/55 hover:text-brand-dark dark:hover:text-white'}`}>
|
|
{t('landing.pricing.annual')}
|
|
<span className="absolute -top-6 left-1/2 -translate-x-1/2 text-[9px] font-bold text-brand-accent whitespace-nowrap italic">
|
|
(-20%)
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-black/5 dark:bg-white/5 rounded-[40px] overflow-hidden border border-black/5 dark:border-white/5">
|
|
{plans.map((p, i) => (
|
|
<div key={i} className="p-16 bg-white dark:bg-[#141414] flex flex-col text-left transition-all duration-500 hover:z-10 hover:shadow-2xl hover:scale-[1.01]">
|
|
<h3 className="text-2xl font-serif font-medium mb-2 text-brand-dark dark:text-white tracking-tight">{p.name}</h3>
|
|
<p className="text-brand-dark/60 dark:text-white/60 text-xs font-light mb-14 tracking-widest uppercase">{p.desc}</p>
|
|
|
|
<div className="mb-14">
|
|
<span className="text-5xl font-serif font-medium leading-none text-brand-dark dark:text-white">{priceFor(p.id, p.monthlyPrice, p.annualPrice)} €</span>
|
|
<span className="text-brand-dark/55 dark:text-white/55 font-light ml-2 uppercase text-[10px] tracking-[0.2em]">/ {t('landing.pricing.month')}</span>
|
|
</div>
|
|
|
|
<ul className="space-y-6 mb-16 flex-grow">
|
|
{p.features.map((f, j) => (
|
|
<li key={j} className="flex items-start gap-4 text-sm text-brand-dark/50 dark:text-white/50 font-medium leading-snug">
|
|
<div className="w-1 h-1 rounded-full bg-brand-accent mt-2 shrink-0"></div> <span className="font-light">{f}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<Link
|
|
href={p.popular ? "/auth/register" : `/pricing?plan=${p.name.toLowerCase()}${isAnnual ? '&billing=yearly' : ''}`}
|
|
className={`w-full py-4 rounded-2xl text-xs font-bold uppercase tracking-widest transition-all leading-none text-center block ${p.popular ? 'bg-brand-dark dark:bg-brand-accent text-white dark:text-brand-dark shadow-xl' : 'bg-transparent text-brand-dark dark:text-white border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5'}`}
|
|
>
|
|
{p.cta}
|
|
</Link>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<p className="text-center mt-12 text-brand-dark/50 dark:text-white/50 text-xs font-bold uppercase tracking-widest">{t('landing.pricing.footer')}</p>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const CTASection = () => {
|
|
const { t } = useI18n();
|
|
return (
|
|
<section className="py-40 px-4 text-center">
|
|
<div className="max-w-4xl mx-auto">
|
|
<h2 className="text-5xl md:text-7xl text-brand-dark dark:text-white mb-10 leading-tight tracking-tight font-serif font-medium">
|
|
{t('landing.cta.title')}
|
|
</h2>
|
|
<p className="text-lg text-brand-dark/60 dark:text-white/60 mb-16 font-light">
|
|
{t('landing.cta.subtitle')}
|
|
</p>
|
|
<div className="flex flex-col sm:flex-row gap-6 justify-center items-center">
|
|
<Link href="/auth/register" className="premium-button px-16 py-6 text-lg inline-block text-center rounded-[32px]">
|
|
{t('landing.cta.button')}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const Footer = () => {
|
|
const { t } = useI18n();
|
|
return (
|
|
<footer className="bg-brand-muted/30 dark:bg-[#1f1f1f]/30 pt-40 pb-20 px-6">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="grid md:grid-cols-12 gap-20 mb-40">
|
|
<div className="md:col-span-12 lg:col-span-6">
|
|
<Logo />
|
|
<p className="text-brand-dark/60 dark:text-white/60 text-lg font-medium mt-12 mb-12 max-w-sm leading-relaxed uppercase tracking-widest">
|
|
{t('landing.footer.desc')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="md:col-span-4 lg:col-span-2">
|
|
<h4 className="text-[10px] font-bold uppercase tracking-[0.3em] text-brand-dark dark:text-white mb-10">{t('landing.footer.product')}</h4>
|
|
<ul className="space-y-6 text-xs text-brand-dark/60 dark:text-white/60 font-bold uppercase tracking-widest leading-none">
|
|
<li><a href="#features" className="hover:text-brand-dark dark:hover:text-white transition-colors">{t('landing.nav.why')}</a></li>
|
|
<li><a href="#formats" className="hover:text-brand-dark dark:hover:text-white transition-colors">{t('landing.nav.formats')}</a></li>
|
|
<li><a href="#pricing" className="hover:text-brand-dark dark:hover:text-white transition-colors">{t('landing.nav.pricing')}</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="pt-10 border-t border-black/5 dark:border-white/5 flex flex-col md:flex-row justify-between items-center gap-10">
|
|
<div className="text-[10px] font-bold text-brand-dark/50 dark:text-white/50 uppercase tracking-[0.4em]">
|
|
{t('landing.footer.rights')}
|
|
</div>
|
|
<div className="flex items-center gap-6">
|
|
<button
|
|
type="button"
|
|
onClick={() => import('@/components/ui/cookie-consent').then(m => m.reopenCookieConsent())}
|
|
className="text-[10px] font-bold text-brand-dark/50 dark:text-white/50 uppercase tracking-[0.2em] hover:text-brand-dark dark:hover:text-white transition-colors"
|
|
>
|
|
{t('cookieConsent.reopen')}
|
|
</button>
|
|
<div className="text-[10px] font-mono text-brand-dark/55 dark:text-white/55 uppercase tracking-widest">
|
|
wordly.art
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
);
|
|
};
|
|
|
|
export default function LandingPage() {
|
|
return (
|
|
<div className="min-h-screen font-sans bg-brand-bg dark:bg-[#0a0a0a] text-brand-dark dark:text-white selection:bg-brand-accent/20 overflow-x-hidden">
|
|
<Navbar />
|
|
<main>
|
|
<Hero />
|
|
<Steps />
|
|
<FeaturesContext />
|
|
<LayoutFeatures />
|
|
<FormatsSection />
|
|
<PricingSection />
|
|
<CTASection />
|
|
</main>
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|