Social Scheduler Landing New
A warm, animated landing page for a social media scheduling app with real network logos, live composer previews, heatmaps and analytics.
Installation
01
Install with the shadcn CLI. The NiceUI theme comes along the first time.
$ npx shadcn@latest add https://niceui.dev/r/landing-social-scheduler-landing.json02
Or copy the source below (14 files). Dependencies: lucide-react.
Source
components/niceui/pages/landing/LandingPageCadence.tsx
import { Hero, Nav } from './cadence/Hero';
import { NetworkMarquee } from './cadence/NetworkMarquee';
import { Composer } from './cadence/Composer';
import { Scheduling } from './cadence/Scheduling';
import { Analytics } from './cadence/Analytics';
import { Workflow } from './cadence/Workflow';
import { Pricing, Testimonials } from './cadence/Social';
import { FAQ, FinalCTA, Footer } from './cadence/Closing';
const STYLES = `
@keyframes cad-pop { from { opacity: 0; transform: translateY(8px) scale(0.94); filter: blur(4px); } to { opacity: 1; transform: none; filter: none; } }
@keyframes cad-drift { 0%,100% { transform: translate(0,0) rotate(0deg); } 33% { transform: translate(6px,-10px) rotate(3deg); } 66% { transform: translate(-6px,-4px) rotate(-3deg); } }
@keyframes cad-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
@keyframes cad-word { from { opacity: 0; transform: translateY(0.4em); filter: blur(8px); } to { opacity: 1; transform: none; filter: none; } }
@keyframes cad-underline { from { stroke-dashoffset: 320; } to { stroke-dashoffset: 0; } }
@keyframes cad-roll { from { opacity: 0; transform: translateY(70%); filter: blur(4px); } to { opacity: 1; transform: none; filter: none; } }
.cad-pop { animation: cad-pop 600ms cubic-bezier(0.16,1,0.3,1) both; }
.cad-drift { animation: cad-drift 7s ease-in-out infinite; }
.cad-draw { stroke-dashoffset: 1; animation: cad-draw 2.2s cubic-bezier(0.65,0,0.35,1) 200ms forwards; }
.cad-word { animation: cad-word 900ms cubic-bezier(0.16,1,0.3,1) both; }
.cad-underline { stroke-dasharray: 320; stroke-dashoffset: 320; animation: cad-underline 1.2s cubic-bezier(0.65,0,0.35,1) 900ms forwards; }
.cad-roll { animation: cad-roll 500ms cubic-bezier(0.16,1,0.3,1) both; }
@media (prefers-reduced-motion: reduce) {
.cad-pop, .cad-drift, .cad-word, .cad-roll { animation: none; }
.cad-draw, .cad-underline { animation: none; stroke-dashoffset: 0; }
}
`;
export function LandingPageCadence() {
return (
<div className="relative min-h-screen bg-[#FBFAF7] font-sans text-stone-900 antialiased selection:bg-[#FF5A36]/20">
<style>{STYLES}</style>
<Nav />
<main>
<Hero />
<NetworkMarquee />
<Composer />
<Scheduling />
<Analytics />
<Workflow />
<Testimonials />
<Pricing />
<FAQ />
<FinalCTA />
</main>
<Footer />
</div>
);
}components/niceui/pages/landing/cadence/Hero.tsx
import { useEffect, useRef, useState } from 'react';
import { ArrowRight, AudioWaveform, Menu, Play, Star, X as Close } from 'lucide-react';
import { cn } from '@/lib/cn';
import { HeroMock, OrbitingLogos } from './HeroMock';
import { Avatar, Eyebrow, GhostButton, PEOPLE, PrimaryButton, scrollToId } from './shared';
const LINKS = [
{ id: 'composer', label: 'Composer' },
{ id: 'scheduling', label: 'Scheduling' },
{ id: 'analytics', label: 'Analytics' },
{ id: 'workflow', label: 'Workflow' },
{ id: 'pricing', label: 'Pricing' },
{ id: 'faq', label: 'FAQ' },
];
export function CadenceLogo({ className }: { className?: string }) {
return (
<span className={cn('inline-flex items-center gap-2', className)}>
<span className="relative flex h-8 w-8 items-center justify-center rounded-xl bg-[#FF5A36] text-white shadow-[0_6px_16px_-6px_rgba(255,90,54,0.8)]">
<AudioWaveform className="h-4 w-4" strokeWidth={2.25} />
</span>
<span className="text-lg font-semibold tracking-[-0.03em] text-stone-900">cadence</span>
</span>
);
}
export function Nav() {
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const sentinel = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = sentinel.current;
if (!el) return;
const io = new IntersectionObserver(([e]) => setScrolled(!e.isIntersecting));
io.observe(el);
return () => io.disconnect();
}, []);
const go = (id: string) => {
setOpen(false);
scrollToId(id);
};
return (
<>
<div ref={sentinel} className="absolute top-0 h-px w-px" aria-hidden="true" />
<header className="sticky top-0 z-50 px-4 pt-3 sm:px-6">
<nav
className={cn(
'mx-auto flex max-w-6xl items-center justify-between rounded-full border px-3 py-2 transition-all duration-500 ease-smooth sm:px-4',
scrolled
? 'border-stone-200/80 bg-[#FBFAF7]/80 shadow-[0_8px_32px_-12px_rgba(28,25,23,0.18)] backdrop-blur-xl'
: 'border-transparent bg-transparent',
)}
>
<button type="button" onClick={() => go('top')} className="rounded-full pl-1" aria-label="Cadence home">
<CadenceLogo />
</button>
<div className="hidden items-center gap-1 lg:flex">
{LINKS.map((l) => (
<button
key={l.id}
type="button"
onClick={() => go(l.id)}
className="rounded-full px-3.5 py-2 text-sm text-stone-600 transition-colors duration-200 hover:bg-stone-900/5 hover:text-stone-900"
>
{l.label}
</button>
))}
</div>
<div className="flex items-center gap-2">
<button type="button" className="hidden rounded-full px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:text-stone-900 sm:block">
Sign in
</button>
<button
type="button"
onClick={() => go('pricing')}
className="hidden rounded-full bg-stone-900 px-4 py-2 text-sm font-medium text-white transition-all duration-300 ease-spring hover:-translate-y-0.5 hover:bg-[#FF5A36] sm:block"
>
Start free
</button>
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex h-10 w-10 items-center justify-center rounded-full text-stone-800 transition-colors hover:bg-stone-900/5 lg:hidden"
aria-label={open ? 'Close menu' : 'Open menu'}
aria-expanded={open}
>
{open ? <Close className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</nav>
<div
className={cn(
'mx-auto mt-2 max-w-6xl origin-top overflow-hidden rounded-3xl border border-stone-200 bg-white/95 shadow-[0_24px_48px_-16px_rgba(28,25,23,0.25)] backdrop-blur-xl transition-all duration-300 ease-spring lg:hidden',
open ? 'pointer-events-auto scale-100 opacity-100' : 'pointer-events-none absolute inset-x-4 scale-95 opacity-0',
)}
>
<div className="grid gap-1 p-3">
{LINKS.map((l) => (
<button
key={l.id}
type="button"
onClick={() => go(l.id)}
className="flex items-center justify-between rounded-2xl px-4 py-3 text-left text-base text-stone-800 transition-colors hover:bg-stone-100"
>
{l.label}
<ArrowRight className="h-4 w-4 text-stone-400" />
</button>
))}
<div className="mt-2 grid grid-cols-2 gap-2">
<button type="button" className="rounded-full border border-stone-300 py-3 text-sm font-medium text-stone-800">Sign in</button>
<button type="button" onClick={() => go('pricing')} className="rounded-full bg-stone-900 py-3 text-sm font-medium text-white">
Start free
</button>
</div>
</div>
</div>
</header>
</>
);
}
const HEADLINE = ['Post', 'everywhere.'];
export function Hero() {
return (
<section id="top" className="relative scroll-mt-20 overflow-hidden pb-24 pt-12 sm:pt-20">
<div className="pointer-events-none absolute inset-0 -z-10">
<div className="absolute left-1/2 top-[-10%] h-[560px] w-[900px] -translate-x-1/2 rounded-full bg-[radial-gradient(closest-side,rgba(255,90,54,0.16),transparent)]" />
<div className="absolute right-[-10%] top-[20%] h-[420px] w-[420px] animate-blob bg-amber-200/30 blur-3xl" />
<div className="absolute left-[-8%] top-[40%] h-[360px] w-[360px] animate-blob bg-sky-200/30 blur-3xl [animation-delay:-6s]" />
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(28,25,23,0.04)_1px,transparent_1px),linear-gradient(to_bottom,rgba(28,25,23,0.04)_1px,transparent_1px)] bg-[size:56px_56px] [mask-image:radial-gradient(ellipse_at_top,black_30%,transparent_70%)]" />
</div>
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<div className="mx-auto max-w-3xl text-center">
<div className="animate-fade-down">
<Eyebrow>
New: AI captions tuned to each network <ArrowRight className="h-3 w-3" />
</Eyebrow>
</div>
<h1 className="mt-6 text-5xl font-medium leading-[1.05] tracking-[-0.045em] text-stone-900 sm:text-6xl lg:text-7xl">
{HEADLINE.map((w, i) => (
<span key={w} className="cad-word mr-[0.22em] inline-block" style={{ animationDelay: `${i * 90}ms` }}>
{w}
</span>
))}
<br className="hidden sm:block" />
<span className="cad-word inline-block font-serif font-normal italic tracking-[-0.01em]" style={{ animationDelay: '220ms' }}>
Stay <span className="relative inline-block text-[#FF5A36]">
in rhythm.
<svg viewBox="0 0 300 20" className="absolute -bottom-2 left-0 h-3 w-full" preserveAspectRatio="none" aria-hidden="true">
<path d="M2 14 C 60 2, 120 2, 160 10 S 260 18, 298 6" fill="none" stroke="#FF5A36" strokeWidth="3" strokeLinecap="round" className="cad-underline" />
</svg>
</span>
</span>
</h1>
<p className="mx-auto mt-8 max-w-xl animate-fade-up text-lg leading-relaxed text-stone-600 [animation-delay:350ms]">
Cadence is the calm command center for social. Plan a month of content in an afternoon, tailor every caption per network,
and publish at the exact moment your audience is listening.
</p>
<div className="mt-8 flex animate-fade-up flex-col items-center justify-center gap-3 [animation-delay:450ms] sm:flex-row">
<PrimaryButton onClick={() => scrollToId('pricing')}>
Start free for 14 days
<ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
</PrimaryButton>
<GhostButton onClick={() => scrollToId('composer')}>
<Play className="h-3.5 w-3.5 fill-current" /> See it in action
</GhostButton>
</div>
<div className="mt-8 flex animate-fade-up flex-col items-center justify-center gap-3 text-sm text-stone-600 [animation-delay:550ms] sm:flex-row">
<div className="flex -space-x-2">
{[PEOPLE.maya, PEOPLE.daniel, PEOPLE.amara, PEOPLE.theo, PEOPLE.lena].map((p) => (
<Avatar key={p.name} src={p.img} name={p.name} />
))}
</div>
<div className="flex items-center gap-2">
<span className="flex text-amber-500">
{[0, 1, 2, 3, 4].map((i) => (
<Star key={i} className="h-4 w-4 fill-current" />
))}
</span>
<span>
Loved by <span className="font-medium text-stone-900">18,000+</span> creators & teams
</span>
</div>
</div>
</div>
<div className="relative mx-auto mt-16 max-w-5xl animate-fade-up [animation-delay:650ms] sm:mt-20">
<OrbitingLogos />
<HeroMock />
</div>
</div>
</section>
);
}components/niceui/pages/landing/cadence/HeroMock.tsx
import { useEffect, useState } from 'react';
import { CalendarDays, Check, ChevronLeft, ChevronRight, Loader2, Plus, TrendingUp } from 'lucide-react';
import { cn } from '@/lib/cn';
import { NetworkBadge, networkMap, type NetworkId } from './logos';
import { IMG, PEOPLE, Avatar } from './shared';
type MockPost = { day: number; slot: number; net: NetworkId; img: string; time: string; title: string };
const DAYS = [
{ d: 'Mon', n: 9 },
{ d: 'Tue', n: 10 },
{ d: 'Wed', n: 11 },
{ d: 'Thu', n: 12 },
{ d: 'Fri', n: 13 },
{ d: 'Sat', n: 14 },
{ d: 'Sun', n: 15 },
];
const SLOTS = ['9 AM', '12 PM', '3 PM', '6 PM'];
const POSTS: MockPost[] = [
{ day: 0, slot: 0, net: 'instagram', img: IMG.latte, time: '09:00', title: 'Morning ritual' },
{ day: 1, slot: 1, net: 'linkedin', img: IMG.interior, time: '12:15', title: 'Studio tour recap' },
{ day: 2, slot: 0, net: 'x', img: IMG.citrus, time: '09:30', title: 'Summer menu drop' },
{ day: 0, slot: 2, net: 'tiktok', img: IMG.sneakers, time: '15:00', title: 'Unboxing in 15s' },
{ day: 3, slot: 3, net: 'youtube', img: IMG.team, time: '18:00', title: 'Behind the brand' },
{ day: 4, slot: 1, net: 'facebook', img: IMG.vase, time: '12:00', title: 'Restock alert' },
{ day: 2, slot: 2, net: 'instagram', img: IMG.cappuccino, time: '15:45', title: 'Latte art reel' },
{ day: 5, slot: 0, net: 'tiktok', img: IMG.bloodOrange, time: '10:00', title: 'Weekend recipe' },
{ day: 1, slot: 3, net: 'x', img: IMG.plant, time: '18:30', title: 'Thread: our process' },
{ day: 6, slot: 2, net: 'linkedin', img: IMG.latteArt, time: '14:00', title: 'Hiring baristas' },
{ day: 4, slot: 3, net: 'instagram', img: IMG.sneakers, time: '19:00', title: 'Drop countdown' },
{ day: 3, slot: 0, net: 'tiktok', img: IMG.latte, time: '08:45', title: 'Day in the life' },
];
const N = POSTS.length;
const CYCLE = N + 5;
const dayVis = (i: number) => (i >= 5 ? 'hidden lg:block' : i >= 3 ? 'hidden sm:block' : 'block');
export function HeroMock() {
const [t, setT] = useState(0);
useEffect(() => {
const id = window.setInterval(() => setT((v) => v + 1), 900);
return () => window.clearInterval(id);
}, []);
const visible = Math.min(t % CYCLE, N);
const tickIdx = Math.floor(t / 3) % N;
const published = t % 3 === 2;
const tick = POSTS[tickIdx];
return (
<div className="relative">
<div className="absolute -inset-x-8 -inset-y-10 -z-10 rounded-[48px] bg-gradient-to-b from-[#FF5A36]/15 via-amber-200/20 to-transparent blur-3xl" />
<div className="overflow-hidden rounded-3xl border border-stone-200/80 bg-white/90 shadow-[0_40px_80px_-32px_rgba(28,25,23,0.35),0_0_0_1px_rgba(255,255,255,0.6)_inset] backdrop-blur">
<div className="flex items-center justify-between gap-4 border-b border-stone-200/80 px-4 py-3 sm:px-6">
<div className="flex items-center gap-3">
<div className="hidden gap-1.5 sm:flex">
<span className="h-2.5 w-2.5 rounded-full bg-stone-200" />
<span className="h-2.5 w-2.5 rounded-full bg-stone-200" />
<span className="h-2.5 w-2.5 rounded-full bg-stone-200" />
</div>
<div className="flex items-center gap-2 text-sm font-medium text-stone-900">
<CalendarDays className="h-4 w-4 text-[#FF5A36]" />
June 9 – 15
</div>
<div className="hidden items-center gap-0.5 sm:flex">
<span className="rounded-md p-1 text-stone-400"><ChevronLeft className="h-4 w-4" /></span>
<span className="rounded-md p-1 text-stone-400"><ChevronRight className="h-4 w-4" /></span>
</div>
</div>
<div className="flex items-center gap-3">
<div className="hidden -space-x-2 md:flex">
<Avatar src={PEOPLE.maya.img} name={PEOPLE.maya.name} className="h-7 w-7" />
<Avatar src={PEOPLE.daniel.img} name={PEOPLE.daniel.name} className="h-7 w-7" />
<Avatar src={PEOPLE.amara.img} name={PEOPLE.amara.name} className="h-7 w-7" />
</div>
<span className="inline-flex items-center gap-1.5 rounded-full bg-stone-900 px-3 py-1.5 text-xs font-medium text-white">
<Plus className="h-3.5 w-3.5" /> New post
</span>
</div>
</div>
<div className="flex">
<div className="hidden w-14 shrink-0 border-r border-stone-100 pt-10 sm:block">
{SLOTS.map((s) => (
<div key={s} className="h-[76px] pr-2 text-right font-mono text-[10px] text-stone-400">{s}</div>
))}
</div>
<div className="grid flex-1 grid-cols-3 sm:grid-cols-5 lg:grid-cols-7">
{DAYS.map((day, di) => (
<div key={day.d} className={cn('border-r border-stone-100 last:border-r-0', dayVis(di))}>
<div className="flex h-10 items-center justify-center gap-1.5 border-b border-stone-100 text-xs">
<span className="text-stone-500">{day.d}</span>
<span className={cn('font-medium', di === 2 ? 'flex h-5 w-5 items-center justify-center rounded-full bg-[#FF5A36] text-white' : 'text-stone-900')}>
{day.n}
</span>
</div>
{SLOTS.map((s, si) => {
const pi = POSTS.findIndex((p) => p.day === di && p.slot === si);
const post = pi >= 0 ? POSTS[pi] : null;
const show = post !== null && pi < visible;
const live = post !== null && pi === tickIdx;
return (
<div key={s} className="h-[76px] border-b border-dashed border-stone-100 p-1.5 last:border-b-0">
{post && show && (
<div
key={`${t - (t % CYCLE)}-${pi}`}
className={cn(
'cad-pop flex h-full items-center gap-2 rounded-xl border bg-white p-1.5 shadow-sm transition-all duration-500',
live ? 'border-[#FF5A36]/50 ring-4 ring-[#FF5A36]/10' : 'border-stone-200/80',
)}
>
<div className="relative h-full w-10 shrink-0 overflow-hidden rounded-lg">
<img src={post.img} alt="" loading="lazy" className="h-full w-full object-cover" />
<NetworkBadge id={post.net} size="xs" className="absolute bottom-0.5 right-0.5 !h-4 !w-4 !rounded-[5px]" />
</div>
<div className="min-w-0">
<div className="truncate text-[11px] font-medium leading-tight text-stone-900">{post.title}</div>
<div className="mt-1 font-mono text-[10px] text-stone-400">{post.time}</div>
</div>
</div>
)}
</div>
);
})}
</div>
))}
</div>
</div>
</div>
<div className="absolute -bottom-6 left-4 right-4 flex justify-center sm:left-auto sm:right-8 sm:justify-end">
<div
key={tickIdx}
className="cad-pop flex items-center gap-3 rounded-2xl border border-stone-200 bg-white px-3 py-2.5 shadow-[0_16px_40px_-16px_rgba(28,25,23,0.35)]"
>
<NetworkBadge id={tick.net} size="sm" />
<div className="min-w-0">
<div className="truncate text-xs font-medium text-stone-900">{tick.title}</div>
<div className="text-[11px] text-stone-500">{networkMap[tick.net].name} · {tick.time}</div>
</div>
<span
className={cn(
'ml-2 inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors duration-300',
published ? 'bg-emerald-50 text-emerald-700' : 'bg-amber-50 text-amber-700',
)}
>
{published ? <Check className="h-3 w-3" /> : <Loader2 className="h-3 w-3 animate-spin" />}
{published ? 'Published' : 'Publishing…'}
</span>
</div>
</div>
<div className="absolute -left-6 top-24 hidden animate-float rounded-2xl border border-stone-200 bg-white px-4 py-3 shadow-[0_16px_40px_-16px_rgba(28,25,23,0.3)] xl:block">
<div className="flex items-center gap-2 text-xs text-stone-500">
<TrendingUp className="h-3.5 w-3.5 text-emerald-600" /> Reach this week
</div>
<div className="mt-1 text-xl font-medium tracking-tight text-stone-900">
+38.4<span className="text-emerald-600">%</span>
</div>
</div>
</div>
);
}
const ORBIT: { id: NetworkId; pos: string; delay: number; rot: number }[] = [
{ id: 'instagram', pos: '-left-4 top-0 sm:-left-10 sm:-top-6', delay: 0, rot: -8 },
{ id: 'x', pos: '-right-3 top-10 sm:-right-8 sm:-top-2', delay: 600, rot: 6 },
{ id: 'tiktok', pos: 'hidden sm:block -right-12 top-1/2', delay: 1200, rot: -6 },
{ id: 'linkedin', pos: 'hidden sm:block -left-14 bottom-24', delay: 300, rot: 8 },
{ id: 'youtube', pos: 'hidden md:block left-1/3 -bottom-14', delay: 900, rot: -4 },
{ id: 'facebook', pos: 'hidden lg:block right-1/4 -top-12', delay: 1500, rot: 5 },
];
export function OrbitingLogos() {
return (
<div className="pointer-events-none absolute inset-0 z-10">
{ORBIT.map((o) => (
<div key={o.id} className={cn('absolute', o.pos)}>
<div className="cad-drift" style={{ animationDelay: `${o.delay}ms` }}>
<div
className="rounded-[20px] bg-white/80 p-1.5 shadow-[0_20px_40px_-16px_rgba(28,25,23,0.35)] ring-1 ring-stone-200/70 backdrop-blur"
style={{ transform: `rotate(${o.rot}deg)` }}
>
<NetworkBadge id={o.id} size="lg" />
</div>
</div>
</div>
))}
</div>
);
}components/niceui/pages/landing/cadence/logos.tsx
import type { ComponentType } from 'react';
import { Facebook, Instagram, Linkedin, Youtube, type LucideIcon } from 'lucide-react';
import { cn } from '@/lib/cn';
type LogoProps = { className?: string };
export function XLogo({ className }: LogoProps) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
<path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" />
</svg>
);
}
export function TikTokLogo({ className }: LogoProps) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
<path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z" />
</svg>
);
}
const wrapLucide = (Icon: LucideIcon) =>
function LucideLogo({ className }: LogoProps) {
return <Icon className={className} strokeWidth={2} />;
};
export const InstagramLogo = wrapLucide(Instagram);
export const FacebookLogo = wrapLucide(Facebook);
export const LinkedinLogo = wrapLucide(Linkedin);
export const YoutubeLogo = wrapLucide(Youtube);
export type NetworkId = 'x' | 'instagram' | 'linkedin' | 'tiktok' | 'facebook' | 'youtube';
export type Network = {
id: NetworkId;
name: string;
color: string;
limit: number;
Logo: ComponentType<LogoProps>;
};
export const networks: Network[] = [
{ id: 'instagram', name: 'Instagram', color: '#E4405F', limit: 2200, Logo: InstagramLogo },
{ id: 'x', name: 'X', color: '#000000', limit: 280, Logo: XLogo },
{ id: 'linkedin', name: 'LinkedIn', color: '#0A66C2', limit: 3000, Logo: LinkedinLogo },
{ id: 'tiktok', name: 'TikTok', color: '#000000', limit: 2200, Logo: TikTokLogo },
{ id: 'facebook', name: 'Facebook', color: '#0866FF', limit: 63206, Logo: FacebookLogo },
{ id: 'youtube', name: 'YouTube', color: '#FF0000', limit: 5000, Logo: YoutubeLogo },
];
export const networkMap = Object.fromEntries(networks.map((n) => [n.id, n])) as Record<NetworkId, Network>;
export const INSTAGRAM_GRADIENT =
'radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%)';
const sizes = {
xs: { tile: 'h-5 w-5 rounded-md', icon: 'h-3 w-3' },
sm: { tile: 'h-7 w-7 rounded-lg', icon: 'h-3.5 w-3.5' },
md: { tile: 'h-10 w-10 rounded-xl', icon: 'h-5 w-5' },
lg: { tile: 'h-14 w-14 rounded-2xl', icon: 'h-7 w-7' },
};
export function NetworkBadge({
id,
size = 'md',
variant = 'solid',
className,
}: {
id: NetworkId;
size?: keyof typeof sizes;
variant?: 'solid' | 'soft';
className?: string;
}) {
const n = networkMap[id];
const s = sizes[size];
if (variant === 'soft') {
return (
<span
className={cn('inline-flex shrink-0 items-center justify-center bg-white ring-1 ring-stone-200', s.tile, className)}
style={{ color: n.color }}
aria-label={n.name}
>
<n.Logo className={s.icon} />
</span>
);
}
const background = id === 'instagram' ? INSTAGRAM_GRADIENT : n.color;
return (
<span
className={cn('relative inline-flex shrink-0 items-center justify-center text-white shadow-sm', s.tile, className)}
style={{ background }}
aria-label={n.name}
>
{id === 'tiktok' ? (
<span className={cn('relative', s.icon)}>
<TikTokLogo className="absolute inset-0 h-full w-full -translate-x-[1px] -translate-y-[1px] text-[#25F4EE]" />
<TikTokLogo className="absolute inset-0 h-full w-full translate-x-[1px] translate-y-[1px] text-[#FE2C55]" />
<TikTokLogo className="absolute inset-0 h-full w-full text-white" />
</span>
) : (
<n.Logo className={s.icon} />
)}
</span>
);
}components/niceui/pages/landing/cadence/shared.tsx
import type { ReactNode } from 'react';
import { cn } from '@/lib/cn';
import { Reveal } from '@/components/niceui/pages/landing/shared/motion';
const px = (id: number, ext: 'jpeg' | 'png' = 'jpeg', w = 400) =>
`https://images.pexels.com/photos/${id}/pexels-photo-${id}.${ext}?auto=compress&cs=tinysrgb&w=${w}`;
export const IMG = {
latte: px(15554368),
latteArt: px(8046389),
cappuccino: px(19375004),
vase: px(29904622),
interior: px(14781780),
plant: px(32473239, 'png'),
sneakers: px(19869753),
citrus: px(18815188),
bloodOrange: px(13546345),
team: px(23496704, 'jpeg', 800),
};
export const PEOPLE = {
maya: { name: 'Maya Okafor', role: 'Head of Social, Brightloom', img: px(7717254, 'jpeg', 160) },
daniel: { name: 'Daniel Reyes', role: 'Founder, Field Notes Co.', img: px(5308640, 'jpeg', 160) },
amara: { name: 'Amara Bello', role: 'Content Lead, Oatside', img: px(6497112, 'jpeg', 160) },
theo: { name: 'Theo Lindqvist', role: 'Creative Director, Northpaw', img: px(35681211, 'jpeg', 160) },
jonah: { name: 'Jonah Whit', role: 'Growth, Parcel', img: px(39977028, 'jpeg', 160) },
lena: { name: 'Lena Marsh', role: 'Agency Partner, Marsh & Co.', img: px(34930167, 'jpeg', 160) },
kofi: { name: 'Kofi Mensah', role: 'Social Producer, Tonic', img: px(14950779, 'jpeg', 160) },
zara: { name: 'Zara Nolan', role: 'Brand Manager, Velo', img: px(6497114, 'jpeg', 160) },
};
export const ACCENT = '#FF5A36';
export function scrollToId(id: string) {
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
export function Eyebrow({ children, className }: { children: ReactNode; className?: string }) {
return (
<span
className={cn(
'inline-flex items-center gap-2 rounded-full border border-stone-200 bg-white/70 px-3 py-1 text-xs font-medium tracking-wide text-stone-600 backdrop-blur',
className,
)}
>
<span className="h-1.5 w-1.5 rounded-full bg-[#FF5A36]" />
{children}
</span>
);
}
export function SectionHeading({
eyebrow,
title,
accent,
body,
align = 'center',
}: {
eyebrow: string;
title: string;
accent: string;
body: string;
align?: 'center' | 'left';
}) {
return (
<div className={cn('max-w-2xl', align === 'center' ? 'mx-auto text-center' : '')}>
<Reveal>
<Eyebrow>{eyebrow}</Eyebrow>
</Reveal>
<Reveal delay={80}>
<h2 className="mt-4 text-4xl font-medium leading-[1.1] tracking-[-0.03em] text-stone-900 sm:text-5xl">
{title} <span className="font-serif font-normal italic tracking-normal text-[#FF5A36]">{accent}</span>
</h2>
</Reveal>
<Reveal delay={160}>
<p className="mt-4 text-base leading-relaxed text-stone-600 sm:text-lg">{body}</p>
</Reveal>
</div>
);
}
export function Avatar({ src, name, className }: { src: string; name: string; className?: string }) {
return (
<img
src={src}
alt={name}
loading="lazy"
className={cn('h-8 w-8 shrink-0 rounded-full object-cover ring-2 ring-white', className)}
/>
);
}
export function PrimaryButton({ children, onClick, className }: { children: ReactNode; onClick?: () => void; className?: string }) {
return (
<button
type="button"
onClick={onClick}
className={cn(
'group relative inline-flex items-center justify-center gap-2 overflow-hidden rounded-full bg-stone-900 px-6 py-3 text-sm font-medium text-white shadow-[0_8px_24px_-8px_rgba(28,25,23,0.6)] transition-all duration-300 ease-spring hover:-translate-y-0.5 hover:bg-[#FF5A36] hover:shadow-[0_12px_32px_-8px_rgba(255,90,54,0.6)] active:translate-y-0 active:scale-[0.98]',
className,
)}
>
{children}
</button>
);
}
export function GhostButton({ children, onClick, className }: { children: ReactNode; onClick?: () => void; className?: string }) {
return (
<button
type="button"
onClick={onClick}
className={cn(
'inline-flex items-center justify-center gap-2 rounded-full border border-stone-300 bg-white/70 px-6 py-3 text-sm font-medium text-stone-800 backdrop-blur transition-all duration-300 ease-spring hover:-translate-y-0.5 hover:border-stone-900 hover:bg-white active:translate-y-0 active:scale-[0.98]',
className,
)}
>
{children}
</button>
);
}components/niceui/pages/landing/shared/motion.tsx
import { type ReactNode, useEffect, useRef, useState } from 'react';
import { cn } from '@/lib/cn';
export function useInView<T extends Element>(threshold = 0.2) {
const ref = useRef<T>(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setInView(true);
io.disconnect();
}
},
{ threshold },
);
io.observe(el);
return () => io.disconnect();
}, [threshold]);
return { ref, inView };
}
export function Reveal({ children, delay = 0, className }: { children: ReactNode; delay?: number; className?: string }) {
const { ref, inView } = useInView<HTMLDivElement>(0.15);
return (
<div
ref={ref}
className={cn('transition-all duration-1000 ease-smooth', inView ? 'translate-y-0 opacity-100 blur-0' : 'translate-y-6 opacity-0 blur-[2px]', className)}
style={{ transitionDelay: `${delay}ms` }}
>
{children}
</div>
);
}
export function useCountUp(target: number, active: boolean, duration = 1800) {
const [val, setVal] = useState(0);
useEffect(() => {
if (!active) return;
let raf = 0;
const start = performance.now();
const loop = (now: number) => {
const t = Math.min(1, (now - start) / duration);
setVal(target * (1 - Math.pow(2, -10 * t)));
if (t < 1) raf = requestAnimationFrame(loop);
else setVal(target);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [target, active, duration]);
return val;
}components/niceui/pages/landing/cadence/NetworkMarquee.tsx
import { NetworkBadge, networks } from './logos';
import { Reveal } from '@/components/niceui/pages/landing/shared/motion';
const STATS: Record<string, string> = {
instagram: 'Posts, Reels, Stories',
x: 'Posts & threads',
linkedin: 'Profiles & pages',
tiktok: 'Videos & carousels',
facebook: 'Pages & groups',
youtube: 'Videos & Shorts',
};
export function NetworkMarquee() {
const row = [...networks, ...networks];
return (
<section className="relative border-y border-stone-200/80 bg-white/60 py-12">
<Reveal>
<p className="text-center text-sm text-stone-500">
Publish everywhere your audience is{' '}
<span className="font-serif text-base italic text-stone-900">— from one calm place.</span>
</p>
</Reveal>
<div className="group relative mt-8 overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]">
<div className="flex w-max animate-marquee gap-4 group-hover:[animation-play-state:paused]">
{[...row, ...row].map((n, i) => (
<div
key={`${n.id}-${i}`}
className="flex items-center gap-3 rounded-2xl border border-stone-200 bg-white px-4 py-3 shadow-sm transition-all duration-300 ease-spring hover:-translate-y-1 hover:shadow-[0_12px_24px_-12px_rgba(28,25,23,0.25)]"
>
<NetworkBadge id={n.id} size="md" />
<div>
<div className="text-sm font-medium text-stone-900">{n.name}</div>
<div className="text-xs text-stone-500">{STATS[n.id]}</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}components/niceui/pages/landing/cadence/Composer.tsx
import { useEffect, useRef, useState } from 'react';
import { Check, Hash, ImageIcon, Smile, Sparkles, Wand2 } from 'lucide-react';
import { cn } from '@/lib/cn';
import { Reveal } from '@/components/niceui/pages/landing/shared/motion';
import { NetworkBadge, networkMap, type NetworkId } from './logos';
import { SectionHeading } from './shared';
import { InstagramPreview, LinkedInPreview, TikTokPreview, XPreview } from './Previews';
const COMPOSER_NETS: NetworkId[] = ['x', 'instagram', 'linkedin', 'tiktok'];
type Tone = 'Witty' | 'Professional' | 'Casual';
const REWRITES: Record<Tone, string> = {
Witty:
'Our new cold brew is so smooth it should come with a warning label. Steeped 18 hours, gone in 18 seconds. First 50 cups are on us this Saturday — set an alarm. #coldbrew',
Professional:
'Introducing our 18-hour slow-steeped cold brew: a smoother body, lower acidity and beans sourced directly from our partner farm in Huila, Colombia. Available in every store from Saturday. #specialtycoffee',
Casual:
'ok we finally did it. cold brew, steeped for 18 hours, ridiculously smooth. swing by saturday — first 50 cups are on the house #coffeetime',
};
const DEFAULT_TEXT =
'Big news: our 18-hour cold brew lands this Saturday. Smoother, bolder and brewed in small batches. First 50 cups are on us!';
const HASHTAGS = ['#coldbrew', '#coffeelover', '#smallbatch', '#weekendvibes', '#localcafe', '#specialtycoffee'];
function Counter({ id, len }: { id: NetworkId; len: number }) {
const limit = networkMap[id].limit;
const ratio = Math.min(1, len / limit);
const left = limit - len;
const r = 9;
const c = 2 * Math.PI * r;
const tone = left < 0 ? '#E11D48' : ratio > 0.85 ? '#D97706' : '#FF5A36';
return (
<div className="flex items-center gap-2 rounded-xl border border-stone-200 bg-white px-2.5 py-2">
<NetworkBadge id={id} size="xs" />
<svg viewBox="0 0 24 24" className="h-5 w-5 -rotate-90">
<circle cx="12" cy="12" r={r} fill="none" stroke="#E7E5E4" strokeWidth="3" />
<circle
cx="12"
cy="12"
r={r}
fill="none"
stroke={tone}
strokeWidth="3"
strokeLinecap="round"
strokeDasharray={c}
strokeDashoffset={c * (1 - ratio)}
className="transition-all duration-300 ease-smooth"
/>
</svg>
<span className={cn('font-mono text-xs tabular-nums', left < 0 ? 'text-rose-600' : 'text-stone-600')}>
{left.toLocaleString()}
</span>
</div>
);
}
export function Composer() {
const [text, setText] = useState(DEFAULT_TEXT);
const [active, setActive] = useState<NetworkId[]>(['x', 'instagram', 'linkedin', 'tiktok']);
const [tone, setTone] = useState<Tone>('Witty');
const [streaming, setStreaming] = useState(false);
const timer = useRef<number | undefined>(undefined);
useEffect(() => () => window.clearInterval(timer.current), []);
const toggle = (id: NetworkId) =>
setActive((cur) => (cur.includes(id) ? (cur.length > 1 ? cur.filter((x) => x !== id) : cur) : [...cur, id]));
const rewrite = () => {
if (streaming) return;
const target = REWRITES[tone];
let i = 0;
setStreaming(true);
setText('');
window.clearInterval(timer.current);
timer.current = window.setInterval(() => {
i += 3;
setText(target.slice(0, i));
if (i >= target.length) {
window.clearInterval(timer.current);
setStreaming(false);
}
}, 22);
};
const addTag = (tag: string) => {
if (streaming) return;
setText((t) => (t.includes(tag) ? t.replace(new RegExp(`\\s?${tag}\\b`), '') : `${t.trimEnd()} ${tag}`));
};
const ordered = COMPOSER_NETS.filter((n) => active.includes(n));
return (
<section id="composer" className="scroll-mt-20 py-24 sm:py-32">
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<SectionHeading
eyebrow="Composer"
title="Write once."
accent="Look native everywhere."
body="Draft a single post and watch it adapt to every network in real time — character limits, truncation, hashtags and all. Then let AI tune the voice."
/>
<div className="mt-16 grid gap-8 lg:grid-cols-[minmax(0,5fr)_minmax(0,6fr)]">
<Reveal>
<div className="rounded-3xl border border-stone-200 bg-white p-4 shadow-[0_24px_48px_-24px_rgba(28,25,23,0.25)] sm:p-6 lg:sticky lg:top-24">
<div className="flex flex-wrap gap-2">
{COMPOSER_NETS.map((id) => {
const on = active.includes(id);
return (
<button
key={id}
type="button"
onClick={() => toggle(id)}
aria-pressed={on}
className={cn(
'group flex items-center gap-2 rounded-full border py-1.5 pl-1.5 pr-3 text-sm transition-all duration-300 ease-spring active:scale-95',
on ? 'border-stone-900 bg-stone-900 text-white' : 'border-stone-200 bg-white text-stone-500 hover:border-stone-400',
)}
>
<NetworkBadge id={id} size="sm" className={cn('transition-all duration-300', !on && 'opacity-40 grayscale')} />
{networkMap[id].name}
<span
className={cn(
'flex h-4 w-4 items-center justify-center rounded-full transition-all duration-300',
on ? 'scale-100 bg-[#FF5A36]' : 'scale-0',
)}
>
<Check className="h-3 w-3 text-white" strokeWidth={3} />
</span>
</button>
);
})}
</div>
<div
className={cn(
'relative mt-4 rounded-2xl border bg-stone-50/60 transition-all duration-300',
streaming ? 'border-[#FF5A36]/50 ring-4 ring-[#FF5A36]/10' : 'border-stone-200 focus-within:border-stone-400',
)}
>
<label htmlFor="cad-composer" className="sr-only">Post text</label>
<textarea
id="cad-composer"
value={text}
onChange={(e) => setText(e.target.value)}
readOnly={streaming}
rows={6}
placeholder="What do you want to share?"
className="block w-full resize-none bg-transparent p-4 text-[15px] leading-relaxed text-stone-900 placeholder:text-stone-400 focus:outline-none"
/>
{streaming && (
<div className="pointer-events-none absolute inset-0 overflow-hidden rounded-2xl">
<div className="absolute inset-0 animate-shimmer bg-[linear-gradient(110deg,transparent_30%,rgba(255,90,54,0.08)_50%,transparent_70%)] bg-[length:200%_100%]" />
</div>
)}
<div className="flex items-center justify-between border-t border-stone-200/80 px-3 py-2">
<div className="flex gap-1 text-stone-400">
<span className="rounded-lg p-1.5 hover:bg-stone-200/60 hover:text-stone-700"><ImageIcon className="h-4 w-4" /></span>
<span className="rounded-lg p-1.5 hover:bg-stone-200/60 hover:text-stone-700"><Smile className="h-4 w-4" /></span>
<span className="rounded-lg p-1.5 hover:bg-stone-200/60 hover:text-stone-700"><Hash className="h-4 w-4" /></span>
</div>
<span className="font-mono text-xs text-stone-400">{text.length} chars</span>
</div>
</div>
<div className="mt-4 flex flex-wrap gap-2">
{ordered.map((id) => (
<Counter key={id} id={id} len={text.length} />
))}
</div>
<div className="mt-6 rounded-2xl bg-gradient-to-br from-[#FFF3EE] to-amber-50 p-4">
<div className="flex items-center gap-2 text-sm font-medium text-stone-900">
<Sparkles className="h-4 w-4 text-[#FF5A36]" /> AI rewrite
</div>
<div className="mt-3 flex flex-wrap items-center gap-2">
{(Object.keys(REWRITES) as Tone[]).map((t) => (
<button
key={t}
type="button"
onClick={() => setTone(t)}
className={cn(
'rounded-full px-3 py-1.5 text-xs font-medium transition-all duration-200',
tone === t ? 'bg-white text-stone-900 shadow-sm ring-1 ring-stone-900/10' : 'text-stone-600 hover:bg-white/60',
)}
>
{t}
</button>
))}
<button
type="button"
onClick={rewrite}
disabled={streaming}
className="ml-auto inline-flex items-center gap-1.5 rounded-full bg-[#FF5A36] px-4 py-2 text-xs font-medium text-white shadow-[0_8px_20px_-8px_rgba(255,90,54,0.8)] transition-all duration-300 ease-spring hover:-translate-y-0.5 disabled:translate-y-0 disabled:opacity-70"
>
<Wand2 className={cn('h-3.5 w-3.5', streaming && 'animate-spin-slow')} />
{streaming ? 'Writing…' : 'Rewrite'}
</button>
</div>
</div>
<div className="mt-6">
<div className="text-xs font-medium uppercase tracking-wider text-stone-500">Suggested hashtags</div>
<div className="mt-3 flex flex-wrap gap-2">
{HASHTAGS.map((tag) => {
const on = new RegExp(`${tag}\\b`).test(text);
return (
<button
key={tag}
type="button"
onClick={() => addTag(tag)}
className={cn(
'rounded-full border px-3 py-1 text-xs transition-all duration-200 active:scale-95',
on ? 'border-emerald-300 bg-emerald-50 text-emerald-800' : 'border-stone-200 text-stone-600 hover:border-stone-400 hover:text-stone-900',
)}
>
{tag}
</button>
);
})}
</div>
</div>
</div>
</Reveal>
<Reveal delay={120}>
<div className="columns-1 gap-4 sm:columns-2">
{ordered.map((id) => {
const props = { text: text || ' ', streaming };
if (id === 'x') return <XPreview key={id} {...props} />;
if (id === 'instagram') return <InstagramPreview key={id} {...props} />;
if (id === 'linkedin') return <LinkedInPreview key={id} {...props} />;
return <TikTokPreview key={id} {...props} />;
})}
</div>
</Reveal>
</div>
</div>
</section>
);
}components/niceui/pages/landing/cadence/Previews.tsx
import type { ReactNode } from 'react';
import {
BadgeCheck,
BarChart2,
Bookmark,
Globe2,
Heart,
MessageCircle,
MoreHorizontal,
Music2,
Repeat2,
Send,
Share,
ThumbsUp,
type LucideIcon,
} from 'lucide-react';
import { cn } from '@/lib/cn';
import { IMG, PEOPLE } from './shared';
import { NetworkBadge, networkMap, type NetworkId } from './logos';
const LI_ACTIONS: [LucideIcon, string][] = [
[ThumbsUp, 'Like'],
[MessageCircle, 'Comment'],
[Repeat2, 'Repost'],
[Send, 'Send'],
];
const TT_ACTIONS: [LucideIcon, string][] = [
[Heart, '84K'],
[MessageCircle, '1.2K'],
[Bookmark, '9.8K'],
[Share, '3.1K'],
];
const BRAND = { name: 'Cadence Coffee', handle: 'cadencecoffee', img: PEOPLE.theo.img };
function Rich({ text, tagClass }: { text: string; tagClass: string }) {
const parts = text.split(/(\s+)/);
return (
<>
{parts.map((p, i) =>
/^[#@]\w+/.test(p) ? (
<span key={i} className={tagClass}>{p}</span>
) : (
<span key={i}>{p}</span>
),
)}
</>
);
}
function Shell({ id, children, className }: { id: NetworkId; children: ReactNode; className?: string }) {
return (
<div className={cn('cad-pop mb-4 break-inside-avoid overflow-hidden rounded-2xl border border-stone-200 bg-white shadow-[0_12px_32px_-16px_rgba(28,25,23,0.25)]', className)}>
<div className="flex items-center gap-2 border-b border-stone-100 bg-stone-50/70 px-3 py-2 text-[11px] font-medium text-stone-500">
<NetworkBadge id={id} size="xs" />
{networkMap[id].name} preview
</div>
{children}
</div>
);
}
function Caret({ on }: { on: boolean }) {
return on ? <span className="ml-0.5 inline-block h-[1em] w-[2px] translate-y-[2px] animate-blink bg-[#FF5A36]" /> : null;
}
export function XPreview({ text, streaming }: { text: string; streaming: boolean }) {
const limit = networkMap.x.limit;
const ok = text.slice(0, limit);
const over = text.slice(limit);
return (
<Shell id="x">
<div className="flex gap-3 p-4">
<img src={BRAND.img} alt="" className="h-10 w-10 shrink-0 rounded-full object-cover" />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1 text-[15px] leading-tight">
<span className="truncate font-semibold text-stone-950">{BRAND.name}</span>
<BadgeCheck className="h-4 w-4 shrink-0 fill-sky-500 text-white" />
<span className="truncate text-stone-500">@{BRAND.handle} · 2m</span>
</div>
<p className="mt-1 whitespace-pre-wrap break-words text-[15px] leading-normal text-stone-900">
<Rich text={ok} tagClass="text-sky-600" />
{over && <span className="rounded bg-rose-100 text-rose-700">{over}</span>}
<Caret on={streaming} />
</p>
<img src={IMG.latteArt} alt="" className="mt-3 aspect-[16/10] w-full rounded-2xl border border-stone-200 object-cover" />
<div className="mt-3 flex justify-between pr-4 text-[13px] text-stone-500">
<span className="flex items-center gap-1.5"><MessageCircle className="h-4 w-4" />24</span>
<span className="flex items-center gap-1.5"><Repeat2 className="h-4 w-4" />118</span>
<span className="flex items-center gap-1.5"><Heart className="h-4 w-4" />1.2K</span>
<span className="flex items-center gap-1.5"><BarChart2 className="h-4 w-4" />48K</span>
</div>
</div>
</div>
</Shell>
);
}
export function InstagramPreview({ text, streaming }: { text: string; streaming: boolean }) {
const short = text.length > 125 ? text.slice(0, 125).trimEnd() : text;
return (
<Shell id="instagram">
<div className="flex items-center gap-2.5 px-3 py-2.5">
<span className="rounded-full bg-[conic-gradient(from_180deg,#feda75,#fa7e1e,#d62976,#feda75)] p-[2px]">
<img src={BRAND.img} alt="" className="h-8 w-8 rounded-full border-2 border-white object-cover" />
</span>
<div className="flex-1 text-sm font-semibold text-stone-950">{BRAND.handle}</div>
<MoreHorizontal className="h-5 w-5 text-stone-700" />
</div>
<img src={IMG.latte} alt="" className="aspect-square w-full object-cover" />
<div className="px-3 pb-4 pt-3">
<div className="flex items-center gap-4 text-stone-900">
<Heart className="h-6 w-6 transition-transform hover:scale-110" />
<MessageCircle className="h-6 w-6" />
<Send className="h-6 w-6" />
<Bookmark className="ml-auto h-6 w-6" />
</div>
<div className="mt-2 text-sm font-semibold text-stone-950">2,481 likes</div>
<p className="mt-1 whitespace-pre-wrap break-words text-sm leading-normal text-stone-900">
<span className="mr-1.5 font-semibold">{BRAND.handle}</span>
<Rich text={short} tagClass="text-sky-800" />
{short.length < text.length && <span className="text-stone-500">… more</span>}
<Caret on={streaming} />
</p>
</div>
</Shell>
);
}
export function LinkedInPreview({ text, streaming }: { text: string; streaming: boolean }) {
const short = text.length > 210 ? text.slice(0, 210).trimEnd() : text;
return (
<Shell id="linkedin">
<div className="flex gap-2.5 px-4 pt-4">
<img src={BRAND.img} alt="" className="h-12 w-12 rounded-md object-cover" />
<div className="min-w-0 leading-tight">
<div className="text-sm font-semibold text-stone-950">{BRAND.name}</div>
<div className="text-xs text-stone-500">12,408 followers</div>
<div className="mt-0.5 flex items-center gap-1 text-xs text-stone-500">2m · <Globe2 className="h-3 w-3" /></div>
</div>
</div>
<p className="whitespace-pre-wrap break-words px-4 py-3 text-sm leading-normal text-stone-900">
<Rich text={short} tagClass="font-semibold text-[#0A66C2]" />
{short.length < text.length && <span className="text-stone-500">…see more</span>}
<Caret on={streaming} />
</p>
<img src={IMG.interior} alt="" className="aspect-[1.91/1] w-full object-cover" />
<div className="flex items-center gap-1 px-4 py-2 text-xs text-stone-500">
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-[#0A66C2]"><ThumbsUp className="h-2.5 w-2.5 fill-white text-white" /></span>
<span className="-ml-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-rose-500"><Heart className="h-2.5 w-2.5 fill-white text-white" /></span>
<span className="ml-1">342 · 28 comments</span>
</div>
<div className="grid grid-cols-4 border-t border-stone-100 px-2 py-1 text-xs font-medium text-stone-600">
{LI_ACTIONS.map(([Icon, label]) => (
<span key={label} className="flex items-center justify-center gap-1.5 rounded-md py-2 hover:bg-stone-100">
<Icon className="h-4 w-4" />
<span className="hidden sm:inline">{label}</span>
</span>
))}
</div>
</Shell>
);
}
export function TikTokPreview({ text, streaming }: { text: string; streaming: boolean }) {
const short = text.length > 150 ? `${text.slice(0, 150).trimEnd()}…` : text;
return (
<Shell id="tiktok">
<div className="relative aspect-[9/14] bg-stone-950">
<img src={IMG.cappuccino} alt="" className="absolute inset-0 h-full w-full object-cover opacity-90" />
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/10 to-black/30" />
<div className="absolute inset-x-0 top-3 flex justify-center gap-4 text-sm font-semibold text-white/70">
<span>Following</span>
<span className="text-white underline decoration-2 underline-offset-8">For You</span>
</div>
<div className="absolute bottom-20 right-3 flex flex-col items-center gap-4 text-white">
<img src={BRAND.img} alt="" className="h-10 w-10 rounded-full border-2 border-white object-cover" />
{TT_ACTIONS.map(([Icon, n]) => (
<span key={n} className="flex flex-col items-center text-[11px] font-semibold">
<Icon className="h-7 w-7 fill-white/95 drop-shadow" />
{n}
</span>
))}
</div>
<div className="absolute inset-x-0 bottom-0 p-4 pr-16 text-white">
<div className="text-sm font-semibold">@{BRAND.handle}</div>
<p className="mt-1 whitespace-pre-wrap break-words text-[13px] leading-snug">
<Rich text={short} tagClass="font-semibold" />
<Caret on={streaming} />
</p>
<div className="mt-2 flex items-center gap-1.5 text-xs">
<Music2 className="h-3.5 w-3.5" />
<span className="truncate">original sound · Cadence Coffee</span>
</div>
</div>
</div>
</Shell>
);
}components/niceui/pages/landing/cadence/Scheduling.tsx
import { useState } from 'react';
import { ArrowDown, ArrowUp, Clock, Sparkles, Zap } from 'lucide-react';
import { cn } from '@/lib/cn';
import { Reveal, useInView } from '@/components/niceui/pages/landing/shared/motion';
import { NetworkBadge, networkMap, type NetworkId } from './logos';
import { IMG, SectionHeading } from './shared';
const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const HOURS = Array.from({ length: 16 }, (_, i) => i + 6);
const HEAT_NETS: NetworkId[] = ['instagram', 'linkedin', 'tiktok'];
const PEAKS: Record<string, { h: number; w: number }[]> = {
instagram: [{ h: 8, w: 0.6 }, { h: 12, w: 0.75 }, { h: 19, w: 1 }],
linkedin: [{ h: 8, w: 1 }, { h: 12, w: 0.8 }, { h: 17, w: 0.55 }],
tiktok: [{ h: 13, w: 0.5 }, { h: 20, w: 1 }, { h: 21, w: 0.9 }],
};
function score(net: NetworkId, d: number, h: number) {
const weekend = d >= 5;
const dayW = net === 'linkedin' ? (weekend ? 0.35 : [0.9, 1, 0.95, 0.9, 0.7][d]) : weekend ? 1 : 0.8 + (d % 3) * 0.06;
const peak = PEAKS[net].reduce((acc, p) => acc + p.w * Math.exp(-((h - p.h) ** 2) / 3.2), 0);
const jitter = ((d * 7 + h * 13) % 9) / 60;
return Math.max(0.05, Math.min(1, (peak * 0.85 + jitter) * dayW));
}
const fmtHour = (h: number) => `${((h + 11) % 12) + 1} ${h < 12 ? 'AM' : 'PM'}`;
function Heatmap() {
const [net, setNet] = useState<NetworkId>('instagram');
const [hover, setHover] = useState<{ d: number; h: number } | null>(null);
const { ref, inView } = useInView<HTMLDivElement>(0.25);
let best = { d: 0, h: 6, v: 0 };
DAYS.forEach((_, d) =>
HOURS.forEach((h) => {
const v = score(net, d, h);
if (v > best.v) best = { d, h, v };
}),
);
return (
<div ref={ref} className="rounded-3xl border border-stone-200 bg-white p-4 shadow-[0_24px_48px_-24px_rgba(28,25,23,0.25)] sm:p-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<div className="text-sm font-medium text-stone-900">Best time to post</div>
<div className="text-xs text-stone-500">Based on 90 days of your audience activity</div>
</div>
<div className="flex rounded-full bg-stone-100 p-1">
{HEAT_NETS.map((id) => (
<button
key={id}
type="button"
onClick={() => setNet(id)}
className={cn(
'flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-all duration-300',
net === id ? 'bg-white text-stone-900 shadow-sm' : 'text-stone-500 hover:text-stone-800',
)}
>
<NetworkBadge id={id} size="xs" />
<span className="hidden sm:inline">{networkMap[id].name}</span>
</button>
))}
</div>
</div>
<div className="-mx-2 mt-6 overflow-x-auto px-2 pb-2 pt-1">
<div className="min-w-[560px]">
<div className="grid grid-cols-[40px_repeat(16,minmax(0,1fr))] gap-1">
<span />
{HOURS.map((h) => (
<span key={h} className="text-center font-mono text-[10px] text-stone-400">
{h % 3 === 0 ? fmtHour(h).replace(' ', '') : ''}
</span>
))}
{DAYS.map((day, d) => (
<div key={day} className="contents">
<span className="flex items-center text-xs text-stone-500">{day}</span>
{HOURS.map((h, hi) => {
const v = score(net, d, h);
const isHover = hover?.d === d && hover.h === h;
const isBest = best.d === d && best.h === h;
return (
<button
key={h}
type="button"
onMouseEnter={() => setHover({ d, h })}
onMouseLeave={() => setHover(null)}
onFocus={() => setHover({ d, h })}
onBlur={() => setHover(null)}
aria-label={`${day} ${fmtHour(h)}: score ${Math.round(v * 100)}`}
className="relative aspect-square rounded-[6px] bg-stone-100"
>
<span
className={cn(
'absolute inset-0 rounded-[6px] transition-all duration-700 ease-spring',
inView ? 'scale-100' : 'scale-0',
isHover && 'scale-110 ring-2 ring-stone-900',
)}
style={{
backgroundColor: `rgba(255,90,54,${(0.08 + v * 0.92).toFixed(2)})`,
transitionDelay: inView && !isHover ? `${(d * 16 + hi) * 6}ms` : '0ms',
}}
/>
{isBest && inView && <span className="absolute inset-0 animate-pulse-ring rounded-[6px] ring-2 ring-[#FF5A36]" />}
{isHover && (
<span
className={cn(
'pointer-events-none absolute left-1/2 z-20 w-max -translate-x-1/2 animate-scale-in rounded-xl bg-stone-900 px-3 py-2 text-left text-xs text-white shadow-xl',
d < 2 ? 'top-full mt-2' : 'bottom-full mb-2',
)}
>
<span className="block font-medium">{day} · {fmtHour(h)}</span>
<span className="block text-stone-300">Engagement score {Math.round(v * 100)}</span>
</span>
)}
</button>
);
})}
</div>
))}
</div>
</div>
</div>
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2 rounded-full bg-[#FFF3EE] px-3 py-1.5 text-xs text-stone-800">
<Zap className="h-3.5 w-3.5 text-[#FF5A36]" />
Peak for {networkMap[net].name}: <span className="font-medium">{DAYS[best.d]} at {fmtHour(best.h)}</span>
</div>
<div className="flex items-center gap-2 text-[11px] text-stone-500">
Low
<span className="h-2 w-24 rounded-full bg-gradient-to-r from-[#FF5A36]/10 to-[#FF5A36]" />
High
</div>
</div>
</div>
);
}
type QItem = { id: number; title: string; net: NetworkId; img: string };
const SLOT_TIMES = ['Today · 9:00 AM', 'Today · 12:30 PM', 'Today · 7:00 PM', 'Tomorrow · 8:15 AM', 'Tomorrow · 6:45 PM'];
const ROW = 76;
function Queue() {
const [items, setItems] = useState<QItem[]>([
{ id: 1, title: 'Cold brew launch teaser', net: 'instagram', img: IMG.latte },
{ id: 2, title: 'Why we roast in small batches', net: 'linkedin', img: IMG.interior },
{ id: 3, title: '15s pour-over ASMR', net: 'tiktok', img: IMG.cappuccino },
{ id: 4, title: 'Thread: 5 brewing myths', net: 'x', img: IMG.plant },
{ id: 5, title: 'Summer citrus tonic', net: 'facebook', img: IMG.citrus },
]);
const [moved, setMoved] = useState<number | null>(null);
const move = (idx: number, dir: -1 | 1) => {
const j = idx + dir;
if (j < 0 || j >= items.length) return;
const next = [...items];
[next[idx], next[j]] = [next[j], next[idx]];
setItems(next);
setMoved(items[idx].id);
};
return (
<div className="rounded-3xl border border-stone-200 bg-white p-4 shadow-[0_24px_48px_-24px_rgba(28,25,23,0.25)] sm:p-6">
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium text-stone-900">Queue</div>
<div className="text-xs text-stone-500">Reorder — posts snap into your best slots</div>
</div>
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-50 px-2.5 py-1 text-[11px] font-medium text-emerald-700">
<Sparkles className="h-3 w-3" /> Auto-slotting
</span>
</div>
<div className="relative mt-5" style={{ height: items.length * ROW }}>
{[...items].sort((a, b) => a.id - b.id).map((it) => {
const idx = items.findIndex((x) => x.id === it.id);
return (
<div
key={it.id}
className="absolute inset-x-0 transition-transform duration-500 ease-spring"
style={{ transform: `translateY(${idx * ROW}px)`, height: ROW - 8 }}
>
<div
className={cn(
'flex h-full items-center gap-3 rounded-2xl border bg-white px-2.5 transition-all duration-500',
moved === it.id ? 'border-[#FF5A36]/40 shadow-[0_12px_24px_-12px_rgba(255,90,54,0.45)]' : 'border-stone-200 hover:border-stone-300',
)}
>
<div className="relative h-12 w-12 shrink-0">
<img src={it.img} alt="" loading="lazy" className="h-full w-full rounded-xl object-cover" />
<NetworkBadge id={it.net} size="xs" className="absolute -bottom-1 -right-1 ring-2 ring-white" />
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-stone-900">{it.title}</div>
<div className="mt-0.5 flex items-center gap-1 font-mono text-[11px] text-stone-500">
<Clock className="h-3 w-3" /> {SLOT_TIMES[idx]}
</div>
</div>
<div className="flex flex-col gap-0.5">
<button
type="button"
onClick={() => move(idx, -1)}
disabled={idx === 0}
aria-label={`Move ${it.title} earlier`}
className="rounded-md p-1 text-stone-500 transition-colors hover:bg-stone-100 hover:text-stone-900 disabled:opacity-30"
>
<ArrowUp className="h-3.5 w-3.5" />
</button>
<button
type="button"
onClick={() => move(idx, 1)}
disabled={idx === items.length - 1}
aria-label={`Move ${it.title} later`}
className="rounded-md p-1 text-stone-500 transition-colors hover:bg-stone-100 hover:text-stone-900 disabled:opacity-30"
>
<ArrowDown className="h-3.5 w-3.5" />
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
);
}
export function Scheduling() {
return (
<section id="scheduling" className="relative scroll-mt-20 bg-[#F4F1EA] py-24 sm:py-32">
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<SectionHeading
eyebrow="Smart scheduling"
title="Publish when they're"
accent="actually listening."
body="Cadence learns when each audience is most active and quietly slots every post into the moment it'll land hardest."
/>
<div className="mt-16 grid gap-6 lg:grid-cols-[minmax(0,7fr)_minmax(0,5fr)]">
<Reveal>
<Heatmap />
</Reveal>
<Reveal delay={120}>
<Queue />
</Reveal>
</div>
</div>
</section>
);
}components/niceui/pages/landing/cadence/Analytics.tsx
import { useMemo, useState, type MouseEvent } from 'react';
import { ArrowUpRight, Eye, Heart, MousePointerClick, Users } from 'lucide-react';
import { cn } from '@/lib/cn';
import { Reveal, useCountUp, useInView } from '@/components/niceui/pages/landing/shared/motion';
import { NetworkBadge, networkMap, type NetworkId } from './logos';
import { IMG, SectionHeading } from './shared';
const KPIS = [
{ label: 'Impressions', value: 2.48, suffix: 'M', decimals: 2, delta: '+24.1%', Icon: Eye },
{ label: 'Engagement rate', value: 6.2, suffix: '%', decimals: 1, delta: '+1.8 pts', Icon: Heart },
{ label: 'Followers gained', value: 18420, suffix: '', decimals: 0, delta: '+12.6%', Icon: Users },
{ label: 'Link clicks', value: 42910, suffix: '', decimals: 0, delta: '+31.0%', Icon: MousePointerClick },
];
const RANGES = { '7D': 7, '30D': 30, '90D': 45 } as const;
type Range = keyof typeof RANGES;
const W = 640;
const H = 220;
const PAD = 8;
function series(n: number, seed: number, lift: number) {
return Array.from({ length: n }, (_, i) => {
const t = i / (n - 1);
const wave = Math.sin(i * 0.7 + seed) * 0.08 + Math.sin(i * 0.23 + seed * 2) * 0.12;
return Math.max(0.08, Math.min(0.95, 0.25 + t * lift + wave));
});
}
function smoothPath(pts: [number, number][]) {
return pts.reduce((d, p, i, a) => {
if (i === 0) return `M${p[0]},${p[1]}`;
const p0 = a[i - 2] ?? a[i - 1];
const p1 = a[i - 1];
const p2 = p;
const p3 = a[i + 1] ?? p;
const c1 = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
const c2 = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
return `${d} C${c1[0]},${c1[1]} ${c2[0]},${c2[1]} ${p2[0]},${p2[1]}`;
}, '');
}
function Kpi({ k, active, delay }: { k: (typeof KPIS)[number]; active: boolean; delay: number }) {
const v = useCountUp(k.value, active, 1800 + delay);
const shown = k.decimals ? v.toFixed(k.decimals) : Math.round(v).toLocaleString();
return (
<div className="group rounded-2xl border border-stone-200 bg-white p-4 transition-all duration-300 ease-spring hover:-translate-y-1 hover:shadow-[0_16px_32px_-16px_rgba(28,25,23,0.25)] sm:p-5">
<div className="flex items-center justify-between">
<span className="flex h-8 w-8 items-center justify-center rounded-xl bg-[#FFF3EE] text-[#FF5A36] transition-transform duration-300 group-hover:scale-110">
<k.Icon className="h-4 w-4" />
</span>
<span className="inline-flex items-center gap-0.5 rounded-full bg-emerald-50 px-2 py-0.5 text-[11px] font-medium text-emerald-700">
<ArrowUpRight className="h-3 w-3" />
{k.delta}
</span>
</div>
<div className="mt-4 text-2xl font-medium tabular-nums tracking-tight text-stone-900 sm:text-3xl">
{shown}
<span className="text-stone-400">{k.suffix}</span>
</div>
<div className="mt-1 text-xs text-stone-500">{k.label}</div>
</div>
);
}
function Chart({ active }: { active: boolean }) {
const [range, setRange] = useState<Range>('30D');
const [hover, setHover] = useState<number | null>(null);
const n = RANGES[range];
const { cur, prev, line, prevLine, area, pts } = useMemo(() => {
const cur = series(n, n * 0.37, 0.5);
const prev = series(n, n * 0.91 + 2, 0.25).map((v) => v * 0.8);
const toPts = (s: number[]) =>
s.map((v, i) => [PAD + (i / (n - 1)) * (W - PAD * 2), H - PAD - v * (H - PAD * 2)] as [number, number]);
const pts = toPts(cur);
const line = smoothPath(pts);
return { cur, prev, pts, line, prevLine: smoothPath(toPts(prev)), area: `${line} L${W - PAD},${H} L${PAD},${H} Z` };
}, [n]);
const onMove = (e: MouseEvent<SVGSVGElement>) => {
const r = e.currentTarget.getBoundingClientRect();
const x = ((e.clientX - r.left) / r.width) * W;
setHover(Math.max(0, Math.min(n - 1, Math.round(((x - PAD) / (W - PAD * 2)) * (n - 1)))));
};
return (
<div className="rounded-3xl border border-stone-200 bg-white p-4 shadow-[0_24px_48px_-24px_rgba(28,25,23,0.25)] sm:p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="text-sm font-medium text-stone-900">Engagement over time</div>
<div className="mt-1 flex items-center gap-4 text-xs text-stone-500">
<span className="flex items-center gap-1.5"><span className="h-2 w-2 rounded-full bg-[#FF5A36]" /> This period</span>
<span className="flex items-center gap-1.5"><span className="h-0.5 w-3 bg-stone-300" /> Previous</span>
</div>
</div>
<div className="flex rounded-full bg-stone-100 p-1">
{(Object.keys(RANGES) as Range[]).map((r) => (
<button
key={r}
type="button"
onClick={() => setRange(r)}
className={cn('rounded-full px-3 py-1 font-mono text-xs transition-all', range === r ? 'bg-white text-stone-900 shadow-sm' : 'text-stone-500 hover:text-stone-800')}
>
{r}
</button>
))}
</div>
</div>
<div className="relative mt-6">
<svg viewBox={`0 0 ${W} ${H}`} className="h-56 w-full overflow-visible sm:h-64" preserveAspectRatio="none" onMouseMove={onMove} onMouseLeave={() => setHover(null)}>
<defs>
<linearGradient id="cad-area" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="#FF5A36" stopOpacity="0.28" />
<stop offset="100%" stopColor="#FF5A36" stopOpacity="0" />
</linearGradient>
</defs>
{[0.25, 0.5, 0.75].map((g) => (
<line key={g} x1={0} x2={W} y1={H * g} y2={H * g} stroke="#E7E5E4" strokeDasharray="3 5" vectorEffect="non-scaling-stroke" />
))}
<g key={range}>
<path d={prevLine} fill="none" stroke="#D6D3D1" strokeWidth="1.5" strokeDasharray="4 4" vectorEffect="non-scaling-stroke" />
<path d={area} fill="url(#cad-area)" className={cn('transition-opacity delay-700 duration-1000', active ? 'opacity-100' : 'opacity-0')} />
<path
d={line}
fill="none"
stroke="#FF5A36"
strokeWidth="2.5"
strokeLinecap="round"
pathLength={1}
strokeDasharray="1"
className={active ? 'cad-draw' : 'opacity-0'}
/>
</g>
{hover !== null && (
<line x1={pts[hover][0]} x2={pts[hover][0]} y1={0} y2={H} stroke="#1C1917" strokeOpacity="0.25" vectorEffect="non-scaling-stroke" />
)}
</svg>
{hover !== null && (
<>
<span
className="pointer-events-none absolute h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-[#FF5A36] shadow"
style={{ left: `${(pts[hover][0] / W) * 100}%`, top: `${(pts[hover][1] / H) * 100}%` }}
/>
<div
className="pointer-events-none absolute top-0 z-10 -translate-x-1/2 rounded-xl bg-stone-900 px-3 py-2 text-xs text-white shadow-xl"
style={{ left: `${Math.min(88, Math.max(12, (pts[hover][0] / W) * 100))}%` }}
>
<div className="font-mono text-stone-400">Day {hover + 1}</div>
<div className="font-medium">{Math.round(cur[hover] * 12000).toLocaleString()} engagements</div>
<div className="text-emerald-400">+{Math.round(((cur[hover] - prev[hover]) / prev[hover]) * 100)}% vs prev</div>
</div>
</>
)}
</div>
</div>
);
}
const BREAKDOWN: { id: NetworkId; pct: number; value: string }[] = [
{ id: 'instagram', pct: 86, value: '1.02M' },
{ id: 'tiktok', pct: 72, value: '846K' },
{ id: 'linkedin', pct: 48, value: '312K' },
{ id: 'x', pct: 36, value: '204K' },
{ id: 'youtube', pct: 22, value: '98K' },
];
const TOP = [
{ title: 'Latte art in 15 seconds', id: 'tiktok' as NetworkId, img: IMG.cappuccino, eng: '84.2K', rate: '11.4%' },
{ title: 'Our cold brew, explained', id: 'instagram' as NetworkId, img: IMG.latte, eng: '42.7K', rate: '8.9%' },
{ title: 'How we hire for craft', id: 'linkedin' as NetworkId, img: IMG.interior, eng: '18.1K', rate: '7.2%' },
];
export function Analytics() {
const { ref, inView } = useInView<HTMLDivElement>(0.2);
return (
<section id="analytics" className="scroll-mt-20 py-24 sm:py-32">
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<SectionHeading
eyebrow="Analytics"
title="Know what works."
accent="Do more of it."
body="Every network in one beautifully clear dashboard. No spreadsheets, no exports — just the numbers that move your strategy."
/>
<div ref={ref} className="mt-16 grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-4">
{KPIS.map((k, i) => (
<Reveal key={k.label} delay={i * 80}>
<Kpi k={k} active={inView} delay={i * 120} />
</Reveal>
))}
</div>
<div className="mt-4 grid gap-4 lg:grid-cols-[minmax(0,7fr)_minmax(0,4fr)]">
<Reveal>
<Chart active={inView} />
</Reveal>
<Reveal delay={120}>
<div className="h-full rounded-3xl border border-stone-200 bg-white p-4 shadow-[0_24px_48px_-24px_rgba(28,25,23,0.25)] sm:p-6">
<div className="text-sm font-medium text-stone-900">Reach by network</div>
<div className="mt-5 space-y-4">
{BREAKDOWN.map((b, i) => (
<div key={b.id}>
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-2 text-stone-700">
<NetworkBadge id={b.id} size="xs" /> {networkMap[b.id].name}
</span>
<span className="font-mono text-stone-500">{b.value}</span>
</div>
<div className="mt-2 h-2 overflow-hidden rounded-full bg-stone-100">
<div
className="h-full rounded-full transition-[width] duration-1000 ease-spring"
style={{
width: inView ? `${b.pct}%` : '0%',
transitionDelay: `${300 + i * 120}ms`,
background: b.id === 'instagram' ? 'linear-gradient(90deg,#fd5949,#d6249f)' : b.id === 'tiktok' ? 'linear-gradient(90deg,#25F4EE,#FE2C55)' : networkMap[b.id].color,
}}
/>
</div>
</div>
))}
</div>
<div className="mt-8 text-sm font-medium text-stone-900">Top posts</div>
<div className="mt-3 space-y-2">
{TOP.map((p) => (
<div key={p.title} className="flex items-center gap-3 rounded-xl p-1.5 transition-colors hover:bg-stone-50">
<div className="relative h-10 w-10 shrink-0">
<img src={p.img} alt="" loading="lazy" className="h-full w-full rounded-lg object-cover" />
<NetworkBadge id={p.id} size="xs" className="absolute -bottom-1 -right-1 !h-4 !w-4 ring-2 ring-white" />
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-medium text-stone-900">{p.title}</div>
<div className="text-[11px] text-stone-500">{p.eng} engagements</div>
</div>
<span className="font-mono text-xs text-emerald-700">{p.rate}</span>
</div>
))}
</div>
</div>
</Reveal>
</div>
</div>
</section>
);
}components/niceui/pages/landing/cadence/Workflow.tsx
import { useEffect, useState } from 'react';
import { CalendarCheck, CheckCircle2, Eye, FileEdit, Lock, MessageSquare, ShieldCheck, Users } from 'lucide-react';
import { cn } from '@/lib/cn';
import { Reveal, useInView } from '@/components/niceui/pages/landing/shared/motion';
import { NetworkBadge } from './logos';
import { Avatar, IMG, PEOPLE } from './shared';
const STAGES = [
{ label: 'Draft', Icon: FileEdit, chip: 'bg-stone-100 text-stone-700', dot: 'bg-stone-400', who: PEOPLE.amara },
{ label: 'In review', Icon: Eye, chip: 'bg-amber-50 text-amber-800', dot: 'bg-amber-500', who: PEOPLE.maya },
{ label: 'Approved', Icon: CheckCircle2, chip: 'bg-emerald-50 text-emerald-800', dot: 'bg-emerald-500', who: PEOPLE.theo },
{ label: 'Scheduled', Icon: CalendarCheck, chip: 'bg-sky-50 text-sky-800', dot: 'bg-sky-500', who: PEOPLE.amara },
];
const COMMENTS = [
{ stage: 0, who: PEOPLE.amara, text: 'First pass on the launch caption — added the Huila farm angle.', time: '9:02' },
{ stage: 1, who: PEOPLE.maya, text: 'Love it. Can we swap the hero for the latte shot? Feels warmer.', time: '9:14' },
{ stage: 1, who: PEOPLE.amara, text: 'Swapped. Also trimmed the X version to 240 chars.', time: '9:16' },
{ stage: 2, who: PEOPLE.theo, text: 'Approved. This one is going to do numbers.', time: '9:31' },
];
const PERKS = [
{ Icon: Users, title: 'Roles & permissions', body: 'Creators draft, editors review, clients approve. Everyone sees exactly what they should.' },
{ Icon: MessageSquare, title: 'Threaded feedback', body: 'Comment on the exact line or image. Resolve, mention and move on.' },
{ Icon: ShieldCheck, title: 'Brand-safe by default', body: 'Nothing goes live without sign-off, and every change is logged.' },
];
export function Workflow() {
const { ref, inView } = useInView<HTMLDivElement>(0.3);
const [stage, setStage] = useState(0);
const [paused, setPaused] = useState(false);
useEffect(() => {
if (!inView || paused) return;
const id = window.setInterval(() => setStage((s) => (s + 1) % STAGES.length), 2400);
return () => window.clearInterval(id);
}, [inView, paused]);
const cur = STAGES[stage];
return (
<section id="workflow" className="scroll-mt-20 bg-stone-900 py-24 text-white sm:py-32">
<div className="mx-auto grid max-w-6xl items-center gap-16 px-4 sm:px-6 lg:grid-cols-2">
<div>
<Reveal>
<span className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-3 py-1 text-xs font-medium tracking-wide text-stone-300">
<span className="h-1.5 w-1.5 rounded-full bg-[#FF5A36]" /> Team workflow
</span>
</Reveal>
<Reveal delay={80}>
<h2 className="mt-4 text-4xl font-medium leading-[1.1] tracking-[-0.03em] sm:text-5xl">
Approvals that feel <span className="font-serif font-normal italic tracking-normal text-[#FF8A6B]">effortless.</span>
</h2>
</Reveal>
<Reveal delay={160}>
<p className="mt-4 max-w-lg text-lg leading-relaxed text-stone-400">
From first draft to published, every post moves through a clear, shared pipeline. No more screenshots in group chats.
</p>
</Reveal>
<div className="mt-10 space-y-6">
{PERKS.map((p, i) => (
<Reveal key={p.title} delay={200 + i * 80}>
<div className="flex gap-4">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-white/5 ring-1 ring-white/10">
<p.Icon className="h-5 w-5 text-[#FF8A6B]" />
</span>
<div>
<div className="font-medium">{p.title}</div>
<div className="mt-1 text-sm leading-relaxed text-stone-400">{p.body}</div>
</div>
</div>
</Reveal>
))}
</div>
</div>
<Reveal delay={120}>
<div
ref={ref}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
className="rounded-3xl bg-[#FBFAF7] p-4 text-stone-900 shadow-[0_40px_80px_-24px_rgba(0,0,0,0.6)] sm:p-6"
>
<div className="relative grid grid-cols-4 gap-2">
<div className="absolute left-[12.5%] right-[12.5%] top-5 h-0.5 bg-stone-200">
<div className="h-full bg-[#FF5A36] transition-all duration-700 ease-spring" style={{ width: `${(stage / (STAGES.length - 1)) * 100}%` }} />
</div>
{STAGES.map((s, i) => (
<button key={s.label} type="button" onClick={() => setStage(i)} className="group relative flex flex-col items-center gap-2">
<span
className={cn(
'relative flex h-10 w-10 items-center justify-center rounded-full border-2 transition-all duration-500 ease-spring',
i < stage && 'border-[#FF5A36] bg-[#FF5A36] text-white',
i === stage && 'scale-110 border-[#FF5A36] bg-white text-[#FF5A36] shadow-[0_0_0_6px_rgba(255,90,54,0.12)]',
i > stage && 'border-stone-200 bg-white text-stone-400 group-hover:border-stone-400',
)}
>
<s.Icon className="h-4 w-4" />
</span>
<span className={cn('text-center text-[11px] font-medium sm:text-xs', i <= stage ? 'text-stone-900' : 'text-stone-400')}>{s.label}</span>
</button>
))}
</div>
<div className="mt-6 rounded-2xl border border-stone-200 bg-white p-4">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Avatar src={cur.who.img} name={cur.who.name} className="h-7 w-7" />
<div className="leading-tight">
<div className="text-xs font-medium">Cold brew launch</div>
<div className="text-[11px] text-stone-500">Owner · {cur.who.name.split(' ')[0]}</div>
</div>
</div>
<span key={stage} className={cn('inline-flex animate-scale-in items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium', cur.chip)}>
<span className={cn('h-1.5 w-1.5 rounded-full', cur.dot)} />
{cur.label}
</span>
</div>
<div className="mt-4 flex gap-3">
<img src={stage >= 1 ? IMG.latte : IMG.citrus} alt="" className="h-20 w-20 shrink-0 rounded-xl object-cover transition-all duration-500" />
<div className="min-w-0">
<p className="text-sm leading-relaxed text-stone-700">
Our 18-hour cold brew lands Saturday. Smoother, bolder, small batch — and the first 50 cups are on us.
</p>
<div className="mt-2 flex gap-1">
<NetworkBadge id="instagram" size="xs" />
<NetworkBadge id="x" size="xs" />
<NetworkBadge id="linkedin" size="xs" />
</div>
</div>
</div>
</div>
<div className="mt-4 min-h-[232px] space-y-3">
{COMMENTS.filter((c) => c.stage <= stage).map((c) => (
<div key={c.text} className="flex animate-fade-up gap-2.5">
<Avatar src={c.who.img} name={c.who.name} className="h-7 w-7" />
<div className="min-w-0 flex-1 rounded-2xl rounded-tl-md bg-white px-3 py-2 ring-1 ring-stone-200">
<div className="flex items-center justify-between text-[11px]">
<span className="font-medium text-stone-900">{c.who.name}</span>
<span className="font-mono text-stone-400">{c.time}</span>
</div>
<div className="mt-0.5 text-xs leading-relaxed text-stone-600">{c.text}</div>
</div>
</div>
))}
{stage === 3 && (
<div className="flex animate-fade-up items-center gap-2 rounded-2xl bg-sky-50 px-3 py-2.5 text-xs text-sky-900 ring-1 ring-sky-100">
<Lock className="h-3.5 w-3.5" />
Locked & scheduled for <span className="font-medium">Sat, 9:00 AM</span> on 3 networks
</div>
)}
</div>
</div>
</Reveal>
</div>
</section>
);
}components/niceui/pages/landing/cadence/Social.tsx
import { useState } from 'react';
import { Check, Star } from 'lucide-react';
import { cn } from '@/lib/cn';
import { Reveal } from '@/components/niceui/pages/landing/shared/motion';
import { NetworkBadge, type NetworkId } from './logos';
import { Avatar, PEOPLE, SectionHeading } from './shared';
type Quote = { who: (typeof PEOPLE)[keyof typeof PEOPLE]; text: string; net: NetworkId };
const QUOTES: Quote[] = [
{ who: PEOPLE.maya, net: 'instagram', text: 'We went from scrambling every morning to having a full month planned. Our reach is up 41% and my team finally takes lunch breaks.' },
{ who: PEOPLE.daniel, net: 'x', text: 'The per-network previews are unreal. I catch truncated captions before they go live, not after.' },
{ who: PEOPLE.amara, net: 'tiktok', text: 'Best-time scheduling alone paid for Cadence in week one. It just knows when our people scroll.' },
{ who: PEOPLE.theo, net: 'linkedin', text: 'Approvals used to live in Slack threads and prayers. Now clients sign off in two clicks.' },
{ who: PEOPLE.jonah, net: 'youtube', text: 'Clean, fast, calm. It is the only tool in our stack people actually open voluntarily.' },
{ who: PEOPLE.lena, net: 'facebook', text: 'We manage 34 brands on Cadence. The workspace switching and reports are worth every penny.' },
{ who: PEOPLE.kofi, net: 'instagram', text: 'AI rewrite gets the tone right for each network. I edit less and post more.' },
{ who: PEOPLE.zara, net: 'linkedin', text: 'Analytics our CMO can read in thirty seconds. That alone changed how we get budget.' },
];
function QuoteCard({ q }: { q: Quote }) {
return (
<figure className="w-[320px] shrink-0 rounded-3xl border border-stone-200 bg-white p-6 shadow-sm transition-all duration-300 ease-spring hover:-translate-y-1 hover:shadow-[0_20px_40px_-20px_rgba(28,25,23,0.3)] sm:w-[380px]">
<div className="flex items-center justify-between">
<span className="flex text-amber-500">
{[0, 1, 2, 3, 4].map((i) => (
<Star key={i} className="h-3.5 w-3.5 fill-current" />
))}
</span>
<NetworkBadge id={q.net} size="xs" variant="soft" />
</div>
<blockquote className="mt-4 text-[15px] leading-relaxed text-stone-800">“{q.text}”</blockquote>
<figcaption className="mt-6 flex items-center gap-3">
<Avatar src={q.who.img} name={q.who.name} className="h-10 w-10 ring-0" />
<div className="leading-tight">
<div className="text-sm font-medium text-stone-900">{q.who.name}</div>
<div className="text-xs text-stone-500">{q.who.role}</div>
</div>
</figcaption>
</figure>
);
}
export function Testimonials() {
const a = QUOTES.slice(0, 4);
const b = QUOTES.slice(4);
return (
<section className="overflow-hidden py-24 sm:py-32">
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<SectionHeading
eyebrow="Loved by teams"
title="The social team's"
accent="favorite tool."
body="18,000+ creators, brands and agencies schedule over 2 million posts a month with Cadence."
/>
</div>
<div className="group mt-16 space-y-4 [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]">
{[a, b].map((row, ri) => (
<div key={ri} className="flex overflow-hidden">
<div
className={cn('flex w-max animate-marquee gap-4 pr-4 group-hover:[animation-play-state:paused]', ri === 1 && '[animation-direction:reverse]')}
style={{ animationDuration: ri === 0 ? '60s' : '70s' }}
>
{[...row, ...row, ...row, ...row].map((q, i) => (
<QuoteCard key={`${q.who.name}-${i}`} q={q} />
))}
</div>
</div>
))}
</div>
</section>
);
}
const PLANS = [
{
name: 'Creator',
monthly: 15,
yearly: 12,
blurb: 'For solo creators finding their rhythm.',
features: ['3 social profiles', 'Unlimited scheduled posts', 'AI captions (100/mo)', 'Best-time suggestions', 'Basic analytics'],
},
{
name: 'Team',
monthly: 49,
yearly: 39,
blurb: 'For brands growing across every network.',
features: ['15 social profiles', 'Everything in Creator', 'Unlimited AI rewrites', 'Approval workflows', 'Advanced analytics & reports', '5 team seats'],
featured: true,
},
{
name: 'Agency',
monthly: 129,
yearly: 103,
blurb: 'For agencies running many brands at once.',
features: ['50 social profiles', 'Everything in Team', 'Client workspaces', 'White-label reports', 'Unlimited seats', 'Priority support & SSO'],
},
];
export function Pricing() {
const [yearly, setYearly] = useState(true);
return (
<section id="pricing" className="scroll-mt-20 bg-[#F4F1EA] py-24 sm:py-32">
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<SectionHeading
eyebrow="Pricing"
title="Simple plans."
accent="Serious results."
body="Start free for 14 days. No credit card, cancel anytime, and switch plans whenever your rhythm changes."
/>
<Reveal delay={200}>
<div className="mt-10 flex justify-center">
<div className="relative flex rounded-full bg-white p-1 shadow-sm ring-1 ring-stone-200">
<span
className={cn(
'absolute bottom-1 top-1 w-[calc(50%-4px)] rounded-full bg-stone-900 transition-transform duration-500 ease-spring',
yearly ? 'translate-x-full' : 'translate-x-0',
)}
/>
{[false, true].map((y) => (
<button
key={String(y)}
type="button"
onClick={() => setYearly(y)}
className={cn('relative z-10 w-32 rounded-full py-2 text-sm font-medium transition-colors duration-300', yearly === y ? 'text-white' : 'text-stone-600')}
>
{y ? 'Yearly' : 'Monthly'}
{y && <span className={cn('ml-1.5 text-[11px]', yearly ? 'text-[#FFB39F]' : 'text-[#FF5A36]')}>−20%</span>}
</button>
))}
</div>
</div>
</Reveal>
<div className="mt-12 grid items-stretch gap-4 lg:grid-cols-3">
{PLANS.map((p, i) => {
const price = yearly ? p.yearly : p.monthly;
return (
<Reveal key={p.name} delay={i * 100} className="h-full">
<div
className={cn(
'relative flex h-full flex-col rounded-3xl p-6 transition-all duration-500 ease-spring hover:-translate-y-1.5 sm:p-8',
p.featured
? 'bg-stone-900 text-white shadow-[0_40px_80px_-32px_rgba(255,90,54,0.55)]'
: 'border border-stone-200 bg-white text-stone-900 hover:shadow-[0_24px_48px_-24px_rgba(28,25,23,0.3)]',
)}
>
{p.featured && (
<span className="absolute -top-3 left-8 rounded-full bg-[#FF5A36] px-3 py-1 text-[11px] font-medium text-white shadow-[0_8px_16px_-6px_rgba(255,90,54,0.8)]">
Most popular
</span>
)}
<div className="text-lg font-medium">{p.name}</div>
<p className={cn('mt-1 text-sm', p.featured ? 'text-stone-400' : 'text-stone-500')}>{p.blurb}</p>
<div className="mt-6 flex items-end gap-1">
<span className="text-2xl font-medium">$</span>
<span className="relative inline-flex h-14 overflow-hidden">
<span key={price} className="cad-roll text-5xl font-medium tabular-nums tracking-tight">{price}</span>
</span>
<span className={cn('mb-1.5 text-sm', p.featured ? 'text-stone-400' : 'text-stone-500')}>/ month</span>
</div>
<div className={cn('mt-1 h-5 text-xs', p.featured ? 'text-stone-400' : 'text-stone-500')}>
{yearly ? `Billed $${price * 12} yearly` : 'Billed monthly'}
</div>
<button
type="button"
className={cn(
'mt-6 rounded-full py-3 text-sm font-medium transition-all duration-300 ease-spring hover:-translate-y-0.5 active:scale-[0.98]',
p.featured ? 'bg-[#FF5A36] text-white hover:bg-[#ff6e4f]' : 'bg-stone-900 text-white hover:bg-[#FF5A36]',
)}
>
Start 14-day trial
</button>
<ul className="mt-8 space-y-3 text-sm">
{p.features.map((f) => (
<li key={f} className="flex items-start gap-2.5">
<span className={cn('mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full', p.featured ? 'bg-[#FF5A36]' : 'bg-emerald-100')}>
<Check className={cn('h-2.5 w-2.5', p.featured ? 'text-white' : 'text-emerald-700')} strokeWidth={3} />
</span>
<span className={p.featured ? 'text-stone-300' : 'text-stone-700'}>{f}</span>
</li>
))}
</ul>
</div>
</Reveal>
);
})}
</div>
</div>
</section>
);
}components/niceui/pages/landing/cadence/Closing.tsx
import { useState } from 'react';
import { ArrowRight, Plus } from 'lucide-react';
import { cn } from '@/lib/cn';
import { Reveal } from '@/components/niceui/pages/landing/shared/motion';
import { NetworkBadge, networks } from './logos';
import { CadenceLogo } from './Hero';
import { PrimaryButton, SectionHeading, scrollToId } from './shared';
const FAQS = [
{ q: 'Which networks does Cadence support?', a: 'Instagram (feed, Reels, Stories), X, LinkedIn profiles and pages, TikTok, Facebook pages and groups, and YouTube including Shorts. We publish natively through each official API.' },
{ q: 'Do I need a credit card to start?', a: 'No. Every plan starts with a 14-day free trial with all features unlocked. Add a card only when you decide to stay.' },
{ q: 'How does best-time scheduling work?', a: 'Cadence analyzes up to 90 days of engagement for each connected profile and builds an hourly activity model. New posts are auto-slotted into the strongest open windows — you can always override.' },
{ q: 'Can clients approve posts without an account?', a: 'Yes. Share a secure approval link and clients can comment, request changes or approve without signing up. Every action is logged.' },
{ q: 'Is the AI rewrite trained on my content?', a: 'Never without permission. Rewrites use your brand voice settings and the post itself. Your content is not used to train shared models.' },
{ q: 'Can I switch or cancel my plan anytime?', a: 'Absolutely. Upgrade, downgrade or cancel from settings in a couple of clicks. Yearly plans are prorated when you switch.' },
];
export function FAQ() {
const [open, setOpen] = useState<number | null>(0);
return (
<section id="faq" className="scroll-mt-20 py-24 sm:py-32">
<div className="mx-auto grid max-w-6xl gap-12 px-4 sm:px-6 lg:grid-cols-[minmax(0,4fr)_minmax(0,7fr)]">
<SectionHeading
align="left"
eyebrow="FAQ"
title="Questions,"
accent="answered."
body="Can't find what you're looking for? Our team replies in under two hours, every day of the week."
/>
<div className="divide-y divide-stone-200 border-y border-stone-200">
{FAQS.map((f, i) => {
const isOpen = open === i;
return (
<Reveal key={f.q} delay={i * 60}>
<button
type="button"
onClick={() => setOpen(isOpen ? null : i)}
aria-expanded={isOpen}
className="group flex w-full items-center justify-between gap-6 py-6 text-left"
>
<span className="text-base font-medium text-stone-900 transition-colors group-hover:text-[#FF5A36] sm:text-lg">{f.q}</span>
<span
className={cn(
'flex h-8 w-8 shrink-0 items-center justify-center rounded-full border transition-all duration-500 ease-spring',
isOpen ? 'rotate-45 border-[#FF5A36] bg-[#FF5A36] text-white' : 'border-stone-300 text-stone-600 group-hover:border-stone-900',
)}
>
<Plus className="h-4 w-4" />
</span>
</button>
<div className={cn('grid transition-all duration-500 ease-smooth', isOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0')}>
<div className="overflow-hidden">
<p className="max-w-2xl pb-6 text-base leading-relaxed text-stone-600">{f.a}</p>
</div>
</div>
</Reveal>
);
})}
</div>
</div>
</section>
);
}
export function FinalCTA() {
return (
<section className="px-4 pb-24 sm:px-6">
<Reveal>
<div className="relative mx-auto max-w-6xl overflow-hidden rounded-[40px] bg-[#FF5A36] px-6 py-16 text-center text-white sm:px-12 sm:py-24">
<div className="pointer-events-none absolute -left-24 -top-24 h-80 w-80 animate-blob bg-amber-300/40 blur-3xl" />
<div className="pointer-events-none absolute -bottom-32 -right-16 h-96 w-96 animate-blob bg-rose-400/40 blur-3xl [animation-delay:-5s]" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(rgba(255,255,255,0.18)_1px,transparent_1px)] bg-[size:20px_20px] [mask-image:radial-gradient(ellipse_at_center,black,transparent_70%)]" />
<div className="relative">
<div className="flex justify-center -space-x-2">
{networks.map((n, i) => (
<span key={n.id} className="animate-float rounded-2xl bg-white p-1 shadow-lg" style={{ animationDelay: `${i * 200}ms` }}>
<NetworkBadge id={n.id} size="md" />
</span>
))}
</div>
<h2 className="mx-auto mt-8 max-w-3xl text-4xl font-medium leading-[1.1] tracking-[-0.035em] sm:text-6xl">
Find your rhythm. <span className="font-serif font-normal italic tracking-normal">Keep it.</span>
</h2>
<p className="mx-auto mt-4 max-w-lg text-lg leading-relaxed text-white/85">
Join 18,000+ creators and teams who plan less, post better and grow faster with Cadence.
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<PrimaryButton onClick={() => scrollToId('pricing')} className="bg-white text-stone-900 hover:bg-stone-900 hover:text-white">
Start free for 14 days <ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
</PrimaryButton>
<button type="button" className="rounded-full px-6 py-3 text-sm font-medium text-white ring-1 ring-white/40 transition-all hover:bg-white/10 hover:ring-white">
Book a demo
</button>
</div>
<p className="mt-4 text-xs text-white/75">No credit card required · Cancel anytime</p>
</div>
</div>
</Reveal>
</section>
);
}
const FOOTER = [
{ title: 'Product', links: [['Composer', 'composer'], ['Scheduling', 'scheduling'], ['Analytics', 'analytics'], ['Workflow', 'workflow'], ['Pricing', 'pricing']] },
{ title: 'Company', links: [['About', ''], ['Careers', ''], ['Press', ''], ['Contact', '']] },
{ title: 'Resources', links: [['Blog', ''], ['Guides', ''], ['Help center', ''], ['FAQ', 'faq']] },
];
export function Footer() {
const [email, setEmail] = useState('');
const [joined, setJoined] = useState(false);
return (
<footer className="border-t border-stone-200 bg-white/60">
<div className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
<div className="grid gap-12 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)]">
<div>
<CadenceLogo />
<p className="mt-4 max-w-sm text-sm leading-relaxed text-stone-600">
The calm command center for social media. Plan, publish and grow — everywhere your audience is.
</p>
<form
onSubmit={(e) => {
e.preventDefault();
if (email.includes('@')) setJoined(true);
}}
className="mt-6 flex max-w-sm gap-2"
>
<label htmlFor="cad-email" className="sr-only">Email</label>
<input
id="cad-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@company.com"
className="min-w-0 flex-1 rounded-full border border-stone-300 bg-white px-4 py-2.5 text-sm text-stone-900 placeholder:text-stone-400 focus:border-stone-900 focus:outline-none"
/>
<button type="submit" className="rounded-full bg-stone-900 px-4 text-sm font-medium text-white transition-colors hover:bg-[#FF5A36]">
{joined ? 'Joined' : 'Subscribe'}
</button>
</form>
<div className="mt-6 flex gap-2">
{networks.map((n) => (
<span key={n.id} className="transition-transform duration-300 ease-spring hover:-translate-y-1">
<NetworkBadge id={n.id} size="sm" variant="soft" />
</span>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-8 sm:grid-cols-3">
{FOOTER.map((col) => (
<div key={col.title}>
<div className="text-sm font-medium text-stone-900">{col.title}</div>
<ul className="mt-4 space-y-3">
{col.links.map(([label, id]) => (
<li key={label}>
<button
type="button"
onClick={() => id && scrollToId(id)}
className="text-sm text-stone-600 transition-colors hover:text-[#FF5A36]"
>
{label}
</button>
</li>
))}
</ul>
</div>
))}
</div>
</div>
<div className="mt-16 flex flex-col items-start justify-between gap-4 border-t border-stone-200 pt-8 text-xs text-stone-500 sm:flex-row sm:items-center">
<span>© {new Date().getFullYear()} Cadence Labs, Inc. All rights reserved.</span>
<div className="flex gap-6">
<button type="button" className="hover:text-stone-900">Privacy</button>
<button type="button" className="hover:text-stone-900">Terms</button>
<button type="button" className="hover:text-stone-900">Security</button>
</div>
</div>
</div>
<div className="pointer-events-none select-none overflow-hidden">
<div className="mx-auto -mb-[0.22em] max-w-6xl px-4 text-center font-serif text-[22vw] italic leading-none text-stone-900/[0.04] lg:text-[240px]">
cadence
</div>
</div>
</footer>
);
}