Make vinyl an optional add-on with checkbox and additive pricing

- Vinyl is now opt-in via a checkbox (unchecked by default)
- Item's base price is preserved; vinyl cost is added on top
- Checkbox label explains it's lettering on a separate 18" foil balloon
- Price breakdown shows vinyl as an add-on, not a replacement
- Validation only requires text/font when the checkbox is checked
- Editing a vinyl cart entry pre-checks the checkbox

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chris 2026-04-29 23:14:42 -04:00
parent be7f98a347
commit 7d7d46af32

View File

@ -55,6 +55,7 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
// Vinyl state
const [vinylConfig, setVinylConfig] = useState<VinylConfig | null>(null)
const [wantsVinyl, setWantsVinyl] = useState(!!(editingEntry?.vinylText))
const [vinylText, setVinylText] = useState(editingEntry?.vinylText ?? '')
const [vinylFontId, setVinylFontId] = useState(editingEntry?.vinylFontId ?? '')
const [vinylShape, setVinylShape] = useState<VinylShape | null>(null)
@ -144,8 +145,8 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
const vinylPriceCents = product.vinylEnabled
? (vinylShape?.priceCents ?? 0) + vinylLetterCount * (vinylConfig?.pricePerLetterCents ?? 65)
: 0
const needsVinylText = product.vinylEnabled && vinylLetterCount === 0
const needsVinylFont = product.vinylEnabled && !vinylFontId
const needsVinylText = product.vinylEnabled && wantsVinyl && vinylLetterCount === 0
const needsVinylFont = product.vinylEnabled && wantsVinyl && !vinylFontId
const canAdd = missingModifiers.length === 0 && !needsColors && !needsVinylText && !needsVinylFont
@ -189,8 +190,9 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
}, 0)
const basePrice = activeVariation?.priceCents ?? product.price ?? 0
const chromeDelta = chromeCount * surchargePerColor
const unitPrice = product.vinylEnabled ? vinylPriceCents : basePrice + modDelta + chromeDelta
const total = unitPrice > 0 ? fmt(unitPrice * quantity) : product.vinylEnabled ? fmt(0) : 'Get Quote'
const vinylAddon = product.vinylEnabled && wantsVinyl ? vinylPriceCents : 0
const unitPrice = basePrice + modDelta + chromeDelta + vinylAddon
const total = unitPrice > 0 ? fmt(unitPrice * quantity) : 'Get Quote'
return (
<div className="modal is-active" onClick={onClose}>
@ -540,8 +542,41 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
{/* ── Vinyl configurator ── */}
{product.vinylEnabled && vinylConfig && (
<div style={{ marginTop: '1.5rem', padding: '1rem', background: '#f8f4ff', border: '1px solid #d8c8f8', borderRadius: '10px' }}>
<p className="label" style={{ marginBottom: '0.75rem', color: '#5a3e9e' }}>Custom Vinyl Text</p>
<div style={{ marginTop: '1.5rem', border: '1px solid #d8c8f8', borderRadius: '10px', overflow: 'hidden' }}>
{/* Checkbox toggle */}
<label style={{
display: 'flex', alignItems: 'flex-start', gap: '10px',
padding: '0.85rem 1rem', cursor: 'pointer',
background: wantsVinyl ? '#f3eeff' : '#faf8ff',
}}>
<input
type="checkbox"
checked={wantsVinyl}
onChange={(e) => {
setWantsVinyl(e.target.checked)
if (!e.target.checked) { setVinylText(''); setVinylFontId('') }
}}
style={{ marginTop: '3px', accentColor: '#7c4dff', flexShrink: 0 }}
/>
<div>
<span style={{ fontWeight: 600, color: '#4a2d9e', fontSize: '0.92rem' }}>
Add custom vinyl lettering
{vinylShape && (
<span style={{ fontWeight: 'normal', color: '#7c5cbf', fontSize: '0.82rem', marginLeft: '8px' }}>
from {fmt(vinylShape.priceCents + vinylConfig.pricePerLetterCents)}
</span>
)}
</span>
<p style={{ fontSize: '0.78rem', color: '#7c5cbf', marginTop: '2px', lineHeight: 1.4 }}>
Your message in custom vinyl lettering, applied to a separate 18&quot; foil balloon included with your order.
</p>
</div>
</label>
{/* Expanded configurator */}
{wantsVinyl && (
<div style={{ padding: '1rem', background: '#f8f4ff', borderTop: '1px solid #e8d8fc' }}>
{/* Shape picker */}
<div className="field" style={{ marginBottom: '1rem' }}>
@ -585,7 +620,7 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
</div>
{/* Font picker */}
<div className="field" style={{ marginBottom: '0.5rem' }}>
<div className="field" style={{ marginBottom: '0.75rem' }}>
<label className="label is-small">Font Style</label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: '8px' }}>
{vinylConfig.fonts.map((font) => {
@ -608,18 +643,23 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
</div>
</div>
{/* Live price summary */}
{vinylLetterCount > 0 && vinylShape && (
<div style={{ marginTop: '0.75rem', padding: '0.6rem 0.85rem', background: '#ede7ff', borderRadius: '8px', fontSize: '0.82rem', color: '#3d2080' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>18&quot; {vinylShape.name} balloon</span><span>{fmt(vinylShape.priceCents)}</span>
{/* Live price breakdown */}
{vinylShape && (
<div style={{ padding: '0.6rem 0.85rem', background: '#ede7ff', borderRadius: '8px', fontSize: '0.82rem', color: '#3d2080' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}>
<span>18&quot; {vinylShape.name} foil balloon</span><span>{fmt(vinylShape.priceCents)}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>Vinyl ({vinylLetterCount} letters)</span><span>{fmt(vinylLetterCount * vinylConfig.pricePerLetterCents)}</span>
<span>Vinyl lettering ({vinylLetterCount > 0 ? `${vinylLetterCount} letters` : 'per letter'})</span>
<span>{vinylLetterCount > 0 ? fmt(vinylLetterCount * vinylConfig.pricePerLetterCents) : `${fmt(vinylConfig.pricePerLetterCents)}/letter`}</span>
</div>
{vinylLetterCount > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 700, borderTop: '1px solid #c4b0f0', marginTop: '4px', paddingTop: '4px' }}>
<span>Total</span><span>{fmt(vinylPriceCents)}</span>
<span>Vinyl add-on total</span><span>+{fmt(vinylPriceCents)}</span>
</div>
)}
</div>
)}
</div>
)}
</div>
@ -689,7 +729,7 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
const selectedVariationId = activeVariation?.id
const storedVariationId = userVariation?.id ?? selectedVariationId
const selectedFont = vinylConfig?.fonts.find((f) => f.id === vinylFontId)
const vinylFields = product.vinylEnabled && vinylText && vinylShape ? {
const vinylFields = product.vinylEnabled && wantsVinyl && vinylText && vinylShape ? {
vinylText,
vinylFontId,
vinylFontName: selectedFont?.name,