'use client' import { useState, useEffect, useMemo } from 'react' import ProductCard from './ProductCard' import WelcomeModal from './WelcomeModal' import GuidedTour from './GuidedTour' import type { CatalogItem } from '@/data/mock-catalog' interface ActiveOccasion { key: string label: string emoji: string blurb: string squareCategorySlug?: string } export default function FeaturedProducts() { const [items, setItems] = useState([]) const [activeOccasions, setActiveOccasions] = useState([]) const [catOrder, setCatOrder] = useState([]) const [catHidden, setCatHidden] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(false) const [category, setCategory] = useState('all') const [search, setSearch] = useState('') const [searchOpen, setSearchOpen] = useState(false) const [showWelcome, setShowWelcome] = useState(false) const [showTour, setShowTour] = useState(false) // Show welcome modal once per browser (after products load so tour targets exist) useEffect(() => { if (!loading && !localStorage.getItem('bpb_seen_welcome')) { setShowWelcome(true) } }, [loading]) const dismissWelcome = () => { localStorage.setItem('bpb_seen_welcome', '1') setShowWelcome(false) } const startTour = () => { localStorage.setItem('bpb_seen_welcome', '1') setShowWelcome(false) setShowTour(true) } const tourInit = () => { setCategory('all') setSearch('') setSearchOpen(false) } const endTour = () => { setShowTour(false) // Close any customization modal that may have been opened during the tour window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })) } const productCategories = useMemo(() => { const seen = new Map() items.forEach((item) => { const cats = item.categories ?? [item.category] const labels = item.categoryLabels ?? [item.categoryLabel] cats.forEach((slug, i) => { if (!seen.has(slug)) seen.set(slug, labels[i] ?? slug) }) }) const all = Array.from(seen.entries()).map(([key, label]) => ({ key, label })) const visible = all.filter((c) => !catHidden.includes(c.key)) visible.sort((a, b) => { const ai = catOrder.indexOf(a.key) const bi = catOrder.indexOf(b.key) if (ai === -1 && bi === -1) return 0 if (ai === -1) return 1 if (bi === -1) return -1 return ai - bi }) return visible }, [items, catOrder, catHidden]) const tabs = useMemo(() => { // Category slugs already represented by an occasion tab — hide them from the regular tabs const occasionSlugs = new Set(activeOccasions.map((o) => o.squareCategorySlug).filter(Boolean) as string[]) return [ ...activeOccasions.map((o) => ({ key: o.key, label: `${o.emoji} ${o.label}`, occasion: true })), { key: 'all', label: 'All', occasion: false }, ...productCategories.filter((c) => !occasionSlugs.has(c.key)).map((c) => ({ ...c, occasion: false })), ] }, [activeOccasions, productCategories]) const activeOccasion: ActiveOccasion | undefined = useMemo( () => activeOccasions.find((o) => o.key === category), [activeOccasions, category] ) useEffect(() => { Promise.all([ fetch('/api/catalog').then((r) => { if (!r.ok) throw new Error('catalog error'); return r.json() }), fetch('/api/inventory').then((r) => r.ok ? r.json() : { counts: {} }).catch(() => ({ counts: {} })), fetch('/api/occasions').then((r) => r.ok ? r.json() : { occasions: [] }).catch(() => ({ occasions: [] })), fetch('/api/categories-display').then((r) => r.ok ? r.json() : { order: [], hidden: [] }).catch(() => ({ order: [], hidden: [] })), ]) .then(([{ items }, { counts }, { occasions }, catDisplay]: [ { items: CatalogItem[] }, { counts: Record }, { occasions: ActiveOccasion[] }, { order: string[]; hidden: string[] }, ]) => { const withInventory = items.map((item) => ({ ...item, variations: item.variations.map((v) => v.id in counts ? { ...v, inventory: counts[v.id] } : v ), })) setItems(withInventory) setActiveOccasions(occasions) setCatOrder(catDisplay.order ?? []) setCatHidden(catDisplay.hidden ?? []) if (occasions.length > 0) setCategory(occasions[0].key) setLoading(false) }) .catch(() => { setError(true); setLoading(false) }) }, []) const q = search.trim().toLowerCase() const filtered = (activeOccasion ? activeOccasion.squareCategorySlug ? items.filter((i) => (i.categories ?? [i.category]).includes(activeOccasion.squareCategorySlug!)) : items : category === 'all' ? items : items.filter((i) => (i.categories ?? [i.category]).includes(category)) ).filter((i) => !q || i.name.toLowerCase().includes(q) || i.description.toLowerCase().includes(q) ) return (
{/* Section header */}

Shop

Choose an arrangement, pick your colors, and place your order.

{/* Persistent tour button */}
{/* Category tabs + search */}
{searchOpen ? (
{ setSearch(e.target.value); if (e.target.value) setCategory('all') }} onBlur={() => { if (!search) setSearchOpen(false) }} onKeyDown={(e) => { if (e.key === 'Escape') { setSearch(''); setSearchOpen(false) } }} style={{ width: '160px' }} />
) : ( )}
{/* Holiday/occasion banner */} {activeOccasion && (
{activeOccasion.emoji}

{activeOccasion.label}

{activeOccasion.blurb}

)} {/* Welcome modal + guided tour */} {showWelcome && } {showTour && } {/* Product grid */} {loading ? ( ) : error ? (

Unable to load products right now. Please try again shortly.

) : filtered.length === 0 ? (

{q ? `No results for "${search}".` : activeOccasion ? `Our ${activeOccasion.label} collection is coming soon — check back shortly!` : 'No products in this category yet.' }

) : (
{filtered.map((item, index) => (
))}
)}
) } function SkeletonGrid() { return (
{Array.from({ length: 8 }).map((_, i) => (
))}
) }