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
|
// Vinyl state
|
||||||
const [vinylConfig, setVinylConfig] = useState<VinylConfig | null>(null)
|
const [vinylConfig, setVinylConfig] = useState<VinylConfig | null>(null)
|
||||||
|
const [wantsVinyl, setWantsVinyl] = useState(!!(editingEntry?.vinylText))
|
||||||
const [vinylText, setVinylText] = useState(editingEntry?.vinylText ?? '')
|
const [vinylText, setVinylText] = useState(editingEntry?.vinylText ?? '')
|
||||||
const [vinylFontId, setVinylFontId] = useState(editingEntry?.vinylFontId ?? '')
|
const [vinylFontId, setVinylFontId] = useState(editingEntry?.vinylFontId ?? '')
|
||||||
const [vinylShape, setVinylShape] = useState<VinylShape | null>(null)
|
const [vinylShape, setVinylShape] = useState<VinylShape | null>(null)
|
||||||
@ -144,8 +145,8 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
|
|||||||
const vinylPriceCents = product.vinylEnabled
|
const vinylPriceCents = product.vinylEnabled
|
||||||
? (vinylShape?.priceCents ?? 0) + vinylLetterCount * (vinylConfig?.pricePerLetterCents ?? 65)
|
? (vinylShape?.priceCents ?? 0) + vinylLetterCount * (vinylConfig?.pricePerLetterCents ?? 65)
|
||||||
: 0
|
: 0
|
||||||
const needsVinylText = product.vinylEnabled && vinylLetterCount === 0
|
const needsVinylText = product.vinylEnabled && wantsVinyl && vinylLetterCount === 0
|
||||||
const needsVinylFont = product.vinylEnabled && !vinylFontId
|
const needsVinylFont = product.vinylEnabled && wantsVinyl && !vinylFontId
|
||||||
|
|
||||||
const canAdd = missingModifiers.length === 0 && !needsColors && !needsVinylText && !needsVinylFont
|
const canAdd = missingModifiers.length === 0 && !needsColors && !needsVinylText && !needsVinylFont
|
||||||
|
|
||||||
@ -189,8 +190,9 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
|
|||||||
}, 0)
|
}, 0)
|
||||||
const basePrice = activeVariation?.priceCents ?? product.price ?? 0
|
const basePrice = activeVariation?.priceCents ?? product.price ?? 0
|
||||||
const chromeDelta = chromeCount * surchargePerColor
|
const chromeDelta = chromeCount * surchargePerColor
|
||||||
const unitPrice = product.vinylEnabled ? vinylPriceCents : basePrice + modDelta + chromeDelta
|
const vinylAddon = product.vinylEnabled && wantsVinyl ? vinylPriceCents : 0
|
||||||
const total = unitPrice > 0 ? fmt(unitPrice * quantity) : product.vinylEnabled ? fmt(0) : 'Get Quote'
|
const unitPrice = basePrice + modDelta + chromeDelta + vinylAddon
|
||||||
|
const total = unitPrice > 0 ? fmt(unitPrice * quantity) : 'Get Quote'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal is-active" onClick={onClose}>
|
<div className="modal is-active" onClick={onClose}>
|
||||||
@ -540,8 +542,41 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
|
|||||||
|
|
||||||
{/* ── Vinyl configurator ── */}
|
{/* ── Vinyl configurator ── */}
|
||||||
{product.vinylEnabled && vinylConfig && (
|
{product.vinylEnabled && vinylConfig && (
|
||||||
<div style={{ marginTop: '1.5rem', padding: '1rem', background: '#f8f4ff', border: '1px solid #d8c8f8', borderRadius: '10px' }}>
|
<div style={{ marginTop: '1.5rem', border: '1px solid #d8c8f8', borderRadius: '10px', overflow: 'hidden' }}>
|
||||||
<p className="label" style={{ marginBottom: '0.75rem', color: '#5a3e9e' }}>Custom Vinyl Text</p>
|
|
||||||
|
{/* 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" foil balloon included with your order.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Expanded configurator */}
|
||||||
|
{wantsVinyl && (
|
||||||
|
<div style={{ padding: '1rem', background: '#f8f4ff', borderTop: '1px solid #e8d8fc' }}>
|
||||||
|
|
||||||
{/* Shape picker */}
|
{/* Shape picker */}
|
||||||
<div className="field" style={{ marginBottom: '1rem' }}>
|
<div className="field" style={{ marginBottom: '1rem' }}>
|
||||||
@ -585,7 +620,7 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Font picker */}
|
{/* Font picker */}
|
||||||
<div className="field" style={{ marginBottom: '0.5rem' }}>
|
<div className="field" style={{ marginBottom: '0.75rem' }}>
|
||||||
<label className="label is-small">Font Style</label>
|
<label className="label is-small">Font Style</label>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: '8px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: '8px' }}>
|
||||||
{vinylConfig.fonts.map((font) => {
|
{vinylConfig.fonts.map((font) => {
|
||||||
@ -608,18 +643,23 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Live price summary */}
|
{/* Live price breakdown */}
|
||||||
{vinylLetterCount > 0 && vinylShape && (
|
{vinylShape && (
|
||||||
<div style={{ marginTop: '0.75rem', padding: '0.6rem 0.85rem', background: '#ede7ff', borderRadius: '8px', fontSize: '0.82rem', color: '#3d2080' }}>
|
<div style={{ padding: '0.6rem 0.85rem', background: '#ede7ff', borderRadius: '8px', fontSize: '0.82rem', color: '#3d2080' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}>
|
||||||
<span>18" {vinylShape.name} balloon</span><span>{fmt(vinylShape.priceCents)}</span>
|
<span>18" {vinylShape.name} foil balloon</span><span>{fmt(vinylShape.priceCents)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<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>
|
</div>
|
||||||
|
{vinylLetterCount > 0 && (
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 700, borderTop: '1px solid #c4b0f0', marginTop: '4px', paddingTop: '4px' }}>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -689,7 +729,7 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
|
|||||||
const selectedVariationId = activeVariation?.id
|
const selectedVariationId = activeVariation?.id
|
||||||
const storedVariationId = userVariation?.id ?? selectedVariationId
|
const storedVariationId = userVariation?.id ?? selectedVariationId
|
||||||
const selectedFont = vinylConfig?.fonts.find((f) => f.id === vinylFontId)
|
const selectedFont = vinylConfig?.fonts.find((f) => f.id === vinylFontId)
|
||||||
const vinylFields = product.vinylEnabled && vinylText && vinylShape ? {
|
const vinylFields = product.vinylEnabled && wantsVinyl && vinylText && vinylShape ? {
|
||||||
vinylText,
|
vinylText,
|
||||||
vinylFontId,
|
vinylFontId,
|
||||||
vinylFontName: selectedFont?.name,
|
vinylFontName: selectedFont?.name,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user