diff --git a/apps/website/src/app/[lang]/layout.tsx b/apps/website/src/app/[lang]/layout.tsx index 9a6edbc..a497157 100644 --- a/apps/website/src/app/[lang]/layout.tsx +++ b/apps/website/src/app/[lang]/layout.tsx @@ -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 diff --git a/apps/website/src/components/ScrollReveal.tsx b/apps/website/src/components/ScrollReveal.tsx new file mode 100644 index 0000000..65fbd49 --- /dev/null +++ b/apps/website/src/components/ScrollReveal.tsx @@ -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 = { + '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({ + 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
{children}
+ } + + const initialTransform = STYLE_TRANSFORMS[clientSettings.style] ?? STYLE_TRANSFORMS['fade-up']; + + return ( +
+ {children} +
+ ); +} diff --git a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx index 9620e2e..3532f72 100644 --- a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx +++ b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx @@ -865,6 +865,8 @@ function ContactFormSection({ payload, lang }: { payload: Record case 'HERO': - return + return ( + + + + ) case 'CATEGORIES': - return + return ( + + + + ) case 'PRODUCTS': - return + return ( + + + + ) case 'PRODUCT_CAROUSEL': - return + return ( + + + + ) case 'BANNER': - return + return ( + + + + ) case 'TEXT': - return + return ( + + + + ) case 'NEWSLETTER': - return + return ( + + + + ) case 'EDITORIAL_TILE': - return + return ( + + + + ) case 'EDITORIAL_CAROUSEL': - return + return ( + + + + ) case 'HERO_ARTICLE': - return + return ( + + + + ) case 'RICH_TEXT': - return + return ( + + + + ) case 'SCENT_NOTES_BREAKDOWN': - return + return ( + + + + ) case 'QUOTE_CALLOUT': - return + return ( + + + + ) case 'ACCORDION': - return + return ( + + + + ) case 'CONTACT_INFO': - return + return ( + + + + ) case 'CONTACT_FORM': - return + return ( + + + + ) 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 ) } + diff --git a/apps/website/src/hooks/useScrollReveal.ts b/apps/website/src/hooks/useScrollReveal.ts new file mode 100644 index 0000000..cbb92a4 --- /dev/null +++ b/apps/website/src/hooks/useScrollReveal.ts @@ -0,0 +1,39 @@ +"use client" + +import { useEffect, useRef, useState } from 'react'; + +interface UseScrollRevealOptions { + threshold?: number; + rootMargin?: string; + once?: boolean; +} + +export function useScrollReveal( + options: UseScrollRevealOptions = {} +) { + const { threshold = 0.1, rootMargin = '0px 0px -60px 0px', once = true } = options; + const ref = useRef(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 }; +}