This commit is contained in:
Your Name committed 2026-06-27 23:19:51 -04:00
1 parent 8f64d89f6f
commit b17e589577
18 files changed
+918 -517

No files matched your search

@@ -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} &rarr;
</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">&rarr;</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':