// Booking — gamified flow: // 1) Customer details (auto-completion checkmarks per field, score ticker) // 2) Pick typology + tower // 3) Schedule & confirm // 4) Confetti + handshake confirmation // Each step transition fires a particle burst, an achievement toast, and an // XP-style progress fill. Buttons live separately from particles via z-index. const BOOKING_KEYS_ID = 'uni-booking-gamified-keys'; function ensureBookingKeys() { if (typeof document === 'undefined') return; if (document.getElementById(BOOKING_KEYS_ID)) return; const s = document.createElement('style'); s.id = BOOKING_KEYS_ID; s.textContent = ` @keyframes uniBadgePop { 0% { transform: scale(0.7); } 50% { transform: scale(1.18); } 100% { transform: scale(1); } } @keyframes uniBadgeStamp { 0% { transform: scale(0.8) rotate(-12deg); } 55% { transform: scale(1.15) rotate(4deg); } 100% { transform: scale(1) rotate(0deg); } } @keyframes uniAchSlide { 0% { opacity: 0; transform: translateX(-50%) translateY(-22px) scale(0.94); } 14% { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); } 78% { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); } 100% { opacity: 0; transform: translateX(-50%) translateY(-12px) scale(0.96); } } @keyframes uniStepIn { 0% { opacity: 0; transform: translateX(40px); filter: blur(6px); } 100% { opacity: 1; transform: translateX(0); filter: blur(0); } } @keyframes uniFieldCheckPop { 0% { transform: translateY(-50%) scale(0); } 55% { transform: translateY(-50%) scale(1.22); } 100% { transform: translateY(-50%) scale(1); } } @keyframes uniTowerSelected { 0% { transform: scale(1); } 40% { transform: scale(1.06); box-shadow: 0 0 0 6px rgba(232,215,168,0.30); } 100% { transform: scale(1); } } `; document.head.appendChild(s); } // Live canvas-height density: 0 on the 16:10 Tab S7 (H=1600), 1 on iPad Pro 4:3 // (H=1920). Grows figures + touch targets so each step fills the taller canvas. // (`dlerp` is a shared global defined in tools.jsx, which loads before this file.) function useBookingDens() { const read = () => { const H = (typeof window !== 'undefined' && window.UNIVERSE_CANVAS && window.UNIVERSE_CANVAS.H) || 1600; return Math.max(0, Math.min(1, (H - 1600) / 320)); }; const [d, setD] = React.useState(read); React.useEffect(() => { const on = () => setD(read()); on(); window.addEventListener('resize', on); window.addEventListener('orientationchange', on); if (window.visualViewport) window.visualViewport.addEventListener('resize', on); return () => { window.removeEventListener('resize', on); window.removeEventListener('orientationchange', on); if (window.visualViewport) window.visualViewport.removeEventListener('resize', on); }; }, []); return d; } function Booking() { ensureBookingKeys(); const dens = useBookingDens(); const [step, setStep] = React.useState(1); const [customer, setCustomer] = React.useState({ ...SAMPLE_CUSTOMER }); const [pick, setPick] = React.useState({ block: TYPOLOGIES[2].code, towerId: 'C' }); const [schedule, setSchedule] = React.useState({ visitDate:'', visitTime:'10:00', notes:'' }); const [bursts, fireBurst] = useBurst(900); const [stepFlash, setStepFlash] = React.useState(0); // bumps each step transition const [achievement, setAchievement] = React.useState(null); const ty = TYPOLOGIES.find(x => x.code === pick.block); const tw = TOWERS.find(x => x.id === pick.towerId); // each step's completion score 0..1 — drives gamified progress fill const customerScore = scoreCustomer(customer); const residenceScore = pick.block && pick.towerId ? 1 : 0; const scheduleScore = (schedule.visitDate ? 0.5 : 0) + (schedule.visitTime ? 0.5 : 0); const stepScores = [customerScore, residenceScore, scheduleScore, 1]; const ACHIEVEMENT_TITLES = [ 'DETAILS CAPTURED · +1', 'RESIDENCE LOCKED · +1', 'VISIT SCHEDULED · +1', 'BOOKING COMPLETE', ]; const advance = (ev) => { if (step >= 4) return; // reward burst at the button center if (ev) { const root = document.querySelector('.tablet-bezel').getBoundingClientRect(); const scale = root.width / 2560; const r = ev.currentTarget.getBoundingClientRect(); const cx = (r.left + r.width / 2 - root.left) / scale; const cy = (r.top + r.height / 2 - root.top) / scale; fireBurst(cx, cy, { count: 16 }); } // achievement toast setAchievement({ id: Date.now(), label: ACHIEVEMENT_TITLES[step - 1] }); setTimeout(() => setAchievement(null), 1700); setStepFlash(f => f + 1); // RealDesk intent event: a completed booking (step 3 → confirmation) if (step === 3 && typeof window !== 'undefined' && window.RDA) { try { window.RDA.track('booking', (schedule && (schedule.date || schedule.time)) ? 'scheduled' : 'confirmed', 1, { step: 'confirm' }); } catch (e) {} } setStep(step + 1); }; return (
{/* === GAMIFIED PROGRESS === XP-bar style: 4 step badges connected by a gold fill that animates smoothly as steps advance. Each badge pops with a spring on activation; completed steps show a checkmark. */} {/* achievement toast */} {/* burst layer for tap rewards (sits behind the buttons) */}
{step === 1 && } {step === 2 && } {step === 3 && } {step === 4 && }
{/* footer nav */}
STEP {step} OF 4
{step < 4 ? ( ) : ( )}
{/* ── COMING SOON — an OPAQUE ivory scrim fully hides the live form behind it; the module reads as built-but-not-yet-open, with a clear way home ── */}
MODULE · 08 / BOOKING
Coming soon
Online reservations open shortly. Until then, our sales team will gladly reserve your residence in person.
); } // === Score helper — how complete is the customer step? ==================== function scoreCustomer(c) { let s = 0; if (c.name && c.name.trim().length > 1) s += 0.30; if (c.phone && c.phone.replace(/\D/g, '').length >= 10) s += 0.25; if (c.email && /@/.test(c.email)) s += 0.25; if (c.pan && c.pan.length >= 5) s += 0.20; return Math.min(1, s); } // === GamifiedProgress — XP-bar with step badges ============================ function GamifiedProgress({ step, stepScores, flashKey }) { const STEPS = ['Customer', 'Residence', 'Schedule', 'Confirm']; const fill = ((step - 1) + (stepScores[step - 1] || 0)) / 3; return (
{/* the connector bar (positioned behind badges) */}
{STEPS.map((s, i) => { const n = i + 1; const done = step > n; const cur = step === n; return (
{done ? : n}
{s}
); })}
); } // === Achievement toast — slides in from top, fades after 1.7s ============ function AchievementToast({ a }) { if (!a) return null; return (
{a.label}
); } // === Score readout — small "x% complete" ticker under STEP X OF 4 ========= function ScoreReadout({ step, score }) { const pct = Math.round(score * 100); return (
{pct}%
); } // === StepFader — cross-fade + slide between steps ======================== function StepFader({ stepKey, children }) { return (
{children}
); } // === NextButton — magnetic-CTA-style with tap reward burst =============== function NextButton({ onTap, label, dens = 0 }) { const d = dens; return ( ); } function StepCustomer({ customer, setCustomer, dens = 0 }) { const d = dens; const set = (k, v) => setCustomer({ ...customer, [k]: v }); return (
{/* LEFT — pitch, vertically centred */}
Tell us a little about you.
We use this information to personalise your visit, your cost sheet, and your home-loan options.
YOUR PRIVACY
Your details are stored only on this tablet. Our sales team will reach out within 24 hours.
{/* RIGHT — fields grouped at the vertical centre (mirrors the left pitch column) so they never stretch apart into dead space on the taller iPad canvas. */}
set('name', v)} dens={d}/>
set('phone',v)} dens={d}/> set('email',v)} dens={d}/>
set('pan', v)} dens={d}/>
set('loanPreApproved', e.target.checked)}/>
); } // Helper — compute burst position in design-space from a click event function tapPos(ev) { const root = document.querySelector('.tablet-bezel'); if (!root) return { cx: 1280, cy: 800 }; const rb = root.getBoundingClientRect(); const scale = rb.width / 2560; const r = ev.currentTarget.getBoundingClientRect(); return { cx: (r.left + r.width / 2 - rb.left) / scale, cy: (r.top + r.height / 2 - rb.top) / scale, }; } function StepResidence({ pick, setPick, fireBurst, dens = 0 }) { const d = dens; const ty = TYPOLOGIES.find(x => x.code === pick.block); const tw = TOWERS.find(x => x.id === pick.towerId); const onTowerTap = (ev, id) => { if (id !== pick.towerId) { const { cx, cy } = tapPos(ev); fireBurst(cx, cy, { count: 10 }); } setPick({ ...pick, towerId: id }); }; return (
Which residence calls to you?
Pick a typology and a tower. We'll lock the unit list to give you priority access at our next visit.
{/* Live preview card — confirms the choice with warmth */}
SELECTED
{ty?.name} · Tower {tw?.id}
TICKET
{ty ? formatINR(ty.price,{decimals:2}) : '—'}
AVAILABLE
{tw ? towerSummary(tw.id).available : '—'} units
CLUSTER
{tw?.cluster}
TYPOLOGY
PREFERRED TOWER
{TOWERS.map(t => { const sel = t.id === pick.towerId; return ( ); })}
); } function StepSchedule({ schedule, setSchedule, fireBurst, dens = 0 }) { const D = dens; const set = (k, v) => setSchedule({ ...schedule, [k]: v }); // Available slots — next 14 days const today = new Date(); const days = Array.from({length: 14}, (_, i) => { const d = new Date(today); d.setDate(today.getDate() + i + 1); return d; }); const onDateTap = (ev, key) => { if (key !== schedule.visitDate) { const { cx, cy } = tapPos(ev); fireBurst(cx, cy, { count: 9 }); } set('visitDate', key); }; const onTimeTap = (ev, slot) => { if (slot !== schedule.visitTime) { const { cx, cy } = tapPos(ev); fireBurst(cx, cy, { count: 8 }); } set('visitTime', slot); }; return (
When can we host you?
Visit our show home at Stratum @ Venus Grounds. The space takes about 60 to 75 minutes to walk through.
Stratum @ Venus Grounds
NEHRU NAGAR · AHMEDABAD · 380015
PICK A DATE
{days.map((d) => { const key = d.toISOString().slice(0,10); const sel = schedule.visitDate === key; return ( ); })}
PREFERRED TIME
{['10:00','12:00','14:00','16:00','17:30','19:00'].map(slot => { const sel = slot === schedule.visitTime; return ( ); })}
SPECIAL REQUESTS (OPTIONAL)