BouquetPicker: fix latex filter, unified color picker, modifier visibility
- Latex items now filtered to categories.includes('latex') — excludes
garlands, arches, and other non-balloon showColors items
- Latex UI replaced with collapsible color families (same as ColorPicker),
chip palette preview, and per-color remove buttons
- Multiple latex sizes show a size selector tab bar
- Modifiers always rendered on mylar cards (dimmed at qty=0, active when >0)
so Helium Weight and other options are always discoverable
- Added console.log for mylar modifier debug output on load
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c9bd3e50d0
commit
3977580b81
@ -9,9 +9,15 @@ import { fmt } from '@/lib/format'
|
|||||||
const MYLAR_MAX = 6
|
const MYLAR_MAX = 6
|
||||||
const LATEX_MAX = 6
|
const LATEX_MAX = 6
|
||||||
|
|
||||||
|
interface ColorEntry {
|
||||||
|
name: string
|
||||||
|
hex: string
|
||||||
|
image?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface ColorFamily {
|
interface ColorFamily {
|
||||||
family: string
|
family: string
|
||||||
colors: { name: string; hex: string }[]
|
colors: ColorEntry[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -29,22 +35,23 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
|
|
||||||
// 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
|
// modifiers per mylar item: itemId → listId → optionIds[]
|
||||||
const [latexColors, setLatexColors] = useState<Record<string, string[]>>({})
|
|
||||||
// modifiers: itemId → listId → selected optionIds
|
|
||||||
const [modChoices, setModChoices] = useState<Record<string, Record<string, string[]>>>({})
|
const [modChoices, setModChoices] = useState<Record<string, Record<string, string[]>>>({})
|
||||||
|
|
||||||
|
// latex: flat list of chosen color names (each = 1 balloon)
|
||||||
|
const [latexSelections, setLatexSelections] = useState<string[]>([])
|
||||||
|
const [latexItemIdx, setLatexItemIdx] = useState(0)
|
||||||
|
const [openFamily, setOpenFamily] = useState<string | null>(null)
|
||||||
|
|
||||||
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),
|
||||||
[mylarState],
|
[mylarState],
|
||||||
)
|
)
|
||||||
const totalLatex = useMemo(
|
const totalLatex = latexSelections.length
|
||||||
() => Object.values(latexColors).reduce((sum, c) => sum + c.length, 0),
|
|
||||||
[latexColors],
|
|
||||||
)
|
|
||||||
|
|
||||||
const canAdd = totalMylars > 0 || totalLatex > 0
|
const canAdd = totalMylars > 0 || totalLatex > 0
|
||||||
|
|
||||||
|
const allColors = useMemo(() => colorFamilies.flatMap((f) => f.colors), [colorFamilies])
|
||||||
|
|
||||||
const totalCents = useMemo(() => {
|
const totalCents = useMemo(() => {
|
||||||
let total = 0
|
let total = 0
|
||||||
mylarItems.forEach((item) => {
|
mylarItems.forEach((item) => {
|
||||||
@ -59,23 +66,29 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
}, 0)
|
}, 0)
|
||||||
total += (base + modDelta) * s.quantity
|
total += (base + modDelta) * s.quantity
|
||||||
})
|
})
|
||||||
latexItems.forEach((item) => {
|
const latexItem = latexItems[latexItemIdx]
|
||||||
const colors = latexColors[item.id] ?? []
|
if (latexItem && totalLatex > 0) total += (latexItem.price ?? 0) * totalLatex
|
||||||
total += (item.price ?? 0) * colors.length
|
|
||||||
})
|
|
||||||
return total
|
return total
|
||||||
}, [mylarItems, latexItems, mylarState, latexColors, modChoices])
|
}, [mylarItems, latexItems, latexItemIdx, mylarState, modChoices, totalLatex])
|
||||||
|
|
||||||
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 + '/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(([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 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)
|
setMylarItems(mylars)
|
||||||
setLatexItems(latex)
|
setLatexItems(latex)
|
||||||
@ -115,14 +128,13 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleColor = (itemId: string, colorName: string) => {
|
const addLatexColor = (name: string) => {
|
||||||
const current = latexColors[itemId] ?? []
|
|
||||||
if (current.includes(colorName)) {
|
|
||||||
setLatexColors((prev) => ({ ...prev, [itemId]: current.filter((c) => c !== colorName) }))
|
|
||||||
} else {
|
|
||||||
if (totalLatex >= LATEX_MAX) return
|
if (totalLatex >= LATEX_MAX) return
|
||||||
setLatexColors((prev) => ({ ...prev, [itemId]: [...current, colorName] }))
|
setLatexSelections((prev) => [...prev, name])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removeLatexColor = (idx: number) => {
|
||||||
|
setLatexSelections((prev) => prev.filter((_, i) => i !== idx))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
@ -143,36 +155,29 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
latexItems.forEach((item) => {
|
const latexItem = latexItems[latexItemIdx]
|
||||||
const colors = latexColors[item.id] ?? []
|
if (latexItem && latexSelections.length > 0) {
|
||||||
if (colors.length === 0) return
|
|
||||||
addToCart({
|
addToCart({
|
||||||
product: item,
|
product: latexItem,
|
||||||
quantity: colors.length,
|
quantity: latexSelections.length,
|
||||||
selectedColors: colors,
|
selectedColors: latexSelections,
|
||||||
modifierChoices: {},
|
modifierChoices: {},
|
||||||
notes: '',
|
notes: '',
|
||||||
bouquetGroupId,
|
bouquetGroupId,
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const allColors = useMemo(
|
|
||||||
() => colorFamilies.flatMap((f) => f.colors),
|
|
||||||
[colorFamilies],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal is-active">
|
<div className="modal is-active">
|
||||||
<div className="modal-background" onClick={onClose} />
|
<div className="modal-background" onClick={onClose} />
|
||||||
<div className="modal-card" style={{ maxWidth: 560, width: '95vw', maxHeight: '90vh' }}>
|
<div className="modal-card" style={{ maxWidth: 580, width: '95vw', maxHeight: '90vh' }}>
|
||||||
|
|
||||||
<header className="modal-card-head" style={{ background: '#11b3be' }}>
|
<header className="modal-card-head" style={{ background: '#11b3be' }}>
|
||||||
<p className="modal-card-title" style={{ color: '#fff', fontSize: '1.1rem' }}>
|
<p className="modal-card-title" style={{ color: '#fff', fontSize: '1.1rem' }}>
|
||||||
Build Your Bouquet
|
Build Your Bouquet
|
||||||
{product.name !== 'Build Your Own Bouquet' && ` — ${product.name}`}
|
|
||||||
</p>
|
</p>
|
||||||
<button className="delete" aria-label="close" onClick={onClose} />
|
<button className="delete" aria-label="close" onClick={onClose} />
|
||||||
</header>
|
</header>
|
||||||
@ -185,28 +190,22 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
background: '#fff', paddingBottom: '0.75rem', marginBottom: '0.5rem',
|
background: '#fff', paddingBottom: '0.75rem', marginBottom: '0.5rem',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
<div style={{
|
{[
|
||||||
|
{ label: 'Mylar balloons', count: totalMylars, max: MYLAR_MAX },
|
||||||
|
{ label: 'Latex balloons', count: totalLatex, max: LATEX_MAX },
|
||||||
|
].map(({ label, count, max }) => (
|
||||||
|
<div key={label} style={{
|
||||||
flex: 1, padding: '0.6rem 1rem', borderRadius: 8,
|
flex: 1, padding: '0.6rem 1rem', borderRadius: 8,
|
||||||
background: totalMylars >= MYLAR_MAX ? '#fff3cd' : '#f0f9fa',
|
background: count >= max ? '#fff3cd' : '#f0f9fa',
|
||||||
border: `1px solid ${totalMylars >= MYLAR_MAX ? '#ffc107' : '#b2e0e4'}`,
|
border: `1px solid ${count >= max ? '#ffc107' : '#b2e0e4'}`,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: '1.3rem', fontWeight: 700, color: totalMylars >= MYLAR_MAX ? '#856404' : '#11b3be' }}>
|
<div style={{ fontSize: '1.3rem', fontWeight: 700, color: count >= max ? '#856404' : '#11b3be' }}>
|
||||||
{totalMylars}/{MYLAR_MAX}
|
{count}/{max}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '0.75rem', color: '#555' }}>Mylar balloons</div>
|
<div style={{ fontSize: '0.75rem', color: '#555' }}>{label}</div>
|
||||||
</div>
|
|
||||||
<div style={{
|
|
||||||
flex: 1, padding: '0.6rem 1rem', borderRadius: 8,
|
|
||||||
background: totalLatex >= LATEX_MAX ? '#fff3cd' : '#f0f9fa',
|
|
||||||
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>
|
|
||||||
<div style={{ fontSize: '0.75rem', color: '#555' }}>Latex balloons</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -215,17 +214,13 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* ── 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 }}>(up to {MYLAR_MAX} total)</span>
|
<span style={{ fontWeight: 400, color: '#888', marginLeft: 6 }}>(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(155px, 1fr))', gap: '0.75rem' }}>
|
||||||
{mylarItems.map((item) => {
|
{mylarItems.map((item) => {
|
||||||
const s = mylarState[item.id]
|
const s = mylarState[item.id]
|
||||||
const qty = s?.quantity ?? 0
|
const qty = s?.quantity ?? 0
|
||||||
@ -237,7 +232,8 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id} style={{
|
<div key={item.id} style={{
|
||||||
border: '1px solid #e6dfc8', borderRadius: 10, padding: '0.65rem',
|
border: `1px solid ${qty > 0 ? '#11b3be' : '#e6dfc8'}`,
|
||||||
|
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',
|
||||||
}}>
|
}}>
|
||||||
@ -260,7 +256,8 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
fontSize: '0.72rem', padding: '2px 6px', borderRadius: 4, border: '1px solid',
|
fontSize: '0.72rem', padding: '2px 6px', borderRadius: 4, 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', cursor: 'pointer',
|
color: s?.variationId === v.id ? '#fff' : '#333',
|
||||||
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
>{v.name}</button>
|
>{v.name}</button>
|
||||||
))}
|
))}
|
||||||
@ -287,12 +284,19 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
}}>+</button>
|
}}>+</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modifiers — shown when item is selected */}
|
{/* Modifiers — always visible, dimmed when qty = 0 */}
|
||||||
{qty > 0 && item.modifiers.length > 0 && (
|
{item.modifiers.length > 0 && (
|
||||||
<div style={{ marginTop: '0.25rem', borderTop: '1px solid #e6dfc8', paddingTop: '0.35rem' }}>
|
<div style={{
|
||||||
|
marginTop: '0.25rem', borderTop: '1px solid #e6dfc8', paddingTop: '0.35rem',
|
||||||
|
opacity: qty === 0 ? 0.45 : 1,
|
||||||
|
pointerEvents: qty === 0 ? 'none' : undefined,
|
||||||
|
}}>
|
||||||
{item.modifiers.map((ml) => (
|
{item.modifiers.map((ml) => (
|
||||||
<div key={ml.id} style={{ marginBottom: '0.2rem' }}>
|
<div key={ml.id} style={{ marginBottom: '0.3rem' }}>
|
||||||
<div style={{ fontSize: '0.7rem', color: '#555', marginBottom: 3 }}>{ml.name}</div>
|
<div style={{ fontSize: '0.7rem', color: '#555', marginBottom: 3, fontWeight: 600 }}>
|
||||||
|
{ml.name}
|
||||||
|
{ml.minSelected > 0 && <span style={{ color: '#c07000', marginLeft: 4 }}>*</span>}
|
||||||
|
</div>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
|
||||||
{ml.options.map((opt) => {
|
{ml.options.map((opt) => {
|
||||||
const chosen = (modChoices[item.id]?.[ml.id] ?? []).includes(opt.id)
|
const chosen = (modChoices[item.id]?.[ml.id] ?? []).includes(opt.id)
|
||||||
@ -300,10 +304,11 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
<button key={opt.id} type="button"
|
<button key={opt.id} type="button"
|
||||||
onClick={() => toggleMod(item.id, ml.id, opt.id, ml.selectionType === 'MULTIPLE')}
|
onClick={() => toggleMod(item.id, ml.id, opt.id, ml.selectionType === 'MULTIPLE')}
|
||||||
style={{
|
style={{
|
||||||
fontSize: '0.68rem', padding: '2px 5px', borderRadius: 3, border: '1px solid',
|
fontSize: '0.68rem', padding: '2px 6px', borderRadius: 3, border: '1px solid',
|
||||||
borderColor: chosen ? '#11b3be' : '#ccc',
|
borderColor: chosen ? '#11b3be' : '#ccc',
|
||||||
background: chosen ? '#11b3be' : '#fff',
|
background: chosen ? '#11b3be' : '#fff',
|
||||||
color: chosen ? '#fff' : '#333', cursor: 'pointer',
|
color: chosen ? '#fff' : '#333',
|
||||||
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{opt.name}{opt.priceDelta ? ` +${fmt(opt.priceDelta)}` : ''}
|
{opt.name}{opt.priceDelta ? ` +${fmt(opt.priceDelta)}` : ''}
|
||||||
@ -313,6 +318,11 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{qty === 0 && (
|
||||||
|
<p style={{ fontSize: '0.68rem', color: '#888', marginTop: 2 }}>
|
||||||
|
Add to select options
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -323,54 +333,161 @@ export default function BouquetPicker({ product, onClose }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Latex section ── */}
|
{/* ── Latex section ── */}
|
||||||
{latexItems.length > 0 && allColors.length > 0 && (
|
|
||||||
<div>
|
<div>
|
||||||
<p className="label" style={{ marginBottom: '0.5rem' }}>
|
<p className="label" style={{ marginBottom: '0.4rem' }}>
|
||||||
Latex Balloons
|
Latex Balloons
|
||||||
<span style={{ fontWeight: 400, color: '#888', marginLeft: 6 }}>
|
<span style={{ fontWeight: 400, color: '#888', marginLeft: 6 }}>(up to {LATEX_MAX} total)</span>
|
||||||
(up to {LATEX_MAX} total — tap a color to add)
|
|
||||||
</span>
|
|
||||||
</p>
|
</p>
|
||||||
{latexItems.map((item) => {
|
|
||||||
const selected = latexColors[item.id] ?? []
|
{/* Size selector when there are multiple latex items */}
|
||||||
return (
|
|
||||||
<div key={item.id} style={{ marginBottom: '1rem' }}>
|
|
||||||
{latexItems.length > 1 && (
|
{latexItems.length > 1 && (
|
||||||
<p style={{ fontSize: '0.82rem', color: '#555', marginBottom: '0.4rem' }}>
|
<div style={{ display: 'flex', gap: 6, marginBottom: '0.6rem', flexWrap: 'wrap' }}>
|
||||||
{item.name}{item.price ? ` · ${fmt(item.price)} each` : ''}
|
{latexItems.map((item, idx) => (
|
||||||
</p>
|
<button key={item.id} type="button" onClick={() => setLatexItemIdx(idx)}
|
||||||
)}
|
|
||||||
{selected.length > 0 && (
|
|
||||||
<p style={{ fontSize: '0.78rem', color: '#11b3be', marginBottom: '0.4rem', fontWeight: 600 }}>
|
|
||||||
Selected: {selected.join(', ')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
|
||||||
{allColors.map(({ name, hex }) => {
|
|
||||||
const isSelected = selected.includes(name)
|
|
||||||
const atCap = totalLatex >= LATEX_MAX
|
|
||||||
return (
|
|
||||||
<button key={name} type="button" title={name}
|
|
||||||
onClick={() => toggleColor(item.id, name)}
|
|
||||||
disabled={!isSelected && atCap}
|
|
||||||
aria-pressed={isSelected} aria-label={name}
|
|
||||||
style={{
|
style={{
|
||||||
width: 26, height: 26, borderRadius: '50%', background: hex,
|
fontSize: '0.78rem', padding: '3px 10px', borderRadius: 4, border: '1px solid',
|
||||||
|
borderColor: latexItemIdx === idx ? '#11b3be' : '#ccc',
|
||||||
|
background: latexItemIdx === idx ? '#11b3be' : '#fff',
|
||||||
|
color: latexItemIdx === idx ? '#fff' : '#333',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name}{item.price ? ` · ${fmt(item.price)}` : ''}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{latexItems.length === 0 && (
|
||||||
|
<p style={{ fontSize: '0.82rem', color: '#888', marginBottom: '1rem' }}>
|
||||||
|
No latex balloon items found in the catalog.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Selected palette chips */}
|
||||||
|
{latexSelections.length > 0 && (
|
||||||
|
<div style={{
|
||||||
|
background: '#f0f9fa', border: '1px solid #b2e0e4',
|
||||||
|
borderRadius: 10, padding: '0.65rem 0.9rem', marginBottom: '0.75rem',
|
||||||
|
}}>
|
||||||
|
<p style={{ fontSize: '0.78rem', fontWeight: 700, color: '#15384c', marginBottom: '0.4rem' }}>
|
||||||
|
Your selection ({latexSelections.length}/{LATEX_MAX})
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||||
|
{latexSelections.map((name, idx) => {
|
||||||
|
const c = allColors.find((col) => col.name === name)
|
||||||
|
return (
|
||||||
|
<button key={idx} type="button" title={`Remove ${name}`}
|
||||||
|
onClick={() => removeLatexColor(idx)}
|
||||||
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 5,
|
||||||
|
background: '#fff', border: '1px solid #cce8eb',
|
||||||
|
borderRadius: 999, padding: '3px 10px 3px 5px',
|
||||||
|
cursor: 'pointer', fontSize: '0.75rem', color: '#334854',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{
|
||||||
|
width: 16, height: 16, borderRadius: '50%', flexShrink: 0,
|
||||||
|
background: c?.image ? `url('/color/${c.image}') center/cover` : (c?.hex ?? '#eee'),
|
||||||
|
border: '1px solid rgba(0,0,0,0.1)', display: 'inline-block',
|
||||||
|
}} />
|
||||||
|
{name}
|
||||||
|
<span style={{ color: '#aaa', marginLeft: 2 }}>×</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Collapsible color family picker */}
|
||||||
|
{latexItems.length > 0 && totalLatex < LATEX_MAX && colorFamilies.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="is-size-7 has-text-grey" style={{ marginBottom: '0.5rem' }}>
|
||||||
|
Tap a family to browse, then tap a color to add it.
|
||||||
|
</p>
|
||||||
|
{colorFamilies.map((family) => {
|
||||||
|
const isOpen = openFamily === family.family
|
||||||
|
const familyChosen = family.colors.filter((c) => latexSelections.includes(c.name)).length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={family.family} style={{ marginBottom: 6 }}>
|
||||||
|
<button type="button"
|
||||||
|
onClick={() => setOpenFamily(isOpen ? null : family.family)}
|
||||||
|
style={{
|
||||||
|
width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||||
|
background: isOpen ? '#f0fafb' : '#fafafa',
|
||||||
|
border: `1px solid ${isOpen ? '#b2e0e4' : '#e8e8e8'}`,
|
||||||
|
borderRadius: isOpen ? '10px 10px 0 0' : '10px',
|
||||||
|
padding: '0.55rem 0.9rem', cursor: 'pointer', textAlign: 'left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
|
||||||
|
<span style={{ display: 'flex', gap: 3, flexShrink: 0 }}>
|
||||||
|
{family.colors.slice(0, 7).map((c) => (
|
||||||
|
<span key={c.name} style={{
|
||||||
|
width: 14, height: 14, borderRadius: '50%', display: 'inline-block', flexShrink: 0,
|
||||||
|
background: c.image ? `url('/color/${c.image}') center/cover` : c.hex,
|
||||||
|
border: '1px solid rgba(0,0,0,0.12)',
|
||||||
|
}} />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: '0.85rem', fontWeight: 600 }}>{family.family}</span>
|
||||||
|
{familyChosen > 0 && (
|
||||||
|
<span style={{
|
||||||
|
fontSize: '0.7rem', background: '#11b3be', color: '#fff',
|
||||||
|
borderRadius: 999, padding: '1px 7px',
|
||||||
|
}}>{familyChosen}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: '0.75rem', color: '#aaa' }}>{isOpen ? '▲' : '▼'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div style={{
|
||||||
|
border: '1px solid #b2e0e4', borderTop: 'none',
|
||||||
|
borderRadius: '0 0 10px 10px', padding: '0.65rem 0.9rem',
|
||||||
|
display: 'flex', flexWrap: 'wrap', gap: 8,
|
||||||
|
}}>
|
||||||
|
{family.colors.map((c) => {
|
||||||
|
const isSelected = latexSelections.includes(c.name)
|
||||||
|
return (
|
||||||
|
<button key={c.name} type="button" title={c.name}
|
||||||
|
onClick={() => {
|
||||||
|
if (isSelected) {
|
||||||
|
setLatexSelections((prev) => {
|
||||||
|
const i = prev.lastIndexOf(c.name)
|
||||||
|
return prev.filter((_, j) => j !== i)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
addLatexColor(c.name)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: 30, height: 30, borderRadius: '50%', cursor: 'pointer',
|
||||||
|
background: c.image ? `url('/color/${c.image}') center/cover` : c.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',
|
|
||||||
opacity: (!isSelected && atCap) ? 0.35 : 1,
|
|
||||||
outline: isSelected ? '2px solid #fff' : 'none', outlineOffset: -5,
|
outline: isSelected ? '2px solid #fff' : 'none', outlineOffset: -5,
|
||||||
flexShrink: 0, boxShadow: isSelected ? '0 0 0 1px #11b3be' : 'none',
|
boxShadow: isSelected ? '0 0 0 1px #11b3be' : 'none',
|
||||||
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{latexItems.length > 0 && totalLatex >= LATEX_MAX && (
|
||||||
|
<p className="is-size-7" style={{ color: '#856404', marginTop: '0.25rem' }}>
|
||||||
|
Maximum {LATEX_MAX} reached — remove a color above to change it.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user