This commit is contained in:
Vula Builder
2026-07-28 12:39:45 +00:00
parent 3971ceff02
commit 7a020c9a4f
@@ -0,0 +1,79 @@
import Image from 'next/image'
import Link from 'next/link'
import { getProductCarousels } from '@/lib/cms'
const BACKEND_URL = process.env.VULA_ECOMMERCE_BACKEND_URL || 'http://vula-ecommerce:9000'
const PUB_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || ''
interface MedusaProduct {
id: string; title: string; thumbnail: string | null
variants?: Array<{ calculated_price?: { calculated_amount?: number; currency_code?: string } }>
}
const CURRENCY_SYMBOLS: Record<string, string> = { zar: 'R', usd: '$', eur: '€', gbp: '£', kes: 'KSh', ngn: '₦', ghs: '₵' }
const fmt = (amount: number, code?: string) =>
`${CURRENCY_SYMBOLS[(code ?? '').toLowerCase()] ?? (code?.toUpperCase() ?? '')}${(amount / 100).toFixed(2)}`
async function fetchProductsByIds(ids: string[]): Promise<MedusaProduct[]> {
if (!ids.length) return []
try {
const query = ids.map(id => `id[]=${id}`).join('&')
const res = await fetch(
`${BACKEND_URL}/store/products?${query}&fields=id,title,thumbnail,variants.calculated_price&limit=${ids.length}`,
{ headers: { 'x-publishable-api-key': PUB_KEY }, next: { revalidate: 300 } }
)
if (!res.ok) return []
const data = await res.json() as { products?: MedusaProduct[] }
return data.products ?? []
} catch { return [] }
}
export default async function ProductCarouselSection() {
const carousels = await getProductCarousels()
if (!carousels.length) return null
const sections = await Promise.all(
carousels.map(async c => {
const ids = Array.isArray(c.data.product_ids) ? (c.data.product_ids as string[]) : []
const prods = await fetchProductsByIds(ids)
return { title: c.data.title as string, products: prods }
})
)
return (
<>
{sections.filter(s => s.products.length > 0).map(section => (
<section key={section.title} className="py-8 px-4">
<div className="max-w-7xl mx-auto">
<h2 className="text-xl font-bold mb-5">{section.title}</h2>
<div className="flex gap-4 overflow-x-auto pb-3 scrollbar-hide snap-x snap-mandatory">
{section.products.map(product => {
const variant = product.variants?.[0]
const price = variant?.calculated_price
return (
<Link
key={product.id}
href={`/products/${product.id}`}
className="flex-none snap-start w-44 group"
>
<div className="relative h-44 w-44 rounded-xl overflow-hidden bg-muted mb-2 group-hover:shadow-md transition-shadow">
{product.thumbnail ? (
<Image src={product.thumbnail} alt={product.title} fill className="object-cover group-hover:scale-105 transition-transform duration-300" />
) : (
<div className="w-full h-full flex items-center justify-center text-muted-foreground text-xs">No image</div>
)}
</div>
<p className="text-xs font-medium line-clamp-2 group-hover:text-primary transition-colors mb-1">{product.title}</p>
{price?.calculated_amount && (
<p className="text-xs font-bold text-primary">{fmt(price.calculated_amount, price.currency_code)}</p>
)}
</Link>
)
})}
</div>
</div>
</section>
))}
</>
)
}