BouquetPicker: fix latex, sticky counters, modifier support
- Latex items now fetched from /api/catalog (Online items with showColors) instead of requiring Build-category items — latex section always shows - Mylar/latex counter boxes are sticky so they stay visible while scrolling - Modifier lists (e.g. Helium Weight) render per mylar item once qty > 0, with price deltas included in running total and cart entry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5e1db823cb
commit
c9bd3e50d0
@ -22,18 +22,17 @@ interface Props {
|
|||||||
export default function BouquetPicker({ product, onClose }: Props) {
|
export default function BouquetPicker({ product, onClose }: Props) {
|
||||||
const { addToCart } = useCart()
|
const { addToCart } = useCart()
|
||||||
|
|
||||||
const [buildItems, setBuildItems] = useState<CatalogItem[]>([])
|
const [mylarItems, setMylarItems] = useState<CatalogItem[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [latexItems, setLatexItems] = useState<CatalogItem[]>([])
|
||||||
const [colorFamilies, setColorFamilies] = useState<ColorFamily[]>([])
|
const [colorFamilies, setColorFamilies] = useState<ColorFamily[]>([])
|
||||||
const [debugSlugs, setDebugSlugs] = useState<string[]>([])
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
// mylar: itemId → { variationId, quantity }
|
// mylar: itemId → { variationId, quantity }
|
||||||
const [mylarState, setMylarState] = useState<Record<string, { variationId: string; quantity: number }>>({})
|
const [mylarState, setMylarState] = useState<Record<string, { variationId: string; quantity: number }>>({})
|
||||||
// latex: itemId → selected color names
|
// latex: itemId → selected color names
|
||||||
const [latexColors, setLatexColors] = useState<Record<string, string[]>>({})
|
const [latexColors, setLatexColors] = useState<Record<string, string[]>>({})
|
||||||
|
// modifiers: itemId → listId → selected optionIds
|
||||||
const mylarItems = useMemo(() => buildItems.filter((i) => !i.showColors), [buildItems])
|
const [modChoices, setModChoices] = useState<Record<string, Record<string, string[]>>>({})
|
||||||
const latexItems = useMemo(() => buildItems.filter((i) => i.showColors), [buildItems])
|
|
||||||
|
|
||||||
const totalMylars = useMemo(
|
const totalMylars = useMemo(
|
||||||
() => Object.values(mylarState).reduce((sum, s) => sum + s.quantity, 0),
|
() => 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]
|
const s = mylarState[item.id]
|
||||||
if (!s || s.quantity === 0) return
|
if (!s || s.quantity === 0) return
|
||||||
const v = item.variations.find((v) => v.id === s.variationId)
|
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) => {
|
latexItems.forEach((item) => {
|
||||||
const colors = latexColors[item.id] ?? []
|
const colors = latexColors[item.id] ?? []
|
||||||
total += (item.price ?? 0) * colors.length
|
total += (item.price ?? 0) * colors.length
|
||||||
})
|
})
|
||||||
return total
|
return total
|
||||||
}, [mylarItems, latexItems, mylarState, latexColors])
|
}, [mylarItems, latexItems, mylarState, latexColors, modChoices])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
|
// Build-category items (bypasses Online filter) → mylar grid
|
||||||
fetch(BASE + '/api/bouquet-items').then((r) => r.ok ? r.json() : { items: [] }),
|
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() : []),
|
fetch(BASE + '/colors.json').then((r) => r.ok ? r.json() : []),
|
||||||
]).then(([{ items }, families]: [{ items: CatalogItem[] }, ColorFamily[]]) => {
|
]).then(([bouquetRes, catalogRes, families]: [{ items: CatalogItem[] }, { items: CatalogItem[] }, ColorFamily[]]) => {
|
||||||
// /api/bouquet-items already filters by the Build category server-side
|
const mylars = bouquetRes.items.filter((i) => !i.showColors)
|
||||||
const mylars = (items as CatalogItem[]).filter((i) => !i.showColors)
|
const latex = catalogRes.items.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)`])
|
|
||||||
|
|
||||||
setBuildItems([...mylars, ...latex])
|
setMylarItems(mylars)
|
||||||
|
setLatexItems(latex)
|
||||||
setColorFamilies(families)
|
setColorFamilies(families)
|
||||||
|
|
||||||
const initMylar: Record<string, { variationId: string; quantity: number }> = {}
|
const initMylar: Record<string, { variationId: string; quantity: number }> = {}
|
||||||
@ -88,16 +95,26 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
|
|
||||||
const adjustMylar = (itemId: string, delta: number) => {
|
const adjustMylar = (itemId: string, delta: number) => {
|
||||||
if (delta > 0 && totalMylars >= MYLAR_MAX) return
|
if (delta > 0 && totalMylars >= MYLAR_MAX) return
|
||||||
setMylarState((prev) => {
|
setMylarState((prev) => ({
|
||||||
const current = prev[itemId]?.quantity ?? 0
|
...prev,
|
||||||
return { ...prev, [itemId]: { ...prev[itemId], quantity: Math.max(0, current + delta) } }
|
[itemId]: { ...prev[itemId], quantity: Math.max(0, (prev[itemId]?.quantity ?? 0) + delta) },
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const setVariation = (itemId: string, variationId: string) => {
|
const setVariation = (itemId: string, variationId: string) => {
|
||||||
setMylarState((prev) => ({ ...prev, [itemId]: { ...prev[itemId], variationId } }))
|
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 toggleColor = (itemId: string, colorName: string) => {
|
||||||
const current = latexColors[itemId] ?? []
|
const current = latexColors[itemId] ?? []
|
||||||
if (current.includes(colorName)) {
|
if (current.includes(colorName)) {
|
||||||
@ -119,7 +136,7 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
product: item,
|
product: item,
|
||||||
quantity: s.quantity,
|
quantity: s.quantity,
|
||||||
selectedColors: [],
|
selectedColors: [],
|
||||||
modifierChoices: {},
|
modifierChoices: modChoices[item.id] ?? {},
|
||||||
notes: '',
|
notes: '',
|
||||||
selectedVariationId: s.variationId,
|
selectedVariationId: s.variationId,
|
||||||
bouquetGroupId,
|
bouquetGroupId,
|
||||||
@ -161,55 +178,52 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section className="modal-card-body" style={{ padding: '1.25rem', overflowY: 'auto' }}>
|
<section className="modal-card-body" style={{ padding: '1.25rem', overflowY: 'auto' }}>
|
||||||
{/* Progress counters */}
|
|
||||||
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.25rem' }}>
|
{/* ── Sticky progress counters ── */}
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: 1, padding: '0.6rem 1rem', borderRadius: 8,
|
position: 'sticky', top: 0, zIndex: 10,
|
||||||
background: totalMylars >= MYLAR_MAX ? '#fff3cd' : '#f0f9fa',
|
background: '#fff', paddingBottom: '0.75rem', marginBottom: '0.5rem',
|
||||||
border: `1px solid ${totalMylars >= MYLAR_MAX ? '#ffc107' : '#b2e0e4'}`,
|
}}>
|
||||||
textAlign: 'center',
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
}}>
|
<div style={{
|
||||||
<div style={{ fontSize: '1.3rem', fontWeight: 700, color: totalMylars >= MYLAR_MAX ? '#856404' : '#11b3be' }}>
|
flex: 1, padding: '0.6rem 1rem', borderRadius: 8,
|
||||||
{totalMylars}/{MYLAR_MAX}
|
background: totalMylars >= MYLAR_MAX ? '#fff3cd' : '#f0f9fa',
|
||||||
|
border: `1px solid ${totalMylars >= MYLAR_MAX ? '#ffc107' : '#b2e0e4'}`,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '1.3rem', fontWeight: 700, color: totalMylars >= MYLAR_MAX ? '#856404' : '#11b3be' }}>
|
||||||
|
{totalMylars}/{MYLAR_MAX}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: '#555' }}>Mylar balloons</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '0.75rem', color: '#555' }}>Mylar balloons</div>
|
<div style={{
|
||||||
</div>
|
flex: 1, padding: '0.6rem 1rem', borderRadius: 8,
|
||||||
<div style={{
|
background: totalLatex >= LATEX_MAX ? '#fff3cd' : '#f0f9fa',
|
||||||
flex: 1, padding: '0.6rem 1rem', borderRadius: 8,
|
border: `1px solid ${totalLatex >= LATEX_MAX ? '#ffc107' : '#b2e0e4'}`,
|
||||||
background: totalLatex >= LATEX_MAX ? '#fff3cd' : '#f0f9fa',
|
textAlign: 'center',
|
||||||
border: `1px solid ${totalLatex >= LATEX_MAX ? '#ffc107' : '#b2e0e4'}`,
|
}}>
|
||||||
textAlign: 'center',
|
<div style={{ fontSize: '1.3rem', fontWeight: 700, color: totalLatex >= LATEX_MAX ? '#856404' : '#11b3be' }}>
|
||||||
}}>
|
{totalLatex}/{LATEX_MAX}
|
||||||
<div style={{ fontSize: '1.3rem', fontWeight: 700, color: totalLatex >= LATEX_MAX ? '#856404' : '#11b3be' }}>
|
</div>
|
||||||
{totalLatex}/{LATEX_MAX}
|
<div style={{ fontSize: '0.75rem', color: '#555' }}>Latex balloons</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '0.75rem', color: '#555' }}>Latex balloons</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="has-text-grey has-text-centered" style={{ padding: '2rem' }}>Loading…</p>
|
<p className="has-text-grey has-text-centered" style={{ padding: '2rem' }}>Loading…</p>
|
||||||
) : buildItems.length === 0 ? (
|
|
||||||
<div style={{ padding: '1.5rem', fontSize: '0.82rem', color: '#555' }}>
|
|
||||||
<p style={{ marginBottom: '0.5rem' }}>No items found with category slug <code>build</code>.</p>
|
|
||||||
<p style={{ marginBottom: '0.5rem' }}>Category slugs currently in your catalog:</p>
|
|
||||||
<p style={{ fontFamily: 'monospace', background: '#f5f5f5', padding: '0.5rem', borderRadius: 4, wordBreak: 'break-all' }}>
|
|
||||||
{debugSlugs.join(', ') || '(none — catalog may be empty or still loading)'}
|
|
||||||
</p>
|
|
||||||
<p style={{ marginTop: '0.5rem', color: '#888' }}>
|
|
||||||
The filter looks for <code>build</code>. If your Square category produces a different slug, share this list and it will be fixed.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* ── Mylar section ── */}
|
{/* ── Mylar section ── */}
|
||||||
{mylarItems.length > 0 && (
|
{mylarItems.length === 0 ? (
|
||||||
|
<p style={{ fontSize: '0.82rem', color: '#888', marginBottom: '1.25rem' }}>
|
||||||
|
No mylar items found. Add a “Build” category to items in Square.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
<div style={{ marginBottom: '1.5rem' }}>
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
<p className="label" style={{ marginBottom: '0.75rem' }}>
|
<p className="label" style={{ marginBottom: '0.75rem' }}>
|
||||||
Mylar Balloons
|
Mylar Balloons
|
||||||
<span style={{ fontWeight: 400, color: '#888', marginLeft: 6 }}>
|
<span style={{ fontWeight: 400, color: '#888', marginLeft: 6 }}>(up to {MYLAR_MAX} total)</span>
|
||||||
(up to {MYLAR_MAX} total)
|
|
||||||
</span>
|
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: '0.75rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: '0.75rem' }}>
|
||||||
{mylarItems.map((item) => {
|
{mylarItems.map((item) => {
|
||||||
@ -223,19 +237,14 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id} style={{
|
<div key={item.id} style={{
|
||||||
border: '1px solid #e6dfc8',
|
border: '1px solid #e6dfc8', borderRadius: 10, padding: '0.65rem',
|
||||||
borderRadius: 10,
|
|
||||||
padding: '0.65rem',
|
|
||||||
background: qty > 0 ? '#f0f9fa' : '#fafaf8',
|
background: qty > 0 ? '#f0f9fa' : '#fafaf8',
|
||||||
display: 'flex', flexDirection: 'column', gap: '0.4rem',
|
display: 'flex', flexDirection: 'column', gap: '0.4rem',
|
||||||
}}>
|
}}>
|
||||||
{item.imageUrl && (
|
{item.imageUrl && (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img src={item.imageUrl} alt={item.name}
|
||||||
src={item.imageUrl}
|
style={{ width: '100%', height: 80, objectFit: 'contain', borderRadius: 6 }} />
|
||||||
alt={item.name}
|
|
||||||
style={{ width: '100%', height: 80, objectFit: 'contain', borderRadius: 6 }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<div style={{ fontSize: '0.82rem', fontWeight: 600, lineHeight: 1.3 }}>{item.name}</div>
|
<div style={{ fontSize: '0.82rem', fontWeight: 600, lineHeight: 1.3 }}>{item.name}</div>
|
||||||
{itemPrice > 0 && (
|
{itemPrice > 0 && (
|
||||||
@ -246,56 +255,66 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
{selectableVars.length > 1 && (
|
{selectableVars.length > 1 && (
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||||
{selectableVars.map((v) => (
|
{selectableVars.map((v) => (
|
||||||
<button
|
<button key={v.id} type="button" onClick={() => setVariation(item.id, v.id)}
|
||||||
key={v.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setVariation(item.id, v.id)}
|
|
||||||
style={{
|
style={{
|
||||||
fontSize: '0.72rem', padding: '2px 6px', borderRadius: 4,
|
fontSize: '0.72rem', padding: '2px 6px', borderRadius: 4, border: '1px solid',
|
||||||
border: '1px solid',
|
|
||||||
borderColor: s?.variationId === v.id ? '#11b3be' : '#ccc',
|
borderColor: s?.variationId === v.id ? '#11b3be' : '#ccc',
|
||||||
background: s?.variationId === v.id ? '#11b3be' : '#fff',
|
background: s?.variationId === v.id ? '#11b3be' : '#fff',
|
||||||
color: s?.variationId === v.id ? '#fff' : '#333',
|
color: s?.variationId === v.id ? '#fff' : '#333', cursor: 'pointer',
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
}}
|
||||||
>
|
>{v.name}</button>
|
||||||
{v.name}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Quantity stepper */}
|
{/* Quantity stepper */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 'auto' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 'auto' }}>
|
||||||
<button
|
<button type="button" onClick={() => adjustMylar(item.id, -1)} disabled={qty === 0}
|
||||||
type="button"
|
|
||||||
onClick={() => adjustMylar(item.id, -1)}
|
|
||||||
disabled={qty === 0}
|
|
||||||
style={{
|
style={{
|
||||||
width: 26, height: 26, borderRadius: '50%',
|
width: 26, height: 26, borderRadius: '50%', border: '1px solid #ccc',
|
||||||
border: '1px solid #ccc', background: '#f5f5f5',
|
background: '#f5f5f5', cursor: qty === 0 ? 'default' : 'pointer',
|
||||||
cursor: qty === 0 ? 'default' : 'pointer',
|
|
||||||
fontSize: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
fontSize: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
}}
|
}}>−</button>
|
||||||
>−</button>
|
<span style={{ fontSize: '0.9rem', minWidth: 18, textAlign: 'center', fontWeight: 600 }}>{qty}</span>
|
||||||
<span style={{ fontSize: '0.9rem', minWidth: 18, textAlign: 'center', fontWeight: 600 }}>
|
<button type="button" onClick={() => adjustMylar(item.id, 1)} disabled={totalMylars >= MYLAR_MAX}
|
||||||
{qty}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => adjustMylar(item.id, 1)}
|
|
||||||
disabled={totalMylars >= MYLAR_MAX}
|
|
||||||
style={{
|
style={{
|
||||||
width: 26, height: 26, borderRadius: '50%',
|
width: 26, height: 26, borderRadius: '50%', border: '1px solid',
|
||||||
border: '1px solid',
|
|
||||||
borderColor: totalMylars >= MYLAR_MAX ? '#ccc' : '#11b3be',
|
borderColor: totalMylars >= MYLAR_MAX ? '#ccc' : '#11b3be',
|
||||||
background: totalMylars >= MYLAR_MAX ? '#f5f5f5' : '#11b3be',
|
background: totalMylars >= MYLAR_MAX ? '#f5f5f5' : '#11b3be',
|
||||||
color: totalMylars >= MYLAR_MAX ? '#aaa' : '#fff',
|
color: totalMylars >= MYLAR_MAX ? '#aaa' : '#fff',
|
||||||
cursor: totalMylars >= MYLAR_MAX ? 'default' : 'pointer',
|
cursor: totalMylars >= MYLAR_MAX ? 'default' : 'pointer',
|
||||||
fontSize: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
fontSize: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
}}
|
}}>+</button>
|
||||||
>+</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Modifiers — shown when item is selected */}
|
||||||
|
{qty > 0 && item.modifiers.length > 0 && (
|
||||||
|
<div style={{ marginTop: '0.25rem', borderTop: '1px solid #e6dfc8', paddingTop: '0.35rem' }}>
|
||||||
|
{item.modifiers.map((ml) => (
|
||||||
|
<div key={ml.id} style={{ marginBottom: '0.2rem' }}>
|
||||||
|
<div style={{ fontSize: '0.7rem', color: '#555', marginBottom: 3 }}>{ml.name}</div>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
|
||||||
|
{ml.options.map((opt) => {
|
||||||
|
const chosen = (modChoices[item.id]?.[ml.id] ?? []).includes(opt.id)
|
||||||
|
return (
|
||||||
|
<button key={opt.id} type="button"
|
||||||
|
onClick={() => toggleMod(item.id, ml.id, opt.id, ml.selectionType === 'MULTIPLE')}
|
||||||
|
style={{
|
||||||
|
fontSize: '0.68rem', padding: '2px 5px', borderRadius: 3, border: '1px solid',
|
||||||
|
borderColor: chosen ? '#11b3be' : '#ccc',
|
||||||
|
background: chosen ? '#11b3be' : '#fff',
|
||||||
|
color: chosen ? '#fff' : '#333', cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{opt.name}{opt.priceDelta ? ` +${fmt(opt.priceDelta)}` : ''}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -312,15 +331,13 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
(up to {LATEX_MAX} total — tap a color to add)
|
(up to {LATEX_MAX} total — tap a color to add)
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{latexItems.map((item) => {
|
{latexItems.map((item) => {
|
||||||
const selected = latexColors[item.id] ?? []
|
const selected = latexColors[item.id] ?? []
|
||||||
return (
|
return (
|
||||||
<div key={item.id} style={{ marginBottom: '1rem' }}>
|
<div key={item.id} style={{ marginBottom: '1rem' }}>
|
||||||
{latexItems.length > 1 && (
|
{latexItems.length > 1 && (
|
||||||
<p style={{ fontSize: '0.82rem', color: '#555', marginBottom: '0.4rem' }}>
|
<p style={{ fontSize: '0.82rem', color: '#555', marginBottom: '0.4rem' }}>
|
||||||
{item.name}
|
{item.name}{item.price ? ` · ${fmt(item.price)} each` : ''}
|
||||||
{item.price ? ` · ${fmt(item.price)} each` : ''}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{selected.length > 0 && (
|
{selected.length > 0 && (
|
||||||
@ -333,27 +350,18 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
const isSelected = selected.includes(name)
|
const isSelected = selected.includes(name)
|
||||||
const atCap = totalLatex >= LATEX_MAX
|
const atCap = totalLatex >= LATEX_MAX
|
||||||
return (
|
return (
|
||||||
<button
|
<button key={name} type="button" title={name}
|
||||||
key={name}
|
|
||||||
type="button"
|
|
||||||
title={name}
|
|
||||||
onClick={() => toggleColor(item.id, name)}
|
onClick={() => toggleColor(item.id, name)}
|
||||||
disabled={!isSelected && atCap}
|
disabled={!isSelected && atCap}
|
||||||
|
aria-pressed={isSelected} aria-label={name}
|
||||||
style={{
|
style={{
|
||||||
width: 26, height: 26,
|
width: 26, height: 26, borderRadius: '50%', background: hex,
|
||||||
borderRadius: '50%',
|
|
||||||
background: hex,
|
|
||||||
border: isSelected ? '3px solid #11b3be' : '2px solid rgba(0,0,0,0.15)',
|
border: isSelected ? '3px solid #11b3be' : '2px solid rgba(0,0,0,0.15)',
|
||||||
cursor: (!isSelected && atCap) ? 'not-allowed' : 'pointer',
|
cursor: (!isSelected && atCap) ? 'not-allowed' : 'pointer',
|
||||||
opacity: (!isSelected && atCap) ? 0.35 : 1,
|
opacity: (!isSelected && atCap) ? 0.35 : 1,
|
||||||
outline: isSelected ? '2px solid #fff' : 'none',
|
outline: isSelected ? '2px solid #fff' : 'none', outlineOffset: -5,
|
||||||
outlineOffset: -5,
|
flexShrink: 0, boxShadow: isSelected ? '0 0 0 1px #11b3be' : 'none',
|
||||||
position: 'relative',
|
|
||||||
flexShrink: 0,
|
|
||||||
boxShadow: isSelected ? '0 0 0 1px #11b3be' : 'none',
|
|
||||||
}}
|
}}
|
||||||
aria-pressed={isSelected}
|
|
||||||
aria-label={name}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -368,16 +376,12 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer className="modal-card-foot" style={{ gap: '0.75rem', flexWrap: 'wrap' }}>
|
<footer className="modal-card-foot" style={{ gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||||
{!canAdd && !loading && buildItems.length > 0 && (
|
{!canAdd && !loading && (
|
||||||
<p className="is-size-7 has-text-grey" style={{ width: '100%' }}>
|
<p className="is-size-7 has-text-grey" style={{ width: '100%' }}>
|
||||||
Add at least one balloon to continue.
|
Add at least one balloon to continue.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<button
|
<button className="button is-info" disabled={!canAdd} onClick={handleAdd}>
|
||||||
className="button is-info"
|
|
||||||
disabled={!canAdd}
|
|
||||||
onClick={handleAdd}
|
|
||||||
>
|
|
||||||
{canAdd
|
{canAdd
|
||||||
? `Add Bouquet to Order${totalCents > 0 ? ` · ${fmt(totalCents)}` : ''}`
|
? `Add Bouquet to Order${totalCents > 0 ? ` · ${fmt(totalCents)}` : ''}`
|
||||||
: 'Add Bouquet to Order'}
|
: 'Add Bouquet to Order'}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user