/* ============================================================ Interactive Quote & Booking — multi-step with dynamic pricing ============================================================ */ const VEHICLES = [ { id: 'sedan', label: 'Sedan / Hatchback', sub: 'Compact · Mid-size', mult: 1.0, base: 60 }, { id: 'suv', label: 'SUV', sub: 'Crossover · 5–7 seat', mult: 1.25, base: 60 }, { id: 'pickup', label: 'Pickup Truck', sub: 'F-150, Silverado…', mult: 1.35, base: 60 }, { id: 'van', label: 'Van / Family', sub: 'Sienna, Odyssey…', mult: 1.30, base: 60 }, { id: 'lux', label: 'Luxury / Sports', sub: 'Tesla, Mercedes, Porsche', mult: 1.45, base: 60 }, { id: 'comm', label: 'Commercial Van', sub: 'Sprinter, Transit', mult: 1.55, base: 60 }, ]; const SERVICES = [ { id: 'ext', label: 'Exterior Wash', desc: 'Hand wash, wheels, tires, windows.', duration: 45, price: 0 }, { id: 'both', label: 'Interior & Exterior', desc: 'Full hand wash + interior vacuum & wipe-down.', duration: 75, price: 30 }, { id: 'detail', label: 'Full Detail', desc: 'Deep exterior + interior shampoo, restore polish.', duration: 150, price: 90 }, { id: 'intd', label: 'Interior Deep Cleaning', desc: 'Seats, carpets, panels, leather conditioning.', duration: 120, price: 70 }, { id: 'wax', label: 'Wax & Paint Protection', desc: 'Hand wax, sealant, finish protection.', duration: 90, price: 60 }, { id: 'spa', label: 'Premium Auto Spa', desc: 'The full Prestige experience — top-to-bottom.', duration: 210, price: 160 }, ]; const PAYMENTS = [ { id: 'card', label: 'Credit / Debit', fee: 0.04, sub: 'Stripe · 4% processing' }, { id: 'paypal', label: 'PayPal', fee: 0.04, sub: '4% processing' }, { id: 'zelle', label: 'Zelle', fee: 0.03, sub: '3% processing' }, { id: 'cashapp',label: 'Cash App', fee: 0.03, sub: '3% processing' }, { id: 'venmo', label: 'Venmo', fee: 0.03, sub: '3% processing' }, { id: 'cash', label: 'Cash on Service', fee: -0.10, sub: '10% off paid in person' }, ]; const PROMO_CODES = { 'PRESTIGE10': 0.10, 'NEWCLIENT15': 0.15, 'BUILDING5': 0.05, }; // Helpers const fmt = (n) => `$${(Math.round(n*100)/100).toFixed(2)}`; const formatDate = (d) => d.toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric'}); const Booking = ({ initialServiceId } = {}) => { const [step, setStep] = React.useState(0); const [vehicle, setVehicle] = React.useState(null); const [service, setService] = React.useState(null); const [date, setDate] = React.useState(null); const [slot, setSlot] = React.useState(null); const [addr, setAddr] = React.useState({ line: '', city: '', zip: '', notes: '' }); const [contact, setContact] = React.useState({ name: '', email: '', phone: '' }); const [payment, setPayment] = React.useState('card'); const [promo, setPromo] = React.useState(''); const [promoApplied, setPromoApplied] = React.useState(null); const [done, setDone] = React.useState(false); // jump to service if requested from elsewhere React.useEffect(() => { if (initialServiceId) { const s = SERVICES.find(s => s.id === initialServiceId); if (s) { setService(s); setStep(0); } } }, [initialServiceId]); // Calculate pricing const pricing = React.useMemo(() => { if (!vehicle || !service) return null; const base = (vehicle.base + service.price) * vehicle.mult; const subtotal = Math.round(base); const promoPct = promoApplied ? PROMO_CODES[promoApplied] : 0; const promoAmt = subtotal * promoPct; const afterPromo = subtotal - promoAmt; const pay = PAYMENTS.find(p => p.id === payment); const fee = pay ? afterPromo * pay.fee : 0; const total = afterPromo + fee; const duration = Math.round(service.duration * (vehicle.mult > 1.3 ? 1.15 : 1)); return { subtotal, promoPct, promoAmt, fee, total, duration, pay }; }, [vehicle, service, payment, promoApplied]); // Date list — next 14 days const dateOptions = React.useMemo(() => { const out = []; const today = new Date(); for (let i = 0; i < 14; i++){ const d = new Date(today.getFullYear(), today.getMonth(), today.getDate()+i); out.push(d); } return out; }, []); const slots = ['8:00 AM','9:30 AM','11:00 AM','12:30 PM','2:00 PM','3:30 PM','5:00 PM']; const STEPS = [ { id: 'vehicle', label: 'Vehicle' }, { id: 'service', label: 'Service' }, { id: 'when', label: 'Date & time' }, { id: 'where', label: 'Location' }, { id: 'pay', label: 'Review & pay' }, ]; const canProceed = () => { if (step === 0) return !!vehicle; if (step === 1) return !!service; if (step === 2) return !!date && !!slot; if (step === 3) return addr.line && addr.zip && contact.name && contact.email && contact.phone; return true; }; const applyPromo = () => { const code = promo.trim().toUpperCase(); if (PROMO_CODES[code]) { setPromoApplied(code); } else if (code) { setPromoApplied('__invalid'); setTimeout(()=>setPromoApplied(p => p === '__invalid' ? null : p), 1800); } }; const submit = () => { if (window.PrestigeStore && vehicle && service && date && slot) { const PAY = PAYMENTS.find(p => p.id === payment); const req = { id: `REQ-${Date.now().toString(36).slice(-4)}-${Math.random().toString(36).slice(2,6).toUpperCase()}`, createdAt: new Date().toISOString(), status: 'pending', paid: payment !== 'cash', customer: { name: contact.name, email: contact.email, phone: contact.phone }, address: { line: addr.line, city: addr.city, zip: addr.zip, notes: addr.notes }, vehicle: { id: vehicle.id, label: vehicle.label, mult: vehicle.mult, base: vehicle.base }, service: { id: service.id, label: service.label, duration: service.duration, price: service.price }, date: window.PrestigeDate.ymd(date), slot, payment, promo: promoApplied, pricing: pricing ? { subtotal: pricing.subtotal, fee: pricing.fee, total: pricing.total, payLabel: PAY?.label || payment, } : null, emailSent: false, emailSentAt: null, notes: '', }; window.PrestigeStore.upsert(req); } setDone(true); }; if (done) return { setStep(0); setVehicle(null); setService(null); setDate(null); setSlot(null); setAddr({line:'',city:'',zip:'',notes:''}); setContact({name:'',email:'',phone:''}); setPromo(''); setPromoApplied(null); setDone(false); }} />; return (
Book your service

Book in under a minute.

Pick your vehicle, pick a service — see your price instantly.

{/* Stepper */}
{STEPS.map((s, i) => ( ))}
{step === 0 && } {step === 1 && } {step === 2 && } {step === 3 && } {step === 4 && }
{step > 0 && ( )}
{step < STEPS.length - 1 && ( )} {step === STEPS.length - 1 && ( )}
Secure payments Stripe · PayPal · Zelle · Venmo · Cash App
Free cancellation Up to 4 hours before your appointment
Satisfaction guaranteed We’ll make it right if you’re not happy
Prefer to talk to a human? (310) 555-0110
); }; /* ---- Step 1: Vehicle ------------------------------------- */ const StepVehicle = ({ vehicle, setVehicle }) => (

Select your vehicle type

Pricing scales with vehicle size. Choose the option that best matches yours.

{VEHICLES.map(v => ( ))}
); const VehicleSilhouette = ({ type }) => { const files = { sedan: 'sedan.svg', suv: 'suv.svg', pickup: 'pickup.svg', van: 'vanfamily.svg', lux: 'luxury.svg', comm: 'comercial.svg', }; return (
{type}
); }; /* ---- Step 2: Service ------------------------------------- */ const StepService = ({ service, setService, vehicle }) => (

Choose your service

All services scaled to your {vehicle?.label || 'vehicle'}. Prices update automatically.

{SERVICES.map(s => { const price = vehicle ? Math.round((vehicle.base + s.price) * vehicle.mult) : null; return ( ); })}
); /* ---- Step 3: When ---------------------------------------- */ const StepWhen = ({ dateOptions, date, setDate, slots, slot, setSlot }) => { const [taken, setTaken] = React.useState(new Set()); React.useEffect(() => { const refresh = () => { if (!date || !window.PrestigeStore) { setTaken(new Set()); return; } setTaken(window.PrestigeStore.takenSlotsForDate(date)); }; refresh(); window.addEventListener('prestige:requests-changed', refresh); return () => window.removeEventListener('prestige:requests-changed', refresh); }, [date]); return (

Pick a date & time

Real-time availability for our mobile crew across LA. Booked slots are locked.

DATE
{dateOptions.map((d, i) => { const isSel = date && d.toDateString() === date.toDateString(); return ( ); })}
TIME SLOT
{slots.map(s => { const isTaken = taken.has(s); const isActive = slot === s; return ( ); })}
); }; /* ---- Step 4: Where --------------------------------------- */ const StepWhere = ({ addr, setAddr, contact, setContact }) => (

Where should we meet you?

Home, office, garage, hotel — wherever you'll have your vehicle parked.

setAddr({...addr,line:e.target.value})}/>
setAddr({...addr,city:e.target.value})}/>
setAddr({...addr,zip:e.target.value})}/>
setAddr({...addr,notes:e.target.value})}/>
YOUR CONTACT
setContact({...contact,name:e.target.value})}/>
setContact({...contact,phone:e.target.value})}/>
setContact({...contact,email:e.target.value})}/>
); /* ---- Step 5: Pay ----------------------------------------- */ const StepPay = ({ payment, setPayment, promo, setPromo, promoApplied, applyPromo }) => (

Choose how to pay

Pay securely online — or pay in person on the day for a 10% discount.

{PAYMENTS.map(p => ( ))}
setPromo(e.target.value)} onKeyDown={e=>{if(e.key==='Enter') applyPromo();}} />
{promoApplied === '__invalid' && (
Code not recognized.
)} {promoApplied && promoApplied !== '__invalid' && (
✓ {promoApplied} applied · {(PROMO_CODES[promoApplied]*100).toFixed(0)}% off
)}
You'll only be charged after the service is confirmed by our crew. Card payments processed via Stripe.
); /* ---- Sticky summary -------------------------------------- */ const BookingSummary = ({ vehicle, service, date, slot, addr, pricing, promoApplied }) => ( ); const SummaryRow = ({ label, value, sub }) => (
{label}
{value || }
{sub &&
{sub}
}
); /* ---- Confirmation ---------------------------------------- */ const BookingConfirmation = ({ vehicle, service, date, slot, addr, contact, pricing, onReset }) => (
Booking confirmed

You're on the calendar,
{contact.name?.split(' ')[0] || 'friend'}.

A confirmation has been sent to {contact.email || 'your email'}. Our crew will text you 30 minutes before arrival.

ReferencePRS-{Math.floor(Math.random()*9000+1000)}
Vehicle{vehicle?.label}
Service{service?.label}
When{date && formatDate(date)} · {slot}
Where{addr.line}, {addr.city}
Total charged{fmt(pricing.total)}
Back to top
); window.Booking = Booking; window.SERVICES = SERVICES;