Skip to content

Commit 464fb05

Browse files
committed
refactor: reduce section client boundary
1 parent ca0ba0d commit 464fb05

4 files changed

Lines changed: 138 additions & 122 deletions

File tree

src/app/(landing)/globals.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,25 @@
150150
scroll-behavior: auto !important;
151151
}
152152
}
153+
@media (prefers-reduced-motion: no-preference) {
154+
.section-funnel > .section-stagger-item {
155+
opacity: 0;
156+
transform: translateY(1rem);
157+
transition:
158+
opacity 0.4s cubic-bezier(0.22, 1, 0.36, 1),
159+
transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
160+
}
161+
162+
[data-in-view="true"] > .section-funnel > .section-stagger-item {
163+
opacity: 1;
164+
transform: translateY(0);
165+
}
166+
167+
[data-in-view="true"] > .section-funnel > .section-stagger-item:nth-child(2) {
168+
transition-delay: 0.1s;
169+
}
170+
171+
[data-in-view="true"] > .section-funnel > .section-stagger-item:nth-child(3) {
172+
transition-delay: 0.2s;
173+
}
174+
}

src/components/ui/Section.tsx

Lines changed: 32 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
"use client"
1+
import { SectionLinkButton } from "@/components/ui/SectionLinkButton"
2+
import { SectionMotion, type SectionAnimation } from "@/components/ui/SectionMotion"
3+
import { cn } from "@/lib/utils"
4+
import { createElement, type ReactNode } from "react"
25

3-
import { LinkButton } from "@/components/ui/LinkButton"
4-
import { getLocaleFromPath, localizeHref } from "@/lib/i18n"
5-
import { ANIMATION_PRESETS, cn, type AnimationPreset } from "@/lib/utils"
6-
import { m as motion, type Variants } from "motion/react"
7-
import { usePathname } from "next/navigation"
8-
import { createElement, ReactNode, useEffect, useRef } from "react"
9-
10-
interface SectionLinkButton {
6+
interface SectionLink {
117
label?: string | null
128
url?: string | null
139
}
@@ -49,41 +45,12 @@ interface SectionProps {
4945
className?: string
5046
heading?: string | null
5147
description?: string | null
52-
linkButton?: SectionLinkButton | null
48+
linkButton?: SectionLink | null
5349
showFunnel?: boolean
5450
showLinkButton?: boolean
5551
fullHeight?: boolean
5652
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6
57-
animation?: {
58-
preset?: AnimationPreset
59-
delay?: number
60-
duration?: number
61-
once?: boolean
62-
viewportAmount?: number
63-
viewportMargin?: string
64-
}
65-
}
66-
67-
const DEFAULT_SECTION_ANIMATION: NonNullable<SectionProps["animation"]> = {}
68-
const staggerContainer: Variants = {
69-
hidden: {},
70-
show: {
71-
transition: {
72-
staggerChildren: 0.1,
73-
delayChildren: 0.04,
74-
},
75-
},
76-
}
77-
const staggerItem: Variants = {
78-
hidden: { opacity: 0, y: 16 },
79-
show: {
80-
opacity: 1,
81-
y: 0,
82-
transition: {
83-
duration: 0.4,
84-
ease: [0.22, 1, 0.36, 1],
85-
},
86-
},
53+
animation?: SectionAnimation
8754
}
8855

8956
export function Section({
@@ -96,94 +63,37 @@ export function Section({
9663
showFunnel = true,
9764
showLinkButton = true,
9865
fullHeight = false,
99-
animation = DEFAULT_SECTION_ANIMATION,
66+
animation,
10067
headingLevel = 2,
10168
}: SectionProps) {
102-
const { preset = "fade-up", delay = 0, duration, once = true, viewportAmount = 0.2, viewportMargin } = animation
103-
const sectionRef = useRef<HTMLElement | null>(null)
104-
const pathname = usePathname()
105-
const locale = getLocaleFromPath(pathname)
10669
const rawLinkUrl = linkButton?.url?.trim()
107-
const linkUrl = rawLinkUrl ? localizeHref(rawLinkUrl, locale) : undefined
108-
const shouldShowFunnel = showFunnel && Boolean(heading || description || (showLinkButton && linkUrl && linkButton?.label))
109-
const animationConfig = preset === "none" ? null : ANIMATION_PRESETS[preset]
70+
const shouldShowFunnel = showFunnel && Boolean(heading || description || (showLinkButton && rawLinkUrl && linkButton?.label))
11071
const headingTag = `h${headingLevel}` as const
111-
const headingClassName = cn("text-4xl font-semibold", hasHighlightedHeading(heading) ? "text-secondary" : "text-white")
112-
113-
useEffect(() => {
114-
const currentRef = sectionRef.current
115-
if (!currentRef) return
116-
117-
const observer = new IntersectionObserver(
118-
([entry]) => {
119-
if (entry?.isIntersecting) {
120-
currentRef.dataset.inView = "true"
121-
observer.unobserve(currentRef)
122-
}
123-
},
124-
{ rootMargin: "100px" }
125-
)
126-
127-
observer.observe(currentRef)
128-
129-
return () => observer.disconnect()
130-
}, [])
72+
const headingClassName = cn("section-stagger-item text-4xl font-semibold", hasHighlightedHeading(heading) ? "text-secondary" : "text-white")
73+
const funnelClassName = funnelType === "center" ? "section-funnel flex flex-col gap-4 items-center justify-center text-center" : "section-funnel flex flex-col gap-4 text-left"
74+
const descriptionClassName = cn(
75+
"section-stagger-item relative z-10 max-w-[90vw] text-xl font-medium text-secondary lg:w-1/2",
76+
funnelType === "center" && "text-center"
77+
)
13178

13279
return (
133-
<motion.section
134-
ref={sectionRef}
135-
data-in-view="false"
136-
className={cn("group/section relative overflow-hidden flex flex-col gap-16", fullHeight && "h-[200dvh] md:h-[min(100dvh,1080px)]", className)}
137-
initial={animationConfig?.initial}
138-
whileInView={animationConfig?.whileInView}
139-
viewport={animationConfig ? { once, amount: viewportAmount, margin: viewportMargin } : undefined}
140-
transition={
141-
animationConfig
142-
? {
143-
...animationConfig.transition,
144-
delay: delay,
145-
duration: duration ?? animationConfig.transition.duration,
146-
}
147-
: undefined
148-
}
149-
>
150-
{shouldShowFunnel &&
151-
(funnelType === "center" ? (
152-
<motion.div
153-
className={"flex flex-col gap-4 items-center justify-center text-center"}
154-
variants={staggerContainer}
155-
initial="hidden"
156-
whileInView="show"
157-
viewport={{ once, amount: 0.3 }}
158-
>
159-
{createElement(motion[headingTag], { variants: staggerItem, className: headingClassName }, heading ? <FormattedText text={heading} /> : null)}
160-
{description && (
161-
<motion.p variants={staggerItem} className="relative z-10 max-w-[90vw] text-center text-xl font-medium text-secondary lg:w-1/2">
162-
<FormattedText text={description} />
163-
</motion.p>
164-
)}
165-
{showLinkButton && linkUrl && (
166-
<motion.div variants={staggerItem}>
167-
<LinkButton href={linkUrl}>{linkButton?.label}</LinkButton>
168-
</motion.div>
169-
)}
170-
</motion.div>
171-
) : (
172-
<motion.div className={"flex flex-col gap-4 text-left"} variants={staggerContainer} initial="hidden" whileInView="show" viewport={{ once, amount: 0.3 }}>
173-
{createElement(motion[headingTag], { variants: staggerItem, className: headingClassName }, heading ? <FormattedText text={heading} /> : null)}
174-
{description && (
175-
<motion.p variants={staggerItem} className="relative z-10 max-w-[90vw] text-xl font-medium text-secondary lg:w-1/2">
176-
<FormattedText text={description} />
177-
</motion.p>
178-
)}
179-
{showLinkButton && linkUrl && (
180-
<motion.div variants={staggerItem}>
181-
<LinkButton href={linkUrl}>{linkButton?.label}</LinkButton>
182-
</motion.div>
183-
)}
184-
</motion.div>
185-
))}
80+
<SectionMotion animation={animation} className={className} fullHeight={fullHeight}>
81+
{shouldShowFunnel && (
82+
<div className={funnelClassName}>
83+
{createElement(headingTag, { className: headingClassName }, heading ? <FormattedText text={heading} /> : null)}
84+
{description && (
85+
<p className={descriptionClassName}>
86+
<FormattedText text={description} />
87+
</p>
88+
)}
89+
{showLinkButton && rawLinkUrl && (
90+
<div className="section-stagger-item">
91+
<SectionLinkButton label={linkButton?.label} url={rawLinkUrl} />
92+
</div>
93+
)}
94+
</div>
95+
)}
18696
{children}
187-
</motion.section>
97+
</SectionMotion>
18898
)
18999
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"use client"
2+
3+
import { LinkButton } from "@/components/ui/LinkButton"
4+
import { getLocaleFromPath, localizeHref } from "@/lib/i18n"
5+
import { usePathname } from "next/navigation"
6+
7+
interface SectionLinkButtonProps {
8+
label?: string | null
9+
url: string
10+
}
11+
12+
export function SectionLinkButton({ label, url }: SectionLinkButtonProps) {
13+
const locale = getLocaleFromPath(usePathname())
14+
return <LinkButton href={localizeHref(url, locale)}>{label}</LinkButton>
15+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"use client"
2+
3+
import { ANIMATION_PRESETS, cn, type AnimationPreset } from "@/lib/utils"
4+
import { m as motion } from "motion/react"
5+
import { type ReactNode, useEffect, useRef } from "react"
6+
7+
export interface SectionAnimation {
8+
preset?: AnimationPreset
9+
delay?: number
10+
duration?: number
11+
once?: boolean
12+
viewportAmount?: number
13+
viewportMargin?: string
14+
}
15+
16+
interface SectionMotionProps {
17+
animation?: SectionAnimation
18+
children: ReactNode
19+
className?: string
20+
fullHeight?: boolean
21+
}
22+
23+
export function SectionMotion({ animation = {}, children, className, fullHeight = false }: SectionMotionProps) {
24+
const { preset = "fade-up", delay = 0, duration, once = true, viewportAmount = 0.2, viewportMargin } = animation
25+
const sectionRef = useRef<HTMLElement | null>(null)
26+
const animationConfig = preset === "none" ? null : ANIMATION_PRESETS[preset]
27+
28+
useEffect(() => {
29+
const section = sectionRef.current
30+
if (!section) return
31+
32+
const observer = new IntersectionObserver(
33+
([entry]) => {
34+
if (!entry) return
35+
36+
section.dataset.inView = String(entry.isIntersecting)
37+
if (entry.isIntersecting && once) {
38+
observer.unobserve(section)
39+
}
40+
},
41+
{ rootMargin: "100px" }
42+
)
43+
44+
observer.observe(section)
45+
return () => observer.disconnect()
46+
}, [once])
47+
48+
return (
49+
<motion.section
50+
ref={sectionRef}
51+
data-in-view="false"
52+
className={cn("group/section relative flex flex-col gap-16 overflow-hidden", fullHeight && "h-[200dvh] md:h-[min(100dvh,1080px)]", className)}
53+
initial={animationConfig?.initial}
54+
whileInView={animationConfig?.whileInView}
55+
viewport={animationConfig ? { once, amount: viewportAmount, margin: viewportMargin } : undefined}
56+
transition={
57+
animationConfig
58+
? {
59+
...animationConfig.transition,
60+
delay,
61+
duration: duration ?? animationConfig.transition.duration,
62+
}
63+
: undefined
64+
}
65+
>
66+
{children}
67+
</motion.section>
68+
)
69+
}

0 commit comments

Comments
 (0)