cohorta/src/components/landing/StatsBand.tsx
Vadym Samoilenko a9a5fff659 feat: full visual rebrand + landing redesign + auth page refresh + email fix
- Landing: extract 513-line monolith into 12 focused section components
  (Hero, StatsBand, FeatureGrid, HowItWorks, LivePreview, Comparison,
  UseCases, Testimonials, Pricing, FAQ, FinalCTA, TrustBar)
- Auth pages: replace flat orange panel with animated live mock
  (real persona SVGs, typewriter messages, theme bars); Login label
  fixed to "Email or username"; Register wires ?plan= badge
- Brand: new Logo SVG (C-arc + 3 figures + wordmark/tagline), expanded
  palette tokens, fluid display type scale, framer-motion shared variants
- Header: scroll progress bar, removed non-functional language pill
- Footer: fixed all dead links, legal stubs, new logo
- Legal: /about /privacy /terms /cookies /gdpr real pages added
- Email: FROM_EMAIL default fixed to noreply@ai-impress.com (verified
  apex domain), HTML template rewritten to match new brand
- Tooling: Playwright screenshot script for visual self-check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:02:03 +01:00

86 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { motion, useReducedMotion, useSpring, useTransform, useInView } from 'framer-motion';
import { useEffect, useRef } from 'react';
import { Clock, DollarSign, Globe } from 'lucide-react';
import { fadeUp, staggerChildren, viewportOnce } from '@/lib/motion';
const STATS = [
{
icon: Clock,
prefix: '',
value: 10,
suffix: '×',
label: 'FASTER',
sub: 'Insights in hours, not weeks',
},
{
icon: DollarSign,
prefix: '',
value: 99,
suffix: '%',
label: 'CHEAPER',
sub: 'No recruiter, incentives, or no-shows',
},
{
icon: Globe,
prefix: '',
value: 24,
suffix: '/7',
label: 'SCALE',
sub: 'Run 50 sessions in parallel',
},
];
function AnimatedNumber({ value, shouldAnimate }: { value: number; shouldAnimate: boolean }) {
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true, margin: '-40px' });
const spring = useSpring(0, { stiffness: 60, damping: 20 });
const display = useTransform(spring, (v) => Math.round(v).toString());
useEffect(() => {
if (isInView && shouldAnimate) {
spring.set(value);
} else if (!shouldAnimate) {
spring.set(value);
}
}, [isInView, shouldAnimate, spring, value]);
return (
<motion.span ref={ref}>
{shouldAnimate ? <motion.span>{display}</motion.span> : <span>{value}</span>}
</motion.span>
);
}
export default function StatsBand() {
const shouldReduce = useReducedMotion();
const shouldAnimate = !shouldReduce;
return (
<section className="py-20 px-6 bg-[hsl(var(--brand-charcoal))]">
<div className="max-w-5xl mx-auto">
<motion.div
variants={staggerChildren}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="grid grid-cols-1 md:grid-cols-3 gap-5"
>
{STATS.map(({ icon: Icon, prefix, value, suffix, label, sub }) => (
<motion.div key={label} variants={fadeUp} className="corner-card p-8 group">
<div className="h-10 w-10 rounded-xl bg-primary/15 flex items-center justify-center mb-4 group-hover:bg-primary/25 transition-colors">
<Icon className="h-5 w-5 text-primary" />
</div>
<div className="font-display font-black text-5xl text-foreground mb-1">
{prefix}
<AnimatedNumber value={value} shouldAnimate={shouldAnimate} />
{suffix}
</div>
<div className="text-xs font-bold tracking-widest text-primary uppercase mb-2">{label}</div>
<p className="text-sm text-muted-foreground">{sub}</p>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}