|
| 1 | +"use client" |
| 2 | + |
| 3 | +import { StaggerContainer, StaggerItem } from "@/components/animations/Stagger" |
| 4 | +import { Section } from "@/components/ui/Section" |
| 5 | +import type { StatsLayoutBlock } from "@/lib/cms" |
| 6 | +import { cn } from "@/lib/utils" |
| 7 | +import NumberFlow from "@number-flow/react" |
| 8 | +import { useInView } from "motion/react" |
| 9 | +import { useRef } from "react" |
| 10 | + |
| 11 | +interface StatsSectionProps { |
| 12 | + content?: StatsLayoutBlock | null |
| 13 | +} |
| 14 | + |
| 15 | +function getCompactNumber(number: number) { |
| 16 | + const absoluteNumber = Math.abs(number) |
| 17 | + |
| 18 | + if (absoluteNumber >= 1_000_000_000) return { value: Math.round(number / 1_000_000_000), suffix: "B" } |
| 19 | + if (absoluteNumber >= 1_000_000) return { value: Math.round(number / 1_000_000), suffix: "M" } |
| 20 | + if (absoluteNumber >= 1_000) return { value: Math.round(number / 1_000), suffix: "K" } |
| 21 | + |
| 22 | + return { value: Math.round(number), suffix: "" } |
| 23 | +} |
| 24 | + |
| 25 | +function StatNumber({ number, animate, showPlus }: { number: number; animate: boolean; showPlus: boolean }) { |
| 26 | + const ref = useRef<HTMLDivElement>(null) |
| 27 | + const isInView = useInView(ref, { once: true, amount: 0.5 }) |
| 28 | + const compactNumber = getCompactNumber(number) |
| 29 | + const suffix = `${compactNumber.suffix}${showPlus ? "+" : ""}` |
| 30 | + |
| 31 | + return ( |
| 32 | + <div ref={ref} className="text-5xl font-semibold tabular-nums text-white md:text-6xl"> |
| 33 | + <NumberFlow |
| 34 | + value={animate && !isInView ? 0 : compactNumber.value} |
| 35 | + suffix={!animate || isInView ? suffix : ""} |
| 36 | + animated={animate} |
| 37 | + transformTiming={{ duration: 1200, easing: "ease-out" }} |
| 38 | + /> |
| 39 | + </div> |
| 40 | + ) |
| 41 | +} |
| 42 | + |
| 43 | +export function StatsSection({ content }: StatsSectionProps) { |
| 44 | + if (!content?.items?.length) return null |
| 45 | + |
| 46 | + const desktopColumns = { |
| 47 | + 1: "md:grid-cols-1", |
| 48 | + 2: "md:grid-cols-2", |
| 49 | + 3: "md:grid-cols-3", |
| 50 | + }[content.items.length] |
| 51 | + |
| 52 | + return ( |
| 53 | + <Section |
| 54 | + heading={content.sectionHeading} |
| 55 | + description={content.sectionDescription} |
| 56 | + linkButton={content.sectionLinkButton} |
| 57 | + funnelType={content.sectionLayout ?? "center"} |
| 58 | + animation={{ preset: "none" }} |
| 59 | + > |
| 60 | + <StaggerContainer className={cn("grid w-full grid-cols-1", desktopColumns)} delayChildren={0.06} staggerChildren={0.12}> |
| 61 | + {content.items.map((item, index) => ( |
| 62 | + <StaggerItem |
| 63 | + className={cn( |
| 64 | + "flex flex-col gap-3 py-8 text-center first:pt-0 last:pb-0 md:px-8 md:py-0 md:first:pl-0 md:last:pr-0", |
| 65 | + index > 0 && "border-t border-white/10 md:border-l md:border-t-0" |
| 66 | + )} |
| 67 | + y={18} |
| 68 | + duration={0.4} |
| 69 | + key={item.id ?? index} |
| 70 | + > |
| 71 | + <StatNumber number={item.number} animate={item.enableNumberFlow !== false} showPlus={item.showPlus === true} /> |
| 72 | + <p className="text-lg font-medium text-secondary">{item.description}</p> |
| 73 | + </StaggerItem> |
| 74 | + ))} |
| 75 | + </StaggerContainer> |
| 76 | + </Section> |
| 77 | + ) |
| 78 | +} |
0 commit comments