This commit is contained in:
Your Name committed 2026-06-29 19:51:04 -04:00
1 parent 9922e8a5bc
commit 0144545166
4 files changed
+197 -16

No files matched your search

+10
View File
@@ -129,6 +129,16 @@ function buildThemeStyle(settings: StoreSettings | null): CSSProperties | undefi
['--shadow-card-hover', tokens.shadowCardHover],
]
// Add Scroll Reveal Settings
const scrollEnabled = settings?.scrollRevealEnabled !== false ? '1' : '0'
const scrollStyle = settings?.scrollRevealStyle || 'fade-up'
const scrollDuration = settings?.scrollRevealDuration ? `${settings.scrollRevealDuration}ms` : '700ms'
style['--scroll-reveal-enabled'] = scrollEnabled
style['--scroll-reveal-style'] = scrollStyle
style['--scroll-reveal-duration'] = scrollDuration
for (const [key, value] of vars) {
if (typeof value === 'string' && value.trim()) {
style[key] = value
@@ -0,0 +1,65 @@
"use client"
import { useEffect, useState } from 'react';
import { useScrollReveal } from '../hooks/useScrollReveal';
// Animation style -> initial transform mapping
const STYLE_TRANSFORMS: Record<string, string> = {
'fade-up': 'translateY(28px)',
'fade-in': 'translateY(0)',
'slide-left': 'translateX(28px)',
'zoom-in': 'scale(0.95)',
'none': 'translateY(0)',
}
interface ScrollRevealProps {
children: React.ReactNode;
delay?: number; // ms
className?: string;
}
export default function ScrollReveal({ children, delay = 0, className = '' }: ScrollRevealProps) {
const { ref, isVisible } = useScrollReveal<HTMLDivElement>({
threshold: 0.08,
rootMargin: '0px 0px -40px 0px',
});
const [clientSettings, setClientSettings] = useState({
enabled: true,
style: 'fade-up',
durationMs: 700
});
useEffect(() => {
// Read from CSS variables injected by layout.tsx on the body tag
const body = document.body;
const enabled = body.style.getPropertyValue('--scroll-reveal-enabled').trim() !== '0';
const style = body.style.getPropertyValue('--scroll-reveal-style').trim() || 'fade-up';
const durationVar = body.style.getPropertyValue('--scroll-reveal-duration').trim() || '700ms';
const durationMs = parseInt(durationVar) || 700;
setClientSettings({ enabled, style, durationMs });
}, []);
// If animation is disabled or set to none, just render the children normally
if (!clientSettings.enabled || clientSettings.style === 'none') {
return <div className={className}>{children}</div>
}
const initialTransform = STYLE_TRANSFORMS[clientSettings.style] ?? STYLE_TRANSFORMS['fade-up'];
return (
<div
ref={ref}
className={className}
style={{
opacity: isVisible ? 1 : 0,
transform: isVisible ? 'translateY(0) translateX(0) scale(1)' : initialTransform,
transition: `opacity ${clientSettings.durationMs}ms ease-out ${delay}ms, transform ${clientSettings.durationMs}ms ease-out ${delay}ms`,
willChange: 'opacity, transform',
}}
>
{children}
</div>
);
}
@@ -865,6 +865,8 @@ function ContactFormSection({ payload, lang }: { payload: Record<string, unknown
)
}
import ScrollReveal from '../ScrollReveal'
export default function DynamicPageRenderer({ sections, categories, lang: langProp }: DynamicPageRendererProps) {
const i18nLang = useLang()
// Use i18n.language reactively — switches immediately when user changes language
@@ -881,37 +883,101 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
case 'ANNOUNCEMENT_BAR':
return <AnnouncementBar key={section.id} payload={payload} lang={lang} />
case 'HERO':
return <HeroSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<HeroSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'CATEGORIES':
return <CategoriesSection key={section.id} payload={payload} categories={categories} lang={lang} />
return (
<ScrollReveal key={section.id}>
<CategoriesSection payload={payload} categories={categories} lang={lang} />
</ScrollReveal>
)
case 'PRODUCTS':
return <ProductsSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<ProductsSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'PRODUCT_CAROUSEL':
return <ProductsSection key={section.id} payload={{ ...payload, layout: 'carousel' }} lang={lang} />
return (
<ScrollReveal key={section.id}>
<ProductsSection payload={{ ...payload, layout: 'carousel' }} lang={lang} />
</ScrollReveal>
)
case 'BANNER':
return <BannerSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<BannerSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'TEXT':
return <TextSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<TextSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'NEWSLETTER':
return <NewsletterSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<NewsletterSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'EDITORIAL_TILE':
return <EditorialTileSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<EditorialTileSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'EDITORIAL_CAROUSEL':
return <EditorialCarouselSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<EditorialCarouselSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'HERO_ARTICLE':
return <HeroArticleSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<HeroArticleSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'RICH_TEXT':
return <TextSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<TextSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'SCENT_NOTES_BREAKDOWN':
return <ScentNotesBreakdownSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<ScentNotesBreakdownSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'QUOTE_CALLOUT':
return <QuoteCalloutSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<QuoteCalloutSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'ACCORDION':
return <AccordionSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<AccordionSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'CONTACT_INFO':
return <ContactInfoSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<ContactInfoSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'CONTACT_FORM':
return <ContactFormSection key={section.id} payload={payload} lang={lang} />
return (
<ScrollReveal key={section.id}>
<ContactFormSection payload={payload} lang={lang} />
</ScrollReveal>
)
default:
if (process.env.NODE_ENV !== 'production') {
console.warn(`[DynamicPageRenderer] Unsupported section type: ${section.type}`)
@@ -922,3 +988,4 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
</div>
)
}
+39
View File
@@ -0,0 +1,39 @@
"use client"
import { useEffect, useRef, useState } from 'react';
interface UseScrollRevealOptions {
threshold?: number;
rootMargin?: string;
once?: boolean;
}
export function useScrollReveal<T extends HTMLElement = HTMLDivElement>(
options: UseScrollRevealOptions = {}
) {
const { threshold = 0.1, rootMargin = '0px 0px -60px 0px', once = true } = options;
const ref = useRef<T>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
if (once) observer.disconnect();
} else if (!once) {
setIsVisible(false);
}
},
{ threshold, rootMargin }
);
observer.observe(el);
return () => observer.disconnect();
}, [threshold, rootMargin, once]);
return { ref, isVisible };
}