x
This commit is contained in:
1 parent
9922e8a5bc
commit
0144545166
4 files changed
+197
-16
No files matched your search
@@ -129,6 +129,16 @@ function buildThemeStyle(settings: StoreSettings | null): CSSProperties | undefi
|
|||||||
['--shadow-card-hover', tokens.shadowCardHover],
|
['--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) {
|
for (const [key, value] of vars) {
|
||||||
if (typeof value === 'string' && value.trim()) {
|
if (typeof value === 'string' && value.trim()) {
|
||||||
style[key] = value
|
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) {
|
export default function DynamicPageRenderer({ sections, categories, lang: langProp }: DynamicPageRendererProps) {
|
||||||
const i18nLang = useLang()
|
const i18nLang = useLang()
|
||||||
// Use i18n.language reactively — switches immediately when user changes language
|
// 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':
|
case 'ANNOUNCEMENT_BAR':
|
||||||
return <AnnouncementBar key={section.id} payload={payload} lang={lang} />
|
return <AnnouncementBar key={section.id} payload={payload} lang={lang} />
|
||||||
case 'HERO':
|
case 'HERO':
|
||||||
return <HeroSection key={section.id} payload={payload} lang={lang} />
|
return (
|
||||||
|
<ScrollReveal key={section.id}>
|
||||||
|
<HeroSection payload={payload} lang={lang} />
|
||||||
|
</ScrollReveal>
|
||||||
|
)
|
||||||
case 'CATEGORIES':
|
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':
|
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':
|
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':
|
case 'BANNER':
|
||||||
return <BannerSection key={section.id} payload={payload} lang={lang} />
|
return (
|
||||||
|
<ScrollReveal key={section.id}>
|
||||||
|
<BannerSection payload={payload} lang={lang} />
|
||||||
|
</ScrollReveal>
|
||||||
|
)
|
||||||
case 'TEXT':
|
case 'TEXT':
|
||||||
return <TextSection key={section.id} payload={payload} lang={lang} />
|
return (
|
||||||
|
<ScrollReveal key={section.id}>
|
||||||
|
<TextSection payload={payload} lang={lang} />
|
||||||
|
</ScrollReveal>
|
||||||
|
)
|
||||||
case 'NEWSLETTER':
|
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':
|
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':
|
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':
|
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':
|
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':
|
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':
|
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':
|
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':
|
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':
|
case 'CONTACT_FORM':
|
||||||
return <ContactFormSection key={section.id} payload={payload} lang={lang} />
|
return (
|
||||||
|
<ScrollReveal key={section.id}>
|
||||||
|
<ContactFormSection payload={payload} lang={lang} />
|
||||||
|
</ScrollReveal>
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
console.warn(`[DynamicPageRenderer] Unsupported section type: ${section.type}`)
|
console.warn(`[DynamicPageRenderer] Unsupported section type: ${section.type}`)
|
||||||
@@ -922,3 +988,4 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
Reference in new issue
Block a user