/* global React */ const { useState: useStateBook, useMemo: useMemoBook } = React; const BOOK_SERVICES = [ { id: 'gel', name: 'Gel Polish', duration: '45 min', min: 45 }, { id: 'art', name: 'Nail Art', duration: '60–90 min', min: 90 }, { id: 'ext', name: 'Nail Extensions', duration: '90 min', min: 90 }, { id: 'gext', name: 'Gel Extensions', duration: '90–120 min', min: 120 }, { id: 'press', name: 'Press-On Fitting', duration: '30 min', min: 30 }, { id: 'mani', name: 'Manicure', duration: '45 min', min: 45 }, { id: 'pedi', name: 'Pedicure', duration: '60 min', min: 60 }, { id: 'refill', name: 'Refill', duration: '60 min', min: 60 }, { id: 'custom', name: 'Custom', duration: 'Tell us what you need', min: 60 }, ]; // Convert "HH:MM" to minutes since midnight function timeToMins(t) { const [h, m] = t.split(':').map(Number); return h * 60 + m; } // Is slot `t` blocked given existing bookings (array of {start, duration_min}) and own selection duration? function isSlotBlocked(t, bookings, selectedDuration) { const slotStart = timeToMins(t); const slotEnd = slotStart + selectedDuration; // 1. This slot's own block overlaps an existing booking for (const b of bookings) { const bStart = timeToMins(b.start); const bEnd = bStart + b.duration_min; if (bStart < slotEnd && bEnd > slotStart) return true; } return false; } const TIME_GROUPS = { Morning: ['10:00', '10:30', '11:00', '11:30', '12:00', '12:30'], Afternoon: ['13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30'], Evening: ['17:00', '17:30', '18:00', '18:30', '19:00', '19:30', '20:00'] }; function emptyPerson(idx) { return { id: idx, name: '', mobile: '', email: '', services: idx === 0 ? ['gel'] : [] }; } function formatDate(d) { return d.toLocaleDateString('en-IN', { weekday: 'short', day: 'numeric', month: 'short' }); } window.BookHero = function BookHero() { return (
Reserve · Step into luxury

Book your moment.

A few details and you're set. Confirmation lands on your WhatsApp instantly — no calls, no waiting.

); }; window.BookForm = function BookForm() { const [people, setPeople] = useStateBook([emptyPerson(0)]); const [date, setDate] = useStateBook(null); // Date object const [time, setTime] = useStateBook(null); const [submitted, setSubmitted] = useStateBook(false); const [submitting, setSubmitting] = useStateBook(false); const [submitError, setSubmitError] = useStateBook(''); const [touched, setTouched] = useStateBook(false); const [bookings, setBookings] = useStateBook([]); // [{start, duration_min}] const [loadingSlots, setLoadingSlots] = useStateBook(false); const [payConfig, setPayConfig] = useStateBook(null); const [confirmedRef, setConfirmedRef] = useStateBook(null); const today = new Date(); today.setHours(0, 0, 0, 0); // Load payment config once React.useEffect(() => { if (window.LQ_PUB) window.LQ_PUB.fetchPaymentConfig().then(setPayConfig); }, []); // Total duration of the primary person's selected services (used for slot blocking) const selectedDuration = useMemoBook(() => { const primary = people[0]; if (!primary || primary.services.length === 0) return 60; return primary.services.reduce((sum, sid) => { const svc = BOOK_SERVICES.find(x => x.id === sid); return sum + (svc ? svc.min : 60); }, 0); }, [people[0] && people[0].services.join(',')]); // Re-fetch when date changes React.useEffect(() => { if (!date) { setBookings([]); return; } const iso = date.toISOString().slice(0, 10); setLoadingSlots(true); fetch('/api/booked-slots?date=' + iso) .then(r => r.ok ? r.json() : null) .then(j => { const raw = (j && j.data) || []; // Support both old format (array of strings) and new format (array of objects) const normalised = raw.map(x => typeof x === 'string' ? { start: x, duration_min: 60 } : x); setBookings(normalised); }) .catch(() => setBookings([])) .finally(() => setLoadingSlots(false)); }, [date && date.toDateString()]); // Clear selected time if it becomes blocked after service change React.useEffect(() => { if (time && bookings.length > 0 && isSlotBlocked(time, bookings, selectedDuration)) setTime(null); }, [selectedDuration, bookings.length]); const valid = people.every(p => p.name.trim() && /^\d{10}$/.test(p.mobile.replace(/\s/g, '')) && p.services.length > 0) && date && time; const updatePerson = (id, key, value) => { setPeople(people.map(p => p.id === id ? { ...p, [key]: value } : p)); }; const toggleService = (personId, sid) => { setPeople(people.map(p => { if (p.id !== personId) return p; const has = p.services.includes(sid); return { ...p, services: has ? p.services.filter(s => s !== sid) : [...p.services, sid] }; })); }; const addPerson = () => { if (people.length >= 4) return; setPeople([...people, emptyPerson(Date.now())]); }; const removePerson = (id) => setPeople(people.filter(p => p.id !== id)); const needsPayment = payConfig && payConfig.enable_booking_fee && !payConfig.test_mode && payConfig.razorpay_key_id; function buildBookingBodies(paymentFields) { const isoDate = date.toISOString().slice(0, 10); return people.map(p => { const serviceNames = p.services .map(sid => (BOOK_SERVICES.find(x => x.id === sid) || {}).name) .filter(Boolean) .join(', '); const personDuration = p.services.reduce((sum, sid) => { const svc = BOOK_SERVICES.find(x => x.id === sid); return sum + (svc ? svc.min : 60); }, 0) || 60; const noteParts = []; if (people.length > 1) noteParts.push('Group booking · ' + people.length + ' people'); if (p.services.includes('custom') && p.customNote) noteParts.push('Custom request: ' + p.customNote.trim()); return { name: p.name.trim(), mobile: p.mobile.replace(/\s/g, ''), email: p.email ? p.email.trim() : null, service: serviceNames || 'Multiple services', appt_date: isoDate, appt_time: time, guests: people.length, duration_min: personDuration, notes: noteParts.join(' · '), ...paymentFields, }; }); } async function doSubmit(paymentFields) { setSubmitting(true); try { const bodies = buildBookingBodies(paymentFields || {}); let lastRef = null; for (const body of bodies) { const res = await (window.LQ_PUB ? window.LQ_PUB.submitBooking(body) : Promise.resolve({ data: {} })); if (res && res.data && res.data.ref) lastRef = res.data.ref; } setConfirmedRef(lastRef); setSubmitted(true); window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (err) { setSubmitError((err && (err.error || err.message)) || 'Submission failed. Please try again or message us on WhatsApp.'); } finally { setSubmitting(false); } } const submit = async (e) => { e.preventDefault(); setTouched(true); if (!valid) return; setSubmitError(''); if (needsPayment) { setSubmitting(true); let orderData; try { orderData = await window.LQ_PUB.createPaymentOrder(); } catch (err) { setSubmitError('Could not initiate payment. Please try again.'); setSubmitting(false); return; } setSubmitting(false); const primaryPerson = people[0]; const options = { key: orderData.key_id, amount: orderData.amount, currency: orderData.currency, order_id: orderData.order_id, name: 'Lacquery Nails', description: 'Appointment Booking Deposit', image: 'https://lacquerynails.in/assets/logo-round.png', prefill: { name: primaryPerson.name.trim(), contact: '+91' + primaryPerson.mobile.replace(/\s/g, ''), email: primaryPerson.email || '', }, theme: { color: '#b8973a' }, handler: function (response) { doSubmit({ razorpay_payment_id: response.razorpay_payment_id, razorpay_order_id: response.razorpay_order_id, razorpay_signature: response.razorpay_signature, }); }, modal: { ondismiss: function () { setSubmitError('Payment was cancelled. Please try again to complete your booking.'); } }, }; const rzp = new window.Razorpay(options); rzp.open(); } else { await doSubmit(null); } }; if (submitted) { return { setSubmitted(false); setPeople([emptyPerson(0)]); setDate(null); setTime(null); setTouched(false); setConfirmedRef(null); }} />; } const feeAmount = payConfig && payConfig.booking_fee_amount ? payConfig.booking_fee_amount : 200; return (
{payConfig && payConfig.test_mode && (
Test mode active — payment bypassed
)}
{/* DISCOUNT BANNER */}
= 2 ? 'active' : ''}`}>
Bring a friend, save 10% {people.length >= 2 ? 'Group discount applied at the salon.' : 'Add a second person to unlock 10% off both bookings.'}
{people.length < 2 && ( )}
{/* PEOPLE */} {people.map((p, idx) => (
0{idx + 1} {idx === 0 ? 'Primary booking' : 'Guest ' + idx}
{idx > 0 && ( )}
updatePerson(p.id, 'name', e.target.value)} placeholder="Ananya Reddy" className={touched && !p.name.trim() ? 'invalid' : ''} />
+91 updatePerson(p.id, 'mobile', e.target.value.replace(/\D/g, '').slice(0, 10))} placeholder="98765 43210" className={touched && !/^\d{10}$/.test(p.mobile) ? 'invalid' : ''} />
updatePerson(p.id, 'email', e.target.value)} placeholder="ananya@example.com" />
{BOOK_SERVICES.map(s => { const checked = p.services.includes(s.id); return ( ); })}
{touched && p.services.length === 0 &&
Pick at least one service.
} {p.services.includes('custom') && (