diff --git a/estore/src/components/BouquetPicker.tsx b/estore/src/components/BouquetPicker.tsx index 34f2f45..be836c6 100644 --- a/estore/src/components/BouquetPicker.tsx +++ b/estore/src/components/BouquetPicker.tsx @@ -22,18 +22,17 @@ interface Props { export default function BouquetPicker({ product, onClose }: Props) { const { addToCart } = useCart() - const [buildItems, setBuildItems] = useState([]) - const [loading, setLoading] = useState(true) + const [mylarItems, setMylarItems] = useState([]) + const [latexItems, setLatexItems] = useState([]) const [colorFamilies, setColorFamilies] = useState([]) - const [debugSlugs, setDebugSlugs] = useState([]) + const [loading, setLoading] = useState(true) // mylar: itemId → { variationId, quantity } - const [mylarState, setMylarState] = useState>({}) + const [mylarState, setMylarState] = useState>({}) // latex: itemId → selected color names const [latexColors, setLatexColors] = useState>({}) - - const mylarItems = useMemo(() => buildItems.filter((i) => !i.showColors), [buildItems]) - const latexItems = useMemo(() => buildItems.filter((i) => i.showColors), [buildItems]) + // modifiers: itemId → listId → selected optionIds + const [modChoices, setModChoices] = useState>>({}) const totalMylars = useMemo( () => Object.values(mylarState).reduce((sum, s) => sum + s.quantity, 0), @@ -52,26 +51,34 @@ export default function BouquetPicker({ product, onClose }: Props) { const s = mylarState[item.id] if (!s || s.quantity === 0) return const v = item.variations.find((v) => v.id === s.variationId) - total += (v?.priceCents ?? item.price ?? 0) * s.quantity + const base = v?.priceCents ?? item.price ?? 0 + const modDelta = Object.entries(modChoices[item.id] ?? {}).reduce((sum, [listId, optIds]) => { + const ml = item.modifiers.find((m) => m.id === listId) + if (!ml) return sum + return sum + optIds.reduce((s2, optId) => s2 + (ml.options.find((o) => o.id === optId)?.priceDelta ?? 0), 0) + }, 0) + total += (base + modDelta) * s.quantity }) latexItems.forEach((item) => { const colors = latexColors[item.id] ?? [] total += (item.price ?? 0) * colors.length }) return total - }, [mylarItems, latexItems, mylarState, latexColors]) + }, [mylarItems, latexItems, mylarState, latexColors, modChoices]) useEffect(() => { Promise.all([ + // Build-category items (bypasses Online filter) → mylar grid fetch(BASE + '/api/bouquet-items').then((r) => r.ok ? r.json() : { items: [] }), + // Regular catalog → latex items (showColors = true, already Online) + fetch(BASE + '/api/catalog').then((r) => r.ok ? r.json() : { items: [] }), fetch(BASE + '/colors.json').then((r) => r.ok ? r.json() : []), - ]).then(([{ items }, families]: [{ items: CatalogItem[] }, ColorFamily[]]) => { - // /api/bouquet-items already filters by the Build category server-side - const mylars = (items as CatalogItem[]).filter((i) => !i.showColors) - const latex = (items as CatalogItem[]).filter((i) => i.showColors) - setDebugSlugs([`${(items as CatalogItem[]).length} items fetched (${mylars.length} mylar, ${latex.length} latex)`]) + ]).then(([bouquetRes, catalogRes, families]: [{ items: CatalogItem[] }, { items: CatalogItem[] }, ColorFamily[]]) => { + const mylars = bouquetRes.items.filter((i) => !i.showColors) + const latex = catalogRes.items.filter((i) => i.showColors) - setBuildItems([...mylars, ...latex]) + setMylarItems(mylars) + setLatexItems(latex) setColorFamilies(families) const initMylar: Record = {} @@ -88,16 +95,26 @@ export default function BouquetPicker({ product, onClose }: Props) { const adjustMylar = (itemId: string, delta: number) => { if (delta > 0 && totalMylars >= MYLAR_MAX) return - setMylarState((prev) => { - const current = prev[itemId]?.quantity ?? 0 - return { ...prev, [itemId]: { ...prev[itemId], quantity: Math.max(0, current + delta) } } - }) + setMylarState((prev) => ({ + ...prev, + [itemId]: { ...prev[itemId], quantity: Math.max(0, (prev[itemId]?.quantity ?? 0) + delta) }, + })) } const setVariation = (itemId: string, variationId: string) => { setMylarState((prev) => ({ ...prev, [itemId]: { ...prev[itemId], variationId } })) } + const toggleMod = (itemId: string, listId: string, optId: string, multi: boolean) => { + setModChoices((prev) => { + const cur = prev[itemId]?.[listId] ?? [] + const next = multi + ? cur.includes(optId) ? cur.filter((x) => x !== optId) : [...cur, optId] + : cur.includes(optId) ? [] : [optId] + return { ...prev, [itemId]: { ...(prev[itemId] ?? {}), [listId]: next } } + }) + } + const toggleColor = (itemId: string, colorName: string) => { const current = latexColors[itemId] ?? [] if (current.includes(colorName)) { @@ -119,7 +136,7 @@ export default function BouquetPicker({ product, onClose }: Props) { product: item, quantity: s.quantity, selectedColors: [], - modifierChoices: {}, + modifierChoices: modChoices[item.id] ?? {}, notes: '', selectedVariationId: s.variationId, bouquetGroupId, @@ -161,55 +178,52 @@ export default function BouquetPicker({ product, onClose }: Props) {
- {/* Progress counters */} -
-
= MYLAR_MAX ? '#fff3cd' : '#f0f9fa', - border: `1px solid ${totalMylars >= MYLAR_MAX ? '#ffc107' : '#b2e0e4'}`, - textAlign: 'center', - }}> -
= MYLAR_MAX ? '#856404' : '#11b3be' }}> - {totalMylars}/{MYLAR_MAX} + + {/* ── Sticky progress counters ── */} +
+
+
= MYLAR_MAX ? '#fff3cd' : '#f0f9fa', + border: `1px solid ${totalMylars >= MYLAR_MAX ? '#ffc107' : '#b2e0e4'}`, + textAlign: 'center', + }}> +
= MYLAR_MAX ? '#856404' : '#11b3be' }}> + {totalMylars}/{MYLAR_MAX} +
+
Mylar balloons
-
Mylar balloons
-
-
= LATEX_MAX ? '#fff3cd' : '#f0f9fa', - border: `1px solid ${totalLatex >= LATEX_MAX ? '#ffc107' : '#b2e0e4'}`, - textAlign: 'center', - }}> -
= LATEX_MAX ? '#856404' : '#11b3be' }}> - {totalLatex}/{LATEX_MAX} +
= LATEX_MAX ? '#fff3cd' : '#f0f9fa', + border: `1px solid ${totalLatex >= LATEX_MAX ? '#ffc107' : '#b2e0e4'}`, + textAlign: 'center', + }}> +
= LATEX_MAX ? '#856404' : '#11b3be' }}> + {totalLatex}/{LATEX_MAX} +
+
Latex balloons
-
Latex balloons
{loading ? (

Loading…

- ) : buildItems.length === 0 ? ( -
-

No items found with category slug build.

-

Category slugs currently in your catalog:

-

- {debugSlugs.join(', ') || '(none — catalog may be empty or still loading)'} -

-

- The filter looks for build. If your Square category produces a different slug, share this list and it will be fixed. -

-
) : ( <> {/* ── Mylar section ── */} - {mylarItems.length > 0 && ( + {mylarItems.length === 0 ? ( +

+ No mylar items found. Add a “Build” category to items in Square. +

+ ) : (

Mylar Balloons - - (up to {MYLAR_MAX} total) - + (up to {MYLAR_MAX} total)

{mylarItems.map((item) => { @@ -223,19 +237,14 @@ export default function BouquetPicker({ product, onClose }: Props) { return (
0 ? '#f0f9fa' : '#fafaf8', display: 'flex', flexDirection: 'column', gap: '0.4rem', }}> {item.imageUrl && ( // eslint-disable-next-line @next/next/no-img-element - {item.name} + {item.name} )}
{item.name}
{itemPrice > 0 && ( @@ -246,56 +255,66 @@ export default function BouquetPicker({ product, onClose }: Props) { {selectableVars.length > 1 && (
{selectableVars.map((v) => ( - + >{v.name} ))}
)} {/* Quantity stepper */}
- - - {qty} - - + {qty} + + }}>+
+ + {/* Modifiers — shown when item is selected */} + {qty > 0 && item.modifiers.length > 0 && ( +
+ {item.modifiers.map((ml) => ( +
+
{ml.name}
+
+ {ml.options.map((opt) => { + const chosen = (modChoices[item.id]?.[ml.id] ?? []).includes(opt.id) + return ( + + ) + })} +
+
+ ))} +
+ )}
) })} @@ -312,15 +331,13 @@ export default function BouquetPicker({ product, onClose }: Props) { (up to {LATEX_MAX} total — tap a color to add)

- {latexItems.map((item) => { const selected = latexColors[item.id] ?? [] return (
{latexItems.length > 1 && (

- {item.name} - {item.price ? ` · ${fmt(item.price)} each` : ''} + {item.name}{item.price ? ` · ${fmt(item.price)} each` : ''}

)} {selected.length > 0 && ( @@ -333,27 +350,18 @@ export default function BouquetPicker({ product, onClose }: Props) { const isSelected = selected.includes(name) const atCap = totalLatex >= LATEX_MAX return ( -