x
This commit is contained in:
1 parent
8f64d89f6f
commit
b17e589577
18 files changed
+918
-517
No files matched your search
@@ -573,3 +573,11 @@ body:has(.announcement-bar) .global-navbar[data-scrolled="false"] {
|
||||
top: 44px; /* Adjusts Navbar down when AnnouncementBar is present at the top */
|
||||
}
|
||||
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import Image from 'next/image'
|
||||
import Link from '../LocalizedLink'
|
||||
import { Clock, Mail, MapPin, MessageCircle, Send } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Clock, Mail, MapPin, MessageCircle, Send, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { useState, useRef } from 'react'
|
||||
import type { Category, PageSection, Product } from '@homaroma/types'
|
||||
import { getCategoryImage } from '@homaroma/types'
|
||||
import ProductCard from '../ProductCard'
|
||||
@@ -219,6 +219,10 @@ function ProductsSection({ payload, lang }: { payload: Record<string, unknown>,
|
||||
const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang)
|
||||
const layout = typeof payload.layout === 'string' ? payload.layout : 'grid'
|
||||
const products = getProducts(payload)
|
||||
|
||||
const carouselRef = useRef<HTMLDivElement>(null)
|
||||
const scrollLeft = () => carouselRef.current?.scrollBy({ left: -300, behavior: 'smooth' })
|
||||
const scrollRight = () => carouselRef.current?.scrollBy({ left: 300, behavior: 'smooth' })
|
||||
|
||||
return (
|
||||
<section className="bg-surface-page ui-section-band">
|
||||
@@ -235,21 +239,27 @@ function ProductsSection({ payload, lang }: { payload: Record<string, unknown>,
|
||||
{products.length ? (
|
||||
layout === 'carousel' ? (
|
||||
<div className="relative -mx-4 px-4 sm:mx-0 sm:px-0">
|
||||
<Swiper
|
||||
slidesPerView={1.2}
|
||||
spaceBetween={16}
|
||||
breakpoints={{
|
||||
640: { slidesPerView: 2.2, spaceBetween: 24 },
|
||||
1024: { slidesPerView: 3, spaceBetween: 32 },
|
||||
}}
|
||||
className="w-full"
|
||||
<button
|
||||
onClick={scrollLeft}
|
||||
className="hidden md:flex bg-surface-card rounded-full shadow-card hover:text-brand-primary h-10 w-10 items-center justify-center absolute z-10 top-1/2 -translate-y-1/2 -left-5"
|
||||
aria-label="Scroll left"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</button>
|
||||
<div ref={carouselRef} className="flex overflow-x-auto snap-x snap-mandatory hide-scrollbar gap-4 pb-4">
|
||||
{products.map((product) => (
|
||||
<SwiperSlide key={product.id} className="!h-auto pb-4">
|
||||
<div key={product.id} className="w-[85vw] sm:w-[300px] flex-none snap-start">
|
||||
<ProductCard product={product} />
|
||||
</SwiperSlide>
|
||||
</div>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
<button
|
||||
onClick={scrollRight}
|
||||
className="hidden md:flex bg-surface-card rounded-full shadow-card hover:text-brand-primary h-10 w-10 items-center justify-center absolute z-10 top-1/2 -translate-y-1/2 -right-5"
|
||||
aria-label="Scroll right"
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||
@@ -258,7 +268,7 @@ function ProductsSection({ payload, lang }: { payload: Record<string, unknown>,
|
||||
)
|
||||
) : (
|
||||
<p className="ui-section-card bg-surface-card p-8 text-center text-muted-text">
|
||||
{t('dynamic.emptyProducts', { lng: resolvedLang })}
|
||||
{t('dynamic.noProducts', { lng: resolvedLang })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -387,18 +397,20 @@ function EditorialCarouselSection({ payload, lang }: { payload: Record<string, u
|
||||
const resolvedLang = lang === 'all' ? i18nLang : lang
|
||||
const title = localizedFromRecord(payload, 'title', resolvedLang)
|
||||
const slides = Array.isArray(payload.slides) ? payload.slides.map(toRecord) : []
|
||||
const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined
|
||||
const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined
|
||||
if (!slides.length) return null
|
||||
|
||||
return (
|
||||
<section className="ui-section-dark">
|
||||
<section className="ui-section-dark py-16 md:py-24" style={{ backgroundColor, color: textColor }}>
|
||||
<div className="mx-auto max-w-7xl space-y-12">
|
||||
{title && (
|
||||
<div className="mx-auto max-w-3xl space-y-4 text-center">
|
||||
<h2 className="text-4xl font-semibold text-surface-card md:text-5xl">{renderAccentTitle(title)}</h2>
|
||||
<h2 className="text-4xl font-semibold text-surface-card md:text-5xl" style={{ color: textColor }}>{renderAccentTitle(title)}</h2>
|
||||
<div className="mx-auto h-px w-16 bg-brand-accent" />
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<div className="flex overflow-x-auto snap-x snap-mandatory hide-scrollbar gap-6 pb-8">
|
||||
{slides.map((slide, index) => {
|
||||
const slideTitle = localizedFromRecord(slide, 'title', resolvedLang)
|
||||
const description = localizedFromRecord(slide, 'description', resolvedLang)
|
||||
@@ -408,8 +420,8 @@ function EditorialCarouselSection({ payload, lang }: { payload: Record<string, u
|
||||
const imageUrl = typeof slide.imageUrl === 'string' ? slide.imageUrl : ''
|
||||
|
||||
return (
|
||||
<article key={index} className="ui-section-dark-card group">
|
||||
{imageUrl && (
|
||||
<article key={index} className="ui-section-dark-card group w-[85vw] md:w-[350px] flex-none snap-start">
|
||||
{imageUrl ? (
|
||||
<div className="relative aspect-[4/3] overflow-hidden">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
@@ -419,15 +431,19 @@ function EditorialCarouselSection({ payload, lang }: { payload: Record<string, u
|
||||
className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4 p-6">
|
||||
{category && <p className="text-[10px] font-bold uppercase tracking-[0.24em] text-brand-accent">{category}</p>}
|
||||
{slideTitle && <h3 className="ui-section-dark-card-title">{renderAccentTitle(slideTitle)}</h3>}
|
||||
{description && <p className="ui-section-dark-card-copy">{description}</p>}
|
||||
) : null}
|
||||
<div className="space-y-4 p-6 flex flex-col flex-1 justify-between">
|
||||
<div>
|
||||
{category && <p className="text-[10px] font-bold uppercase tracking-[0.24em] text-brand-accent mb-2">{category}</p>}
|
||||
{slideTitle && <h3 className="ui-section-dark-card-title text-xl mb-3">{renderAccentTitle(slideTitle)}</h3>}
|
||||
{description && <p className="ui-section-dark-card-copy text-sm line-clamp-3">{description}</p>}
|
||||
</div>
|
||||
{buttonText && buttonLink && (
|
||||
<Link href={buttonLink} className="ui-section-dark-link">
|
||||
{buttonText} →
|
||||
</Link>
|
||||
<div className="pt-2">
|
||||
<Link href={buttonLink} className="ui-section-dark-link inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-colors group/link">
|
||||
{buttonText} <span className="group-hover/link:translate-x-1 transition-transform">→</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
@@ -802,8 +818,9 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
|
||||
case 'CATEGORIES':
|
||||
return <CategoriesSection key={section.id} payload={payload} categories={categories} lang={lang} />
|
||||
case 'PRODUCTS':
|
||||
case 'PRODUCT_CAROUSEL':
|
||||
return <ProductsSection key={section.id} payload={payload} lang={lang} />
|
||||
case 'PRODUCT_CAROUSEL':
|
||||
return <ProductsSection key={section.id} payload={{ ...payload, layout: 'carousel' }} lang={lang} />
|
||||
case 'BANNER':
|
||||
return <BannerSection key={section.id} payload={payload} lang={lang} />
|
||||
case 'TEXT':
|
||||
|
||||
Reference in new issue
Block a user