x
This commit is contained in:
1 parent
406cad801b
commit
a2da091b15
21 files changed
+2454
-63
No files matched your search
@@ -22,6 +22,7 @@ interface NavbarProps {
|
||||
categories?: Category[]
|
||||
initialMenu?: MegaMenuPayload[]
|
||||
settings?: StoreSettings | null
|
||||
isTransparentPage?: boolean
|
||||
}
|
||||
|
||||
const LANGUAGES = [
|
||||
@@ -43,7 +44,7 @@ function setLanguageCookie(code: string) {
|
||||
|
||||
|
||||
|
||||
export default function Navbar({ categories = [], initialMenu = [], settings = null }: NavbarProps) {
|
||||
export default function Navbar({ categories = [], initialMenu = [], settings = null, isTransparentPage }: NavbarProps) {
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const [langSheetOpen, setLangSheetOpen] = useState(false)
|
||||
@@ -194,8 +195,23 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
|
||||
return () => window.removeEventListener('product-sticky-change', handleStickyChange)
|
||||
}, [])
|
||||
|
||||
const [isHeroFirst, setIsHeroFirst] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkHeroFirst = () => {
|
||||
const isHero = typeof document !== 'undefined' && document.documentElement.dataset.heroFirst === 'true'
|
||||
setIsHeroFirst(isHero)
|
||||
}
|
||||
checkHeroFirst()
|
||||
const observer = new MutationObserver(checkHeroFirst)
|
||||
if (typeof document !== 'undefined') {
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-hero-first'] })
|
||||
}
|
||||
return () => observer.disconnect()
|
||||
}, [pathname])
|
||||
|
||||
const isHome = getPathWithoutLocale(pathname) === '/'
|
||||
const isTransparent = isHome && !scrolled && !mobileOpen && !searchOpen && !langSheetOpen
|
||||
const isTransparent = (isHome || isHeroFirst || Boolean(isTransparentPage)) && !scrolled && !mobileOpen && !searchOpen && !langSheetOpen
|
||||
const pathWithoutLocale = getPathWithoutLocale(pathname)
|
||||
const isCommercePath = ['/shop', '/products', '/category', '/cart', '/checkout', '/wishlist', '/profile', '/order', '/track']
|
||||
.some((route) => pathWithoutLocale === route || pathWithoutLocale.startsWith(`${route}/`))
|
||||
|
||||
@@ -155,6 +155,95 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
|
||||
)
|
||||
}
|
||||
|
||||
function HeroVideoSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
|
||||
const i18nLang = useLang()
|
||||
const resolvedLang = lang === 'all' ? i18nLang : lang
|
||||
const tagline = localizedFromRecord(payload, 'tagline', resolvedLang)
|
||||
const title = localizedFromRecord(payload, 'title', resolvedLang)
|
||||
const description = localizedFromRecord(payload, 'description', resolvedLang)
|
||||
const buttonText = localizedFromRecord(payload, 'buttonText', resolvedLang)
|
||||
const buttonLink = typeof payload.buttonLink === 'string' ? payload.buttonLink : ''
|
||||
const secondaryButtonText = localizedFromRecord(payload, 'secondaryButtonText', resolvedLang)
|
||||
const secondaryButtonLink = typeof payload.secondaryButtonLink === 'string' ? payload.secondaryButtonLink : ''
|
||||
const videoUrl = typeof payload.videoUrl === 'string' ? payload.videoUrl : ''
|
||||
const posterImageUrl = typeof payload.posterImageUrl === 'string' ? payload.posterImageUrl : undefined
|
||||
const overlayOpacity = typeof payload.overlayOpacity === 'number' ? payload.overlayOpacity : 40
|
||||
const height = typeof payload.height === 'string' ? payload.height : 'large'
|
||||
|
||||
const getHeightClass = (h: string) => {
|
||||
switch (h) {
|
||||
case 'full':
|
||||
return 'h-screen'
|
||||
case 'medium':
|
||||
return 'h-[60vh] min-h-[400px]'
|
||||
case 'large':
|
||||
default:
|
||||
return 'h-[85vh] min-h-[550px]'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={`relative w-full ${getHeightClass(height)} overflow-hidden flex items-center justify-center bg-[#0B0F17] text-white`}>
|
||||
{videoUrl ? (
|
||||
<video
|
||||
autoPlay={payload.autoplay !== false}
|
||||
muted={payload.muted !== false}
|
||||
loop={payload.loop !== false}
|
||||
playsInline
|
||||
poster={posterImageUrl}
|
||||
className="absolute inset-0 w-full h-full object-cover z-0"
|
||||
>
|
||||
<source src={videoUrl} type="video/mp4" />
|
||||
<source src={videoUrl} type="video/webm" />
|
||||
{posterImageUrl && <Image src={posterImageUrl} alt="Hero fallback" fill unoptimized className="object-cover" />}
|
||||
</video>
|
||||
) : posterImageUrl ? (
|
||||
<Image src={posterImageUrl} alt="Hero Poster" fill unoptimized className="object-cover z-0" />
|
||||
) : null}
|
||||
|
||||
<div className="absolute inset-0 z-10 bg-black transition-opacity" style={{ opacity: overlayOpacity / 100 }}></div>
|
||||
|
||||
<div className="relative z-20 mx-auto px-6 py-16 w-full max-w-5xl text-center space-y-6 flex flex-col items-center justify-center">
|
||||
{tagline && (
|
||||
<p className="text-xs md:text-sm font-semibold uppercase tracking-[0.35em] text-[#EAEFEA] drop-shadow-md">
|
||||
{tagline}
|
||||
</p>
|
||||
)}
|
||||
{title && (
|
||||
<h1 className="font-serif text-4xl md:text-6xl lg:text-7xl font-light leading-tight tracking-wide drop-shadow-lg text-white">
|
||||
{renderAccentTitle(title)}
|
||||
</h1>
|
||||
)}
|
||||
{description && (
|
||||
<p className="max-w-2xl text-sm md:text-lg font-light leading-relaxed opacity-90 drop-shadow">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{(buttonText || secondaryButtonText) && (
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 pt-4">
|
||||
{buttonText && buttonLink && (
|
||||
<Link
|
||||
href={buttonLink}
|
||||
className="px-8 py-3.5 bg-white text-neutral-900 font-medium text-xs uppercase tracking-widest hover:bg-brand-primary hover:text-white transition-all shadow-lg rounded-sm"
|
||||
>
|
||||
{buttonText}
|
||||
</Link>
|
||||
)}
|
||||
{secondaryButtonText && secondaryButtonLink && (
|
||||
<Link
|
||||
href={secondaryButtonLink}
|
||||
className="px-8 py-3.5 bg-transparent border border-white/40 text-white font-medium text-xs uppercase tracking-widest hover:bg-white/10 hover:border-white transition-all rounded-sm backdrop-blur-sm"
|
||||
>
|
||||
{secondaryButtonText}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const ICON_MAP: Record<string, React.ElementType> = {
|
||||
leaf: Leaf,
|
||||
droplet: Droplet,
|
||||
@@ -944,6 +1033,110 @@ function EditorialTileSection({ payload, lang }: { payload: Record<string, unkno
|
||||
)
|
||||
}
|
||||
|
||||
function FeaturedTilesSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
|
||||
const i18nLang = useLang()
|
||||
const resolvedLang = lang === 'all' ? i18nLang : lang
|
||||
const title = localizedFromRecord(payload, 'title', resolvedLang)
|
||||
const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang)
|
||||
const columns = typeof payload.columns === 'number' ? payload.columns : 2
|
||||
const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined
|
||||
const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined
|
||||
const tiles = Array.isArray(payload.tiles) ? payload.tiles.map(toRecord) : []
|
||||
|
||||
const getColsClass = (cols: number) => {
|
||||
switch (cols) {
|
||||
case 3:
|
||||
return 'grid-cols-1 md:grid-cols-3'
|
||||
case 4:
|
||||
return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
|
||||
case 2:
|
||||
default:
|
||||
return 'grid-cols-1 md:grid-cols-2'
|
||||
}
|
||||
}
|
||||
|
||||
const getAspectClass = (ratio?: string) => {
|
||||
switch (ratio) {
|
||||
case 'portrait':
|
||||
return 'aspect-[3/4]'
|
||||
case 'tall':
|
||||
return 'aspect-[2/3]'
|
||||
case 'auto':
|
||||
return 'aspect-auto'
|
||||
case 'square':
|
||||
default:
|
||||
return 'aspect-square'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden bg-surface-page ui-section-band" style={{ backgroundColor, color: textColor }}>
|
||||
<div className="mx-auto max-w-7xl space-y-12">
|
||||
{(title || subtitle) && (
|
||||
<div className="mx-auto max-w-2xl text-center space-y-3">
|
||||
{title && (
|
||||
<h2 className="text-3xl font-semibold leading-tight text-text-main md:text-5xl" style={{ color: textColor }}>
|
||||
{renderAccentTitle(title)}
|
||||
</h2>
|
||||
)}
|
||||
{subtitle && <p className="text-base leading-relaxed text-muted-text md:text-lg" style={{ color: textColor }}>{subtitle}</p>}
|
||||
<div className="mx-auto h-px w-14 bg-brand-primary" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`grid gap-8 md:gap-12 ${getColsClass(columns)}`}>
|
||||
{tiles.map((tile, idx) => {
|
||||
const tileTitle = localizedFromRecord(tile, 'title', resolvedLang)
|
||||
const tileDesc = localizedFromRecord(tile, 'description', resolvedLang)
|
||||
const buttonText = localizedFromRecord(tile, 'buttonText', resolvedLang)
|
||||
const buttonLink = typeof tile.buttonLink === 'string' ? tile.buttonLink : ''
|
||||
const imageUrl = typeof tile.imageUrl === 'string' ? tile.imageUrl : ''
|
||||
const imageAspectRatio = typeof tile.imageAspectRatio === 'string' ? tile.imageAspectRatio : 'square'
|
||||
|
||||
return (
|
||||
<div key={idx} className="group flex flex-col items-center text-center space-y-5">
|
||||
{imageUrl && (
|
||||
<div className={`relative ${getAspectClass(imageAspectRatio)} w-full overflow-hidden rounded-3xl shadow-card`}>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={tileTitle || 'Campaign Card'}
|
||||
fill
|
||||
unoptimized
|
||||
className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
{tileTitle && (
|
||||
<h3 className="text-xl font-semibold leading-snug text-text-main md:text-2xl" style={{ color: textColor }}>
|
||||
{tileTitle}
|
||||
</h3>
|
||||
)}
|
||||
{tileDesc && (
|
||||
<p className="text-sm leading-relaxed text-muted-text md:text-base" style={{ color: textColor }}>
|
||||
{tileDesc}
|
||||
</p>
|
||||
)}
|
||||
{buttonText && buttonLink && (
|
||||
<div className="pt-2">
|
||||
<Link
|
||||
href={buttonLink}
|
||||
className="inline-flex rounded-full border border-current px-8 py-3 text-xs font-bold uppercase tracking-[0.22em] transition hover:bg-brand-primary hover:text-surface-card"
|
||||
>
|
||||
{buttonText}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function EditorialCarouselSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
|
||||
const i18nLang = useLang()
|
||||
const resolvedLang = lang === 'all' ? i18nLang : lang
|
||||
@@ -1415,12 +1608,30 @@ function ContactFormSection({ payload, lang }: { payload: Record<string, unknown
|
||||
)
|
||||
}
|
||||
|
||||
import { useEffect } from 'react'
|
||||
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
|
||||
const lang = langProp ?? i18nLang
|
||||
|
||||
useEffect(() => {
|
||||
const HERO_TYPES = new Set(['HERO', 'HERO_V2', 'HERO_VIDEO'])
|
||||
const firstActiveSection = sections.find(s => s.isActive && s.type.toUpperCase() !== 'ANNOUNCEMENT_BAR')
|
||||
const isHero = Boolean(firstActiveSection && HERO_TYPES.has(firstActiveSection.type.toUpperCase()))
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.dataset.heroFirst = isHero ? 'true' : 'false'
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.dataset.heroFirst = 'false'
|
||||
}
|
||||
}
|
||||
}, [sections])
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
{sections.map((section) => {
|
||||
@@ -1444,6 +1655,8 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
|
||||
<HeroV2Section payload={payload} lang={lang} />
|
||||
</ScrollReveal>
|
||||
)
|
||||
case 'HERO_VIDEO':
|
||||
return <HeroVideoSection key={section.id} payload={payload} lang={lang} />
|
||||
case 'USP_BAR':
|
||||
return (
|
||||
<ScrollReveal key={section.id}>
|
||||
@@ -1492,6 +1705,12 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
|
||||
<EditorialTileSection payload={payload} lang={lang} />
|
||||
</ScrollReveal>
|
||||
)
|
||||
case 'FEATURED_TILES':
|
||||
return (
|
||||
<ScrollReveal key={section.id}>
|
||||
<FeaturedTilesSection payload={payload} lang={lang} />
|
||||
</ScrollReveal>
|
||||
)
|
||||
case 'EDITORIAL_CAROUSEL':
|
||||
return (
|
||||
<ScrollReveal key={section.id}>
|
||||
|
||||
Reference in new issue
Block a user