// 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 */}
setStep(Math.max(1, step-1))} disabled={step===1} className="mono"
onMouseDown={e=>{ if(step>1) e.currentTarget.style.transform='scale(0.97)'; }}
onMouseUp={e=>{ e.currentTarget.style.transform='scale(1)'; }}
onMouseLeave={e=>{ e.currentTarget.style.transform='scale(1)'; }}
style={{
padding:dlerp(dens,21,25)+'px '+dlerp(dens,40,48)+'px', borderRadius:105, border:'1px solid var(--line)', background:'transparent',
color: step===1 ? 'var(--muted)' : 'var(--ink)',
fontSize:dlerp(dens,19,23), letterSpacing:'0.2em', textTransform:'uppercase',
opacity: step===1 ? 0.4 : 1, cursor: step===1 ? 'default' : 'pointer',
transition:'transform 160ms cubic-bezier(0.22,1,0.36,1)',
}}>← Back
{step < 4 ? (
) : (
navigate('home')} className="mono"
onMouseDown={e=>{ e.currentTarget.style.transform='scale(0.97)'; }}
onMouseUp={e=>{ e.currentTarget.style.transform='scale(1)'; }}
onMouseLeave={e=>{ e.currentTarget.style.transform='scale(1)'; }}
style={{
padding:dlerp(dens,21,25)+'px '+dlerp(dens,40,48)+'px', borderRadius:105, border:'1px solid var(--gold)', background:'var(--night)', color:'var(--gold)',
fontSize:dlerp(dens,19,23), letterSpacing:'0.2em', textTransform:'uppercase', cursor:'pointer',
transition:'transform 160ms cubic-bezier(0.22,1,0.36,1)',
}}>Back to home →
)}
{/* ── 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.
navigate('home')} className="mono"
onMouseDown={e=>{ e.currentTarget.style.transform='scale(0.97)'; }}
onMouseUp={e=>{ e.currentTarget.style.transform='scale(1)'; }}
onMouseLeave={e=>{ e.currentTarget.style.transform='scale(1)'; }}
style={{marginTop:40, display:'inline-flex', alignItems:'center', gap:12, cursor:'pointer',
padding:'20px 42px', borderRadius:105, border:'1px solid var(--gold-deep)',
background:'linear-gradient(135deg, var(--gold), var(--gold-deep))', color:'#2a1d05',
fontSize:18, letterSpacing:'0.2em', fontWeight:600, textTransform:'uppercase',
boxShadow:'0 14px 30px rgba(176,138,63,0.30)', transition:'transform 160ms cubic-bezier(0.22,1,0.36,1)'}}>
← Back to home
);
}
// === 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 (
);
})}
);
}
// === Achievement toast — slides in from top, fades after 1.7s ============
function AchievementToast({ a }) {
if (!a) return null;
return (
);
}
// === Score readout — small "x% complete" ticker under STEP X OF 4 =========
function ScoreReadout({ step, score }) {
const pct = Math.round(score * 100);
return (
);
}
// === 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 (
{ e.currentTarget.style.transform = 'scale(0.97)'; }}
onMouseUp={e => { e.currentTarget.style.transform = 'scale(1)'; }}
onMouseLeave={e=> { e.currentTarget.style.transform = 'scale(1)'; }}>
{label}
);
}
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. */}
);
}
// 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
TYPOLOGY
setPick({...pick, block:e.target.value})}>
{TYPOLOGIES.map(x => {x.name} ({formatINR(x.price,{decimals:2})}) )}
PREFERRED TOWER
{TOWERS.map(t => {
const sel = t.id === pick.towerId;
return (
onTowerTap(ev, t.id)}
style={{
position:'relative',
padding:dlerp(d,29,40)+'px 0', borderRadius:17,
border:'1px solid '+(sel ? 'var(--gold-deep)' : 'rgba(176,138,63,0.18)'),
background: sel
? 'linear-gradient(180deg, var(--tile-light) 0%, var(--tile) 60%, var(--tile-deep) 100%)'
: 'var(--ivory-2)',
color: sel ? 'var(--on-tile)' : 'var(--ink)',
cursor:'pointer', display:'flex', flexDirection:'column', alignItems:'center', gap:6,
boxShadow: sel
? '0 19px 40px rgba(50,32,12,0.32), inset 0 1px 0 rgba(255,246,224,0.22), 0 0 0 3px rgba(232,215,168,0.20)'
: '0 5px 15px rgba(10,10,10,0.04)',
transition:'transform 220ms cubic-bezier(0.22,1,0.36,1), box-shadow 220ms ease, background 240ms',
animation: sel ? 'uniTowerSelected 460ms cubic-bezier(0.22,1,0.36,1)' : 'none',
}}>
{t.id}
{towerSummary(t.id).available} avail
{sel && (
)}
);
})}
);
}
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 (
onDateTap(ev, key)} style={{
flexShrink:0, padding:dlerp(D,23,30)+'px '+dlerp(D,27,32)+'px', borderRadius:17,
border:'1px solid '+(sel ? 'var(--gold-deep)' : 'rgba(176,138,63,0.16)'),
background: sel
? 'linear-gradient(180deg, var(--tile-light) 0%, var(--tile) 60%, var(--tile-deep) 100%)'
: 'var(--ivory-2)',
color: sel ? 'var(--on-tile)' : 'var(--ink)',
display:'flex', flexDirection:'column', alignItems:'center', gap:dlerp(D,6,9), minWidth:dlerp(D,111,128), cursor:'pointer',
boxShadow: sel
? '0 19px 36px rgba(50,32,12,0.32), inset 0 1px 0 rgba(255,246,224,0.20), 0 0 0 3px rgba(232,215,168,0.18)'
: '0 4px 13px rgba(10,10,10,0.03)',
transition:'transform 220ms cubic-bezier(0.22,1,0.36,1), box-shadow 220ms ease, background 240ms',
animation: sel ? 'uniTowerSelected 460ms cubic-bezier(0.22,1,0.36,1)' : 'none',
}}>
{d.toLocaleDateString('en',{weekday:'short'}).toUpperCase()}
{d.getDate()}
{d.toLocaleDateString('en',{month:'short'}).toUpperCase()}
);
})}
PREFERRED TIME
{['10:00','12:00','14:00','16:00','17:30','19:00'].map(slot => {
const sel = slot === schedule.visitTime;
return (
onTimeTap(ev, slot)} className="mono" style={{
padding:dlerp(D,23,30)+'px 0', borderRadius:15,
border:'1px solid '+(sel ? 'var(--gold-deep)' : 'rgba(176,138,63,0.16)'),
background: sel
? 'linear-gradient(180deg, var(--tile-light) 0%, var(--tile) 100%)'
: 'var(--ivory-2)',
color: sel ? 'var(--on-tile)' : 'var(--ink)',
fontSize:dlerp(D,19,23), letterSpacing:'0.18em', cursor:'pointer', fontWeight: sel ? 700 : 500,
boxShadow: sel
? '0 15px 29px rgba(50,32,12,0.28), inset 0 1px 0 rgba(255,246,224,0.20)'
: '0 3px 11px rgba(10,10,10,0.03)',
transition:'transform 220ms cubic-bezier(0.22,1,0.36,1), box-shadow 220ms ease, background 240ms',
animation: sel ? 'uniTowerSelected 420ms cubic-bezier(0.22,1,0.36,1)' : 'none',
}}>{slot}
);
})}
SPECIAL REQUESTS (OPTIONAL)
);
}
function StepConfirm({ customer, ty, tw, schedule, dens = 0 }) {
const D = dens;
const t = useLoop();
// Trigger confetti shortly after the check finishes drawing
const [fire, setFire] = React.useState(0);
React.useEffect(() => {
const t1 = setTimeout(() => setFire(f => f + 1), 1200);
return () => clearTimeout(t1);
}, []);
return (
{/* gold check ring */}
{/* contained pulse ring — stays within the 220px frame */}
{[0,1].map(i => {
const phase = ((t + i*0.7) % 2.4) / 2.4;
return (
);
})}
YOUR VISIT IS CONFIRMED
We look forward to hosting you, {(customer.name||'').split(' ').slice(-1)[0]}.
Booking ID · UNI-{Math.floor(Math.random() * 9000 + 1000)} · A confirmation has been queued for your records.
);
}
function Detail({ k, v, sub, dens = 0 }) {
const d = dens;
return (
);
}
function Field({ label, v, set, span = 1, dens = 0 }) {
const d = dens;
const filled = v && v.toString().trim().length > 1;
const [focused, setFocused] = React.useState(false);
return (
{label.toUpperCase()}
set(e.target.value)}
onFocus={()=>setFocused(true)} onBlur={()=>setFocused(false)}
style={{
width:'100%', paddingRight: 74,
fontSize:dlerp(d,24,30), minHeight:dlerp(d,76,92),
borderColor: focused ? 'var(--gold)' : (filled ? 'rgba(176,138,63,0.40)' : 'var(--line)'),
background: filled ? 'rgba(232,215,168,0.10)' : 'var(--ivory-2)',
transition:'border-color 240ms, background 240ms',
}}/>
{/* completion check — pops in when field becomes filled */}
{filled && (
)}
);
}
window.Booking = Booking;