/* ============================================================ Interactive Quote & Booking — multi-step with dynamic pricing ============================================================ */ const VEHICLES = [ { id: 'sedan', label: 'Sedan', sub: 'Compact · Mid-size · Hatchback' }, { id: 'suv', label: 'SUV', sub: 'Crossover · 5–7 seat' }, { id: 'pickup', label: 'Pick-Up', sub: 'F-150, Silverado…' }, { id: 'van', label: 'Van', sub: 'Sienna, Odyssey…' }, { id: 'comm', label: 'Commercial Van', sub: 'Sprinter, Transit' }, { id: 'truck', label: 'Trucks', sub: 'Box truck · Heavy duty' }, ]; /* Fixed price matrix (USD). Per-vehicle services use `prices`, flat-rate services use `flat`. */ const SERVICES = [ { id: 'regular', label: 'Regular Wash', desc: 'Our most common, basic wash — perfect for keeping your vehicle clean.', duration: 45, prices: { sedan: 50, suv: 80, pickup: 80, van: 99, comm: 159, truck: 199 }, // Notes shown only when that vehicle is selected. vehicleNotes: { pickup: 'If heavily soiled: $100–$120.' }, }, { id: 'detailing', label: 'Detailing', desc: 'Deep interior or exterior cleaning using specialized chemical products for a thorough, professional finish.', duration: 150, prices: { sedan: 299, suv: 349, pickup: 349, van: 349, comm: 499, truck: 649 }, note: 'Exterior-only or interior-only: 30% off.', vehicleNotes: { pickup: 'If heavily soiled: $499.' }, }, { id: 'luxury', label: 'Luxury Wash', desc: 'Premium service designed for high-end vehicles, with the utmost care and attention to detail.', duration: 210, flat: 699, }, { id: 'headlight', label: 'Headlight Polishing', desc: 'We clean and restore the original shine of your headlights, improving both visibility and appearance.', duration: 60, flat: 99, }, ]; /* Resolve the price for a service + vehicle. Flat services ignore vehicle. */ const servicePrice = (service, vehicleId) => { if (!service) return null; if (service.flat != null) return service.flat; if (!vehicleId) return null; return service.prices?.[vehicleId] ?? null; }; const PAYMENTS = [ { id: 'card', label: 'Stripe', fee: 0.04, sub: 'Credit / Debit · 4% 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, }; /* Cities we currently service. Booking can only proceed to payment when the selected city is one of these. */ const SERVICE_AREAS = [ 'Los Angeles', 'Santa Monica', 'Malibu', 'Beverly Hills', 'West Hollywood', 'Culver City', 'Marina del Rey', 'Inglewood', 'Hawthorne', 'Manhattan Beach', 'Torrance', 'Gardena', 'Compton', 'Carson', 'Long Beach', 'Seal Beach', 'Huntington Beach', 'Anaheim', 'Santa Ana', 'Garden Grove', 'Stanton', 'Orange', 'Yorba Linda', 'Buena Park', 'Cerritos', 'Lakewood', 'Bellflower', 'Norwalk', 'Downey', 'South Gate', 'Huntington Park', 'Westmont', 'Whittier', 'La Mirada', 'La Habra', 'Brea', 'Rowland Heights', 'Hacienda Heights', 'La Puente', 'Avocado Heights', 'West Covina', 'Covina', 'Glendora', 'Azusa', 'San Dimas', 'Pomona', 'El Monte', 'Rosemead', 'Alhambra', 'Monterey Park', 'Montebello', 'Pasadena', 'San Marino', 'Arcadia', 'Altadena', 'La Cañada Flintridge', 'Glendale', 'Burbank', 'San Fernando', 'Simi Valley', 'Thousand Oaks', 'Westlake Village', 'Agoura Hills', 'Calabasas', ]; const isServiceableCity = (city) => SERVICE_AREAS.includes(city); // Helpers const fmt = (n) => `$${(Math.round(n*100)/100).toFixed(2)}`; const formatDate = (d) => d.toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric'}); // Card input formatting + validation (mock — no real charge until Stripe backend is wired up) const formatCardNumber = (v) => v.replace(/\D/g,'').slice(0,16).replace(/(.{4})/g,'$1 ').trim(); const formatExp = (v) => { const d = v.replace(/\D/g,'').slice(0,4); return d.length > 2 ? d.slice(0,2)+'/'+d.slice(2) : d; }; const cardIsComplete = (c) => c.number.replace(/\s/g,'').length >= 15 && /^\d{2}\/\d{2}$/.test(c.exp) && c.cvc.length >= 3 && c.name.trim().length > 1; 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 [card, setCard] = React.useState({ number:'', exp:'', cvc:'', name:'' }); const [done, setDone] = React.useState(false); const [bookingRef, setBookingRef] = React.useState(null); const [paying, setPaying] = React.useState(false); const [payError, setPayError] = React.useState(null); // Returning from Stripe Checkout: ?checkout=success | cancel React.useEffect(() => { const params = new URLSearchParams(window.location.search); const status = params.get('checkout'); if (!status) return; const sessionId = params.get('session_id'); // Clean the URL so a refresh doesn't re-trigger this. window.history.replaceState({}, '', window.location.pathname + '#book'); const restoreConfirmation = (snap) => { if (window.PrestigeStore) window.PrestigeStore.upsert({ ...snap.req, paid: true }); // Restore enough state to render the confirmation screen. setBookingRef(snap.req && snap.req.id ? snap.req.id : null); setVehicle(snap.vehicle); setService(snap.service); setDate(new Date(snap.date + 'T00:00:00')); setSlot(snap.slot); setAddr(snap.addr); setContact(snap.contact); setPromoApplied(snap.promoApplied || null); setDone(true); }; if (status === 'success' && sessionId) { // Never trust the redirect alone — confirm with the server that Stripe was // actually paid before marking the booking as paid. fetch('api/verify-session.php?session_id=' + encodeURIComponent(sessionId)) .then(r => r.json()) .then(v => { let snap = null; try { snap = JSON.parse(localStorage.getItem('prestige_pending_checkout') || 'null'); } catch (e) {} if (v && v.paid && snap) { restoreConfirmation(snap); // Returning from Stripe lands at the top of the page — bring the // confirmation (which lives in the booking section) into view. setTimeout(() => { const el = document.getElementById('book'); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 150); } else { setPayError('We could not confirm your payment. If you were charged, please contact us.'); setStep(STEPS.length - 1); // back to the payment step } }) .catch(() => { setPayError('We could not verify your payment. Please contact us if you were charged.'); setStep(STEPS.length - 1); }) .finally(() => { localStorage.removeItem('prestige_pending_checkout'); }); } else if (status === 'cancel') { localStorage.removeItem('prestige_pending_checkout'); setStep(STEPS.length - 1); // back to the payment step } }, []); // 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 = servicePrice(service, vehicle.id); if (base == null) return null; const subtotal = base; const promoPct = (promoApplied && PROMO_CODES[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 = service.duration; 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 = [ '7:00 AM','8:00 AM','9:00 AM','10:00 AM','11:00 AM','12:00 PM','1:00 PM', '2:00 PM','3:00 PM','4:00 PM','5:00 PM','6:00 PM','7: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 && isServiceableCity(addr.city) && 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 last4 = payment === 'card' ? card.number.replace(/\s/g,'').slice(-4) : null; 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, sub: vehicle.sub }, service: { id: service.id, label: service.label, duration: service.duration, price: servicePrice(service, vehicle.id) }, date: window.PrestigeDate.ymd(date), slot, payment, promo: promoApplied, pricing: pricing ? { subtotal: pricing.subtotal, fee: pricing.fee, total: pricing.total, payLabel: (PAY?.label || payment) + (last4 ? ` •••• ${last4}` : ''), } : null, emailSent: false, emailSentAt: null, notes: '', }; window.PrestigeStore.upsert(req); setBookingRef(req.id); } setDone(true); }; // Build the booking request object (shared by cash submit + Stripe checkout). const buildRequest = (paid) => { const PAY = PAYMENTS.find(p => p.id === payment); return { id: `REQ-${Date.now().toString(36).slice(-4)}-${Math.random().toString(36).slice(2,6).toUpperCase()}`, createdAt: new Date().toISOString(), status: 'pending', paid, 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, sub: vehicle.sub }, service: { id: service.id, label: service.label, duration: service.duration, price: servicePrice(service, vehicle.id) }, 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: '', }; }; // Card payment: create a Stripe Checkout Session on the server, then redirect. const startCheckout = async () => { if (!vehicle || !service || !date || !slot || !pricing) return; setPayError(null); setPaying(true); try { // Stash a snapshot so we can show the confirmation when Stripe redirects back. const req = buildRequest(false); const snapshot = { req, vehicle, service, date: window.PrestigeDate.ymd(date), slot, addr, contact, promoApplied }; localStorage.setItem('prestige_pending_checkout', JSON.stringify(snapshot)); const res = await fetch('api/create-checkout-session.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ vehicleId: vehicle.id, serviceId: service.id, promo: promoApplied || '', email: contact.email, label: `${service.label} — ${vehicle.label}`, reference: req.id, }), }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.url) throw new Error(data.error || 'We could not start the payment. Please try again.'); window.location.href = data.url; } catch (e) { localStorage.removeItem('prestige_pending_checkout'); setPaying(false); setPayError(e.message || 'Payment could not be started. Please try again.'); } }; 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); setCard({number:'',exp:'',cvc:'',name:''}); setDone(false); setBookingRef(null); }} />; 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 && ( )}
{payError && (
{payError}
)}
Secure payments Stripe · Cash on service
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? (424) 415-0208
); }; /* ---- 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', comm: 'comercial.svg', truck: 'pickup.svg', // no dedicated truck asset yet — reuses pickup silhouette }; 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 = servicePrice(s, vehicle?.id); 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})}/>
{addr.city && !isServiceableCity(addr.city) && (
Sorry — we don't service this city yet.
)}
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, card, setCard, 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 => ( ))}
{payment === 'card' && (
SECURE CHECKOUT
When you confirm, you'll be taken to Stripe's secure checkout to enter your card and complete payment — then brought right back here.
Secured by Stripe — your card details never touch our servers.
)}
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, reference, 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.

Reference{reference || '—'}
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;