Skip to content

Commit 1ceac69

Browse files
committed
feat: stats section
1 parent fa8a059 commit 1ceac69

11 files changed

Lines changed: 27563 additions & 1 deletion

src/blocks/StatsBlock.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { Block } from "payload"
2+
3+
export const StatsBlock: Block = {
4+
slug: "stats",
5+
labels: {
6+
singular: "Stats",
7+
plural: "Stats Blocks",
8+
},
9+
fields: [
10+
{
11+
type: "collapsible",
12+
label: "Section",
13+
fields: [
14+
{
15+
name: "sectionHeading",
16+
label: "Section Heading",
17+
type: "text",
18+
required: false,
19+
localized: true,
20+
},
21+
{
22+
name: "sectionLayout",
23+
label: "Section Layout",
24+
type: "select",
25+
required: true,
26+
defaultValue: "center",
27+
options: [
28+
{ label: "Center", value: "center" },
29+
{ label: "Left", value: "left" },
30+
],
31+
},
32+
{
33+
name: "sectionDescription",
34+
label: "Section Description",
35+
type: "textarea",
36+
required: false,
37+
localized: true,
38+
},
39+
{
40+
name: "sectionLinkButton",
41+
label: "Section Link Button",
42+
type: "group",
43+
fields: [
44+
{
45+
name: "label",
46+
type: "text",
47+
required: false,
48+
localized: true,
49+
},
50+
{
51+
name: "url",
52+
type: "text",
53+
required: false,
54+
},
55+
],
56+
},
57+
],
58+
},
59+
{
60+
name: "items",
61+
type: "array",
62+
required: true,
63+
minRows: 1,
64+
maxRows: 3,
65+
labels: {
66+
singular: "Stat",
67+
plural: "Stats",
68+
},
69+
fields: [
70+
{
71+
name: "number",
72+
type: "number",
73+
required: true,
74+
},
75+
{
76+
name: "description",
77+
type: "text",
78+
required: true,
79+
localized: true,
80+
},
81+
{
82+
name: "enableNumberFlow",
83+
label: "Animate number",
84+
type: "checkbox",
85+
defaultValue: true,
86+
},
87+
{
88+
name: "showPlus",
89+
label: "Show plus",
90+
type: "checkbox",
91+
defaultValue: false,
92+
},
93+
],
94+
},
95+
],
96+
}

src/collections/pages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type { CollectionConfig } from "payload"
2222
import { StandaloneBlock } from "@/blocks/StandaloneBlock"
2323
import { VideoBlock } from "@/blocks/VideoBlock"
2424
import { WideHeroBlock } from "@/blocks/WideHeroBlock"
25+
import { StatsBlock } from "@/blocks/StatsBlock"
2526

2627
export const Pages: CollectionConfig = {
2728
slug: "pages",
@@ -93,6 +94,7 @@ export const Pages: CollectionConfig = {
9394
VideoBlock,
9495
WideHeroBlock,
9596
ListFeatureSection,
97+
StatsBlock,
9698
],
9799
required: false,
98100
localized: true,

src/components/PageBlockRenderer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import React, { type ReactNode } from "react"
1919
import { StandaloneCardSection } from "./sections/StandaloneCardSection"
2020
import { VideoSection } from "./sections/VideoSection"
2121
import { WideHeroSection } from "./sections/WideHeroSection"
22+
import { StatsSection } from "./sections/StatsSection"
2223

2324
type PageBlock = NonNullable<Page["layout"]>[number]
2425

@@ -51,6 +52,7 @@ const pageBlockRenderers: Partial<Record<PageBlock["blockType"], BlockRenderer>>
5152
video: (block) => <VideoSection content={block as Extract<PageBlock, { blockType: "video" }>} />,
5253
widehero: (block) => <WideHeroSection content={block as Extract<PageBlock, { blockType: "widehero" }>} />,
5354
listFeature: (block) => <ListFeatureSection content={block as Extract<PageBlock, { blockType: "listFeature" }>} />,
55+
stats: (block) => <StatsSection content={block as Extract<PageBlock, { blockType: "stats" }>} />,
5456
}
5557

5658
function renderPageBlock(block: PageBlock, options: PageBlockRenderOptions) {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
}

src/lib/cms.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type StandaloneCardLayoutBlock = Extract<PageLayoutBlock, { blockType: "s
3232
export type VideoLayoutBlock = Extract<PageLayoutBlock, { blockType: "video" }>
3333
export type WideHeroLayoutBlock = Extract<PageLayoutBlock, { blockType: "widehero" }>
3434
export type ListFeatureLayoutBlock = Extract<PageLayoutBlock, { blockType: "listFeature" }>
35+
export type StatsLayoutBlock = Extract<PageLayoutBlock, { blockType: "stats" }>
3536

3637
type FeatureSlug = Feature["slug"]
3738
interface FeatureItem {

0 commit comments

Comments
 (0)