diff --git a/estore/src/components/BouquetPicker.tsx b/estore/src/components/BouquetPicker.tsx index be836c6..1631e4d 100644 --- a/estore/src/components/BouquetPicker.tsx +++ b/estore/src/components/BouquetPicker.tsx @@ -9,9 +9,15 @@ import { fmt } from '@/lib/format' const MYLAR_MAX = 6 const LATEX_MAX = 6 +interface ColorEntry { + name: string + hex: string + image?: string +} + interface ColorFamily { family: string - colors: { name: string; hex: string }[] + colors: ColorEntry[] } interface Props { @@ -22,28 +28,29 @@ interface Props { export default function BouquetPicker({ product, onClose }: Props) { const { addToCart } = useCart() - const [mylarItems, setMylarItems] = useState([]) - const [latexItems, setLatexItems] = useState([]) + const [mylarItems, setMylarItems] = useState([]) + const [latexItems, setLatexItems] = useState([]) const [colorFamilies, setColorFamilies] = useState([]) - const [loading, setLoading] = useState(true) + const [loading, setLoading] = useState(true) // mylar: itemId → { variationId, quantity } - const [mylarState, setMylarState] = useState>({}) - // latex: itemId → selected color names - const [latexColors, setLatexColors] = useState>({}) - // modifiers: itemId → listId → selected optionIds - const [modChoices, setModChoices] = useState>>({}) + const [mylarState, setMylarState] = useState>({}) + // modifiers per mylar item: itemId → listId → optionIds[] + const [modChoices, setModChoices] = useState>>({}) + + // latex: flat list of chosen color names (each = 1 balloon) + const [latexSelections, setLatexSelections] = useState([]) + const [latexItemIdx, setLatexItemIdx] = useState(0) + const [openFamily, setOpenFamily] = useState(null) const totalMylars = useMemo( () => Object.values(mylarState).reduce((sum, s) => sum + s.quantity, 0), [mylarState], ) - const totalLatex = useMemo( - () => Object.values(latexColors).reduce((sum, c) => sum + c.length, 0), - [latexColors], - ) + const totalLatex = latexSelections.length + const canAdd = totalMylars > 0 || totalLatex > 0 - const canAdd = totalMylars > 0 || totalLatex > 0 + const allColors = useMemo(() => colorFamilies.flatMap((f) => f.colors), [colorFamilies]) const totalCents = useMemo(() => { let total = 0 @@ -59,23 +66,29 @@ export default function BouquetPicker({ product, onClose }: Props) { }, 0) total += (base + modDelta) * s.quantity }) - latexItems.forEach((item) => { - const colors = latexColors[item.id] ?? [] - total += (item.price ?? 0) * colors.length - }) + const latexItem = latexItems[latexItemIdx] + if (latexItem && totalLatex > 0) total += (latexItem.price ?? 0) * totalLatex return total - }, [mylarItems, latexItems, mylarState, latexColors, modChoices]) + }, [mylarItems, latexItems, latexItemIdx, mylarState, modChoices, totalLatex]) 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(([bouquetRes, catalogRes, families]: [{ items: CatalogItem[] }, { items: CatalogItem[] }, ColorFamily[]]) => { + ]).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) + // Only real latex balloons: must have "latex" in their categories slug list + const latex = (catalogRes.items as CatalogItem[]).filter( + (i) => i.showColors && (i.categories ?? []).includes('latex') + ) + + console.log('[BouquetPicker] mylars:', mylars.length, 'latex items:', latex.map((i) => i.name)) + console.log('[BouquetPicker] mylar modifiers:', mylars.map((i) => ({ name: i.name, modifiers: i.modifiers }))) setMylarItems(mylars) setLatexItems(latex) @@ -115,14 +128,13 @@ export default function BouquetPicker({ product, onClose }: Props) { }) } - const toggleColor = (itemId: string, colorName: string) => { - const current = latexColors[itemId] ?? [] - if (current.includes(colorName)) { - setLatexColors((prev) => ({ ...prev, [itemId]: current.filter((c) => c !== colorName) })) - } else { - if (totalLatex >= LATEX_MAX) return - setLatexColors((prev) => ({ ...prev, [itemId]: [...current, colorName] })) - } + const addLatexColor = (name: string) => { + if (totalLatex >= LATEX_MAX) return + setLatexSelections((prev) => [...prev, name]) + } + + const removeLatexColor = (idx: number) => { + setLatexSelections((prev) => prev.filter((_, i) => i !== idx)) } const handleAdd = () => { @@ -143,36 +155,29 @@ export default function BouquetPicker({ product, onClose }: Props) { }) }) - latexItems.forEach((item) => { - const colors = latexColors[item.id] ?? [] - if (colors.length === 0) return + const latexItem = latexItems[latexItemIdx] + if (latexItem && latexSelections.length > 0) { addToCart({ - product: item, - quantity: colors.length, - selectedColors: colors, + product: latexItem, + quantity: latexSelections.length, + selectedColors: latexSelections, modifierChoices: {}, notes: '', bouquetGroupId, }) - }) + } onClose() } - const allColors = useMemo( - () => colorFamilies.flatMap((f) => f.colors), - [colorFamilies], - ) - return (
-
+

Build Your Bouquet - {product.name !== 'Build Your Own Bouquet' && ` — ${product.name}`}

@@ -185,28 +190,22 @@ export default function BouquetPicker({ product, onClose }: Props) { background: '#fff', paddingBottom: '0.75rem', marginBottom: '0.5rem', }}>
-
= MYLAR_MAX ? '#fff3cd' : '#f0f9fa', - border: `1px solid ${totalMylars >= MYLAR_MAX ? '#ffc107' : '#b2e0e4'}`, - textAlign: 'center', - }}> -
= MYLAR_MAX ? '#856404' : '#11b3be' }}> - {totalMylars}/{MYLAR_MAX} + {[ + { label: 'Mylar balloons', count: totalMylars, max: MYLAR_MAX }, + { label: 'Latex balloons', count: totalLatex, max: LATEX_MAX }, + ].map(({ label, count, max }) => ( +
= max ? '#fff3cd' : '#f0f9fa', + border: `1px solid ${count >= max ? '#ffc107' : '#b2e0e4'}`, + textAlign: 'center', + }}> +
= max ? '#856404' : '#11b3be' }}> + {count}/{max} +
+
{label}
-
Mylar balloons
-
-
= LATEX_MAX ? '#fff3cd' : '#f0f9fa', - border: `1px solid ${totalLatex >= LATEX_MAX ? '#ffc107' : '#b2e0e4'}`, - textAlign: 'center', - }}> -
= LATEX_MAX ? '#856404' : '#11b3be' }}> - {totalLatex}/{LATEX_MAX} -
-
Latex balloons
-
+ ))}
@@ -215,17 +214,13 @@ export default function BouquetPicker({ product, onClose }: Props) { ) : ( <> {/* ── Mylar section ── */} - {mylarItems.length === 0 ? ( -

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

- ) : ( + {mylarItems.length > 0 && (

Mylar Balloons (up to {MYLAR_MAX} total)

-
+
{mylarItems.map((item) => { const s = mylarState[item.id] const qty = s?.quantity ?? 0 @@ -233,11 +228,12 @@ export default function BouquetPicker({ product, onClose }: Props) { (v) => !(v.inventory !== null && v.inventory <= 0) ) const currentVar = item.variations.find((v) => v.id === s?.variationId) - const itemPrice = currentVar?.priceCents ?? item.price ?? 0 + const itemPrice = currentVar?.priceCents ?? item.price ?? 0 return (
0 ? '#11b3be' : '#e6dfc8'}`, + borderRadius: 10, padding: '0.65rem', background: qty > 0 ? '#f0f9fa' : '#fafaf8', display: 'flex', flexDirection: 'column', gap: '0.4rem', }}> @@ -259,8 +255,9 @@ export default function BouquetPicker({ product, onClose }: Props) { style={{ fontSize: '0.72rem', padding: '2px 6px', borderRadius: 4, border: '1px solid', borderColor: s?.variationId === v.id ? '#11b3be' : '#ccc', - background: s?.variationId === v.id ? '#11b3be' : '#fff', - color: s?.variationId === v.id ? '#fff' : '#333', cursor: 'pointer', + background: s?.variationId === v.id ? '#11b3be' : '#fff', + color: s?.variationId === v.id ? '#fff' : '#333', + cursor: 'pointer', }} >{v.name} ))} @@ -280,19 +277,26 @@ export default function BouquetPicker({ product, onClose }: Props) { style={{ width: 26, height: 26, borderRadius: '50%', border: '1px solid', borderColor: totalMylars >= MYLAR_MAX ? '#ccc' : '#11b3be', - background: totalMylars >= MYLAR_MAX ? '#f5f5f5' : '#11b3be', - color: totalMylars >= MYLAR_MAX ? '#aaa' : '#fff', + background: totalMylars >= MYLAR_MAX ? '#f5f5f5' : '#11b3be', + color: totalMylars >= MYLAR_MAX ? '#aaa' : '#fff', cursor: totalMylars >= MYLAR_MAX ? 'default' : 'pointer', fontSize: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'center', }}>+
- {/* Modifiers — shown when item is selected */} - {qty > 0 && item.modifiers.length > 0 && ( -
+ {/* Modifiers — always visible, dimmed when qty = 0 */} + {item.modifiers.length > 0 && ( +
{item.modifiers.map((ml) => ( -
-
{ml.name}
+
+
+ {ml.name} + {ml.minSelected > 0 && *} +
{ml.options.map((opt) => { const chosen = (modChoices[item.id]?.[ml.id] ?? []).includes(opt.id) @@ -300,10 +304,11 @@ export default function BouquetPicker({ product, onClose }: Props) {
))} + {qty === 0 && ( +

+ Add to select options +

+ )}
)}
@@ -323,54 +333,161 @@ export default function BouquetPicker({ product, onClose }: Props) { )} {/* ── Latex section ── */} - {latexItems.length > 0 && allColors.length > 0 && ( -
-

- Latex Balloons - - (up to {LATEX_MAX} total — tap a color to add) - +

+

+ Latex Balloons + (up to {LATEX_MAX} total) +

+ + {/* Size selector when there are multiple latex items */} + {latexItems.length > 1 && ( +
+ {latexItems.map((item, idx) => ( + + ))} +
+ )} + + {latexItems.length === 0 && ( +

+ No latex balloon items found in the catalog.

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

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

- )} - {selected.length > 0 && ( -

- Selected: {selected.join(', ')} -

- )} -
- {allColors.map(({ name, hex }) => { - const isSelected = selected.includes(name) - const atCap = totalLatex >= LATEX_MAX - return ( - + ) + })} +
+
+ )} + + {/* Collapsible color family picker */} + {latexItems.length > 0 && totalLatex < LATEX_MAX && colorFamilies.length > 0 && ( +
+

+ Tap a family to browse, then tap a color to add it. +

+ {colorFamilies.map((family) => { + const isOpen = openFamily === family.family + const familyChosen = family.colors.filter((c) => latexSelections.includes(c.name)).length + + return ( +
+ + + {isOpen && ( +
+ {family.colors.map((c) => { + const isSelected = latexSelections.includes(c.name) + return ( +
+ )}
-
- ) - })} -
- )} + ) + })} +
+ )} + + {latexItems.length > 0 && totalLatex >= LATEX_MAX && ( +

+ Maximum {LATEX_MAX} reached — remove a color above to change it. +

+ )} +
)}