x
This commit is contained in:
1 parent
ce2f4cf0c8
commit
5a87e7f77d
21 files changed
+1083
-88
No files matched your search
@@ -39,14 +39,12 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
|
||||
<div className="absolute inset-0 z-0">
|
||||
<img
|
||||
src={imageUrl || `${import.meta.env.VITE_API_URL.replace('/api', '')}/assets/hero.png`}
|
||||
className="w-full h-full object-cover scale-[1.02]"
|
||||
className="w-full h-full object-cover"
|
||||
alt="Hero Background"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
<div className="ui-hero-overlay" />
|
||||
<div className="ui-hero-overlay-strong" />
|
||||
</div>
|
||||
|
||||
<div className="ui-hero-content animate-fade-in">
|
||||
|
||||
@@ -16,6 +16,7 @@ import DynamicRichText from './DynamicRichText';
|
||||
import AccordionSection from './sections/AccordionSection';
|
||||
import ContactInfoSection from './sections/ContactInfoSection';
|
||||
import ContactFormSection from './sections/ContactFormSection';
|
||||
import ScrollReveal from './ScrollReveal';
|
||||
|
||||
interface DynamicPageRendererProps {
|
||||
sections: PageSection[];
|
||||
@@ -34,53 +35,58 @@ export default function DynamicPageRenderer({
|
||||
if (!section.isActive) return null;
|
||||
|
||||
switch (section.type.toUpperCase()) {
|
||||
// ไม่ animate — แสดงตั้งแต่ต้น
|
||||
case 'ANNOUNCEMENT_BAR':
|
||||
return <DynamicAnnouncementBar key={section.id} payload={section.payload} />;
|
||||
case 'NEWSLETTER':
|
||||
return <DynamicNewsletter key={section.id} payload={section.payload} />;
|
||||
case 'HERO':
|
||||
return <DynamicHero key={section.id} payload={section.payload} />;
|
||||
|
||||
// animate เมื่อ scroll เข้า viewport
|
||||
case 'NEWSLETTER':
|
||||
return <ScrollReveal key={section.id}><DynamicNewsletter payload={section.payload} /></ScrollReveal>;
|
||||
case 'CATEGORIES':
|
||||
return (
|
||||
<DynamicCategories
|
||||
key={section.id}
|
||||
payload={section.payload}
|
||||
categories={categories}
|
||||
loading={categoriesLoading}
|
||||
/>
|
||||
<ScrollReveal key={section.id}>
|
||||
<DynamicCategories
|
||||
payload={section.payload}
|
||||
categories={categories}
|
||||
loading={categoriesLoading}
|
||||
/>
|
||||
</ScrollReveal>
|
||||
);
|
||||
case 'PRODUCTS':
|
||||
return (
|
||||
<DynamicProducts
|
||||
key={section.id}
|
||||
payload={section.payload}
|
||||
categories={categories}
|
||||
/>
|
||||
<ScrollReveal key={section.id}>
|
||||
<DynamicProducts
|
||||
payload={section.payload}
|
||||
categories={categories}
|
||||
/>
|
||||
</ScrollReveal>
|
||||
);
|
||||
case 'BANNER':
|
||||
return <DynamicBanner key={section.id} payload={section.payload} />;
|
||||
return <ScrollReveal key={section.id}><DynamicBanner payload={section.payload} /></ScrollReveal>;
|
||||
case 'TEXT':
|
||||
return <DynamicText key={section.id} payload={section.payload} />;
|
||||
return <ScrollReveal key={section.id}><DynamicText payload={section.payload} /></ScrollReveal>;
|
||||
case 'RICH_TEXT':
|
||||
return <DynamicRichText key={section.id} section={section} />;
|
||||
return <ScrollReveal key={section.id}><DynamicRichText section={section} /></ScrollReveal>;
|
||||
case 'EDITORIAL_TILE':
|
||||
return <DynamicEditorialTile key={section.id} payload={section.payload} />;
|
||||
return <ScrollReveal key={section.id}><DynamicEditorialTile payload={section.payload} /></ScrollReveal>;
|
||||
case 'EDITORIAL_CAROUSEL':
|
||||
return <DynamicEditorialCarousel key={section.id} payload={section.payload} />;
|
||||
return <ScrollReveal key={section.id}><DynamicEditorialCarousel payload={section.payload} /></ScrollReveal>;
|
||||
case 'HERO_ARTICLE':
|
||||
return <DynamicHeroArticle key={section.id} payload={section.payload} />;
|
||||
return <ScrollReveal key={section.id}><DynamicHeroArticle payload={section.payload} /></ScrollReveal>;
|
||||
case 'SCENT_NOTES_BREAKDOWN':
|
||||
return <DynamicScentNotesBreakdown key={section.id} payload={section.payload} />;
|
||||
return <ScrollReveal key={section.id}><DynamicScentNotesBreakdown payload={section.payload} /></ScrollReveal>;
|
||||
case 'QUOTE_CALLOUT':
|
||||
return <DynamicQuoteCallout key={section.id} payload={section.payload} />;
|
||||
return <ScrollReveal key={section.id}><DynamicQuoteCallout payload={section.payload} /></ScrollReveal>;
|
||||
case 'PRODUCT_CAROUSEL':
|
||||
return <DynamicProductCarousel key={section.id} payload={section.payload} />;
|
||||
return <ScrollReveal key={section.id}><DynamicProductCarousel payload={section.payload} /></ScrollReveal>;
|
||||
case 'ACCORDION':
|
||||
return <AccordionSection key={section.id} payload={section.payload as AccordionSectionPayload & { title?: string }} />;
|
||||
return <ScrollReveal key={section.id}><AccordionSection payload={section.payload as AccordionSectionPayload & { title?: string }} /></ScrollReveal>;
|
||||
case 'CONTACT_INFO':
|
||||
return <ContactInfoSection key={section.id} payload={section.payload as ContactInfoSectionPayload & { title?: string }} />;
|
||||
return <ScrollReveal key={section.id}><ContactInfoSection payload={section.payload as ContactInfoSectionPayload & { title?: string }} /></ScrollReveal>;
|
||||
case 'CONTACT_FORM':
|
||||
return <ContactFormSection key={section.id} payload={section.payload as ContactFormSectionPayload} />;
|
||||
return <ScrollReveal key={section.id}><ContactFormSection payload={section.payload as ContactFormSectionPayload} /></ScrollReveal>;
|
||||
default:
|
||||
console.warn(`[DynamicPageRenderer] Unknown block type: ${section.type}`);
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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',
|
||||
});
|
||||
|
||||
// อ่านค่าจาก CSS variables ที่ ThemeInjector inject ไว้
|
||||
const root = typeof window !== 'undefined' ? document.documentElement : null
|
||||
const enabled = root?.style.getPropertyValue('--scroll-reveal-enabled').trim() !== '0'
|
||||
const style = root?.style.getPropertyValue('--scroll-reveal-style').trim() || 'fade-up'
|
||||
const durationVar = root?.style.getPropertyValue('--scroll-reveal-duration').trim() || '700ms'
|
||||
const durationMs = parseInt(durationVar) || 700
|
||||
|
||||
// ถ้าปิด animation ให้แสดงปกติเลย
|
||||
if (!enabled || style === 'none') {
|
||||
return <div className={className}>{children}</div>
|
||||
}
|
||||
|
||||
const initialTransform = STYLE_TRANSFORMS[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 ${durationMs}ms ease-out ${delay}ms, transform ${durationMs}ms ease-out ${delay}ms`,
|
||||
willChange: 'opacity, transform',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,29 +8,36 @@ export default function ThemeInjector() {
|
||||
fetch(`${API_BASE}/settings?lang=${lang}`)
|
||||
.then(res => res.ok ? res.json() : null)
|
||||
.then(settings => {
|
||||
const tokens = settings?.themeTokens
|
||||
if (!tokens || typeof tokens !== 'object') return
|
||||
|
||||
if (!settings) return
|
||||
const root = document.documentElement
|
||||
|
||||
// Map tokens from the backend (which use brandPrimary etc.)
|
||||
// to the CSS variables expected by SPA's tailwind.config.js
|
||||
const mapping: Record<string, string> = {
|
||||
'--color-pop-pink': tokens.brandPrimary,
|
||||
'--color-pop-yellow': tokens.brandAccent,
|
||||
'--color-pop-orange': tokens.brandWarm,
|
||||
'--color-pop-cyan': tokens.brandCool,
|
||||
'--color-pop-bg': tokens.surfacePage,
|
||||
'--color-pop-black': tokens.textMain,
|
||||
|
||||
'--color-night-900': tokens.surfaceCard,
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(mapping)) {
|
||||
if (value && typeof value === 'string' && value.trim() !== '') {
|
||||
root.style.setProperty(key, value)
|
||||
// --- Theme tokens ---
|
||||
const tokens = settings?.themeTokens
|
||||
if (tokens && typeof tokens === 'object') {
|
||||
const mapping: Record<string, string> = {
|
||||
'--color-pop-pink': tokens.brandPrimary,
|
||||
'--color-pop-yellow': tokens.brandAccent,
|
||||
'--color-pop-orange': tokens.brandWarm,
|
||||
'--color-pop-cyan': tokens.brandCool,
|
||||
'--color-pop-bg': tokens.surfacePage,
|
||||
'--color-pop-black': tokens.textMain,
|
||||
'--color-night-900': tokens.surfaceCard,
|
||||
}
|
||||
for (const [key, value] of Object.entries(mapping)) {
|
||||
if (value && typeof value === 'string' && value.trim() !== '') {
|
||||
root.style.setProperty(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Scroll reveal settings ---
|
||||
const enabled = settings.scrollRevealEnabled ?? true
|
||||
const style = settings.scrollRevealStyle ?? 'fade-up'
|
||||
const duration = settings.scrollRevealDuration ?? 700
|
||||
|
||||
root.style.setProperty('--scroll-reveal-enabled', enabled ? '1' : '0')
|
||||
root.style.setProperty('--scroll-reveal-style', style)
|
||||
root.style.setProperty('--scroll-reveal-duration', `${duration}ms`)
|
||||
})
|
||||
.catch(err => console.error('Failed to inject theme in SPA:', err))
|
||||
}, [])
|
||||
|
||||
Reference in new issue
Block a user