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:
parent
be7f98a347
commit
7d7d46af32
@ -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,86 +542,124 @@ 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' }}>
|
||||
|
||||
{/* Shape picker */}
|
||||
<div className="field" style={{ marginBottom: '1rem' }}>
|
||||
<label className="label is-small">Balloon Shape</label>
|
||||
<div className="buttons">
|
||||
{vinylConfig.shapes.map((shape) => (
|
||||
<button key={shape.variationId} type="button"
|
||||
className={`button is-small${vinylShape?.variationId === shape.variationId ? ' is-info' : ''}`}
|
||||
onClick={() => setVinylShape(shape)}
|
||||
>
|
||||
{shape.name}
|
||||
<span style={{ marginLeft: 6, opacity: 0.7, fontSize: '0.78em' }}>{fmt(shape.priceCents)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text input */}
|
||||
<div className="field" style={{ marginBottom: '1rem' }}>
|
||||
<label className="label is-small">
|
||||
Your message
|
||||
<span style={{ fontWeight: 'normal', marginLeft: '8px', color: '#888' }}>
|
||||
{vinylLetterCount}/{vinylMaxChars} letters (spaces free)
|
||||
{/* 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>
|
||||
</label>
|
||||
<div className="control">
|
||||
<input className="input" type="text" placeholder="e.g. Happy Birthday!"
|
||||
value={vinylText}
|
||||
onChange={(e) => {
|
||||
const sanitized = e.target.value.replace(/[^\x20-\x7E]/g, '')
|
||||
const nonSpace = sanitized.replace(/ /g, '').length
|
||||
if (nonSpace <= vinylMaxChars) setVinylText(sanitized)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{vinylLetterCount > 0 && (
|
||||
<p className="is-size-7" style={{ marginTop: '0.25rem', color: '#5a3e9e' }}>
|
||||
{vinylLetterCount} letter{vinylLetterCount !== 1 ? 's' : ''} × {fmt(vinylConfig.pricePerLetterCents)} = {fmt(vinylLetterCount * vinylConfig.pricePerLetterCents)} vinyl
|
||||
<p style={{ fontSize: '0.78rem', color: '#7c5cbf', marginTop: '2px', lineHeight: 1.4 }}>
|
||||
Your message in custom vinyl lettering, applied to a separate 18" foil balloon included with your order.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Font picker */}
|
||||
<div className="field" style={{ marginBottom: '0.5rem' }}>
|
||||
<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) => {
|
||||
const chosen = vinylFontId === font.id
|
||||
return (
|
||||
<button key={font.id} type="button" onClick={() => setVinylFontId(font.id)}
|
||||
style={{
|
||||
padding: '10px 8px', border: `2px solid ${chosen ? '#7c4dff' : '#d8c8f8'}`,
|
||||
borderRadius: '8px', background: chosen ? '#ede7ff' : '#fff',
|
||||
cursor: 'pointer', textAlign: 'center', transition: 'border-color 0.15s, background 0.15s',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontFamily: `'${font.family}', sans-serif`, fontSize: '1.3rem', display: 'block', color: '#2d1b6b', lineHeight: 1.2 }}>
|
||||
{vinylText || 'Aa'}
|
||||
</span>
|
||||
<span style={{ fontSize: '0.68rem', color: '#888', marginTop: '4px', display: 'block' }}>{font.name}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* 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" {vinylShape.name} balloon</span><span>{fmt(vinylShape.priceCents)}</span>
|
||||
{/* Expanded configurator */}
|
||||
{wantsVinyl && (
|
||||
<div style={{ padding: '1rem', background: '#f8f4ff', borderTop: '1px solid #e8d8fc' }}>
|
||||
|
||||
{/* Shape picker */}
|
||||
<div className="field" style={{ marginBottom: '1rem' }}>
|
||||
<label className="label is-small">Balloon Shape</label>
|
||||
<div className="buttons">
|
||||
{vinylConfig.shapes.map((shape) => (
|
||||
<button key={shape.variationId} type="button"
|
||||
className={`button is-small${vinylShape?.variationId === shape.variationId ? ' is-info' : ''}`}
|
||||
onClick={() => setVinylShape(shape)}
|
||||
>
|
||||
{shape.name}
|
||||
<span style={{ marginLeft: 6, opacity: 0.7, fontSize: '0.78em' }}>{fmt(shape.priceCents)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>Vinyl ({vinylLetterCount} letters)</span><span>{fmt(vinylLetterCount * vinylConfig.pricePerLetterCents)}</span>
|
||||
|
||||
{/* Text input */}
|
||||
<div className="field" style={{ marginBottom: '1rem' }}>
|
||||
<label className="label is-small">
|
||||
Your message
|
||||
<span style={{ fontWeight: 'normal', marginLeft: '8px', color: '#888' }}>
|
||||
{vinylLetterCount}/{vinylMaxChars} letters (spaces free)
|
||||
</span>
|
||||
</label>
|
||||
<div className="control">
|
||||
<input className="input" type="text" placeholder="e.g. Happy Birthday!"
|
||||
value={vinylText}
|
||||
onChange={(e) => {
|
||||
const sanitized = e.target.value.replace(/[^\x20-\x7E]/g, '')
|
||||
const nonSpace = sanitized.replace(/ /g, '').length
|
||||
if (nonSpace <= vinylMaxChars) setVinylText(sanitized)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{vinylLetterCount > 0 && (
|
||||
<p className="is-size-7" style={{ marginTop: '0.25rem', color: '#5a3e9e' }}>
|
||||
{vinylLetterCount} letter{vinylLetterCount !== 1 ? 's' : ''} × {fmt(vinylConfig.pricePerLetterCents)} = {fmt(vinylLetterCount * vinylConfig.pricePerLetterCents)} vinyl
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 700, borderTop: '1px solid #c4b0f0', marginTop: '4px', paddingTop: '4px' }}>
|
||||
<span>Total</span><span>{fmt(vinylPriceCents)}</span>
|
||||
|
||||
{/* Font picker */}
|
||||
<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) => {
|
||||
const chosen = vinylFontId === font.id
|
||||
return (
|
||||
<button key={font.id} type="button" onClick={() => setVinylFontId(font.id)}
|
||||
style={{
|
||||
padding: '10px 8px', border: `2px solid ${chosen ? '#7c4dff' : '#d8c8f8'}`,
|
||||
borderRadius: '8px', background: chosen ? '#ede7ff' : '#fff',
|
||||
cursor: 'pointer', textAlign: 'center', transition: 'border-color 0.15s, background 0.15s',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontFamily: `'${font.family}', sans-serif`, fontSize: '1.3rem', display: 'block', color: '#2d1b6b', lineHeight: 1.2 }}>
|
||||
{vinylText || 'Aa'}
|
||||
</span>
|
||||
<span style={{ fontSize: '0.68rem', color: '#888', marginTop: '4px', display: 'block' }}>{font.name}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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" {vinylShape.name} foil balloon</span><span>{fmt(vinylShape.priceCents)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<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>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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user