// Unit Detail — final step of the inventory drill-down. // params: [towerId, floor, unitNo]. Back → floor//. // LEFT: the unit plan (u.plan) in a pinch / drag / wheel / double-tap zoom-pan // viewer with ± and reset controls. // RIGHT: status + headline, a transparent BASIC PRICE SHEET (unitPriceSheet), // a spec grid, and two actions — primary ENQUIRE / BOOK → booking, and a // distinct HOME → home. // // Reuses window.STATUS, Breadcrumb, fsBackBtn from earlier inventory screens. function UnitDetail() { const t = useLoop(); const e = clamp(t/0.5); const [route] = useRoute(); // Canvas-height density: 0 on 16:10 tablet (1600) → 1 on iPad Pro 4:3 (1920). const CH = (typeof window !== 'undefined' && window.UNIVERSE_CANVAS && window.UNIVERSE_CANVAS.H) || 1600; const dens = clamp((CH - 1600) / 320); const towerId = route.params && route.params[0]; const floor = route.params && parseInt(route.params[1], 10); const unitNo = route.params && route.params[2]; const tower = TOWERS.find(tw => tw.id === towerId); const unit = (tower && Number.isFinite(floor)) ? buildUnits(towerId, floor).find(u => u.no === unitNo) : null; // ── zoom-pan state (ported verbatim) ───────────────────────── const [zoom, setZoom] = React.useState(1); const [pan, setPan] = React.useState({x:0, y:0}); const [hintFaded, setHintFaded] = React.useState(false); const containerRef = React.useRef(null); const gestureRef = React.useRef({ active:false, mode:null, startD:0, startZoom:1, startPanX:0, startPanY:0, startMidX:0, startMidY:0, lastTapAt:0, lastTapX:0, lastTapY:0, dragStartX:0, dragStartY:0, }); React.useEffect(()=>{ setZoom(1); setPan({x:0,y:0}); }, [unitNo]); const clampPan = (p, z) => { const el = containerRef.current; if (!el) return p; const r = el.getBoundingClientRect(); const maxX = Math.max(0, ((z-1)/2)*r.width) + 80; const maxY = Math.max(0, ((z-1)/2)*r.height) + 80; return { x: Math.max(-maxX, Math.min(maxX, p.x)), y: Math.max(-maxY, Math.min(maxY, p.y)) }; }; const fadeHint = () => { if (!hintFaded) setHintFaded(true); }; const handleTouchStart = (ev) => { const g = gestureRef.current; const touches = ev.touches; const el = containerRef.current; const rect = el ? el.getBoundingClientRect() : {left:0,top:0}; if (touches.length === 2) { const [a,b] = [touches[0], touches[1]]; const dx = b.clientX-a.clientX, dy = b.clientY-a.clientY; g.mode='pinch'; g.active=true; g.startD=Math.hypot(dx,dy)||1; g.startZoom=zoom; g.startPanX=pan.x; g.startPanY=pan.y; g.startMidX=(a.clientX+b.clientX)/2-rect.left; g.startMidY=(a.clientY+b.clientY)/2-rect.top; fadeHint(); } else if (touches.length === 1) { const now = performance.now(); const t0 = touches[0]; const tx=t0.clientX, ty=t0.clientY; if (now-g.lastTapAt < 320 && Math.hypot(tx-g.lastTapX, ty-g.lastTapY) < 40) { setZoom(1); setPan({x:0,y:0}); g.lastTapAt=0; g.active=false; g.mode=null; fadeHint(); return; } g.lastTapAt=now; g.lastTapX=tx; g.lastTapY=ty; if (zoom > 1) { g.mode='pan-touch'; g.active=true; g.dragStartX=tx; g.dragStartY=ty; g.startPanX=pan.x; g.startPanY=pan.y; fadeHint(); } else { g.mode=null; g.active=false; } } }; const handleTouchMove = (ev) => { const g = gestureRef.current; if (!g.active) return; if (ev.cancelable) ev.preventDefault(); const touches = ev.touches; if (g.mode==='pinch' && touches.length===2) { const [a,b]=[touches[0],touches[1]]; const dx=b.clientX-a.clientX, dy=b.clientY-a.clientY; const newD=Math.hypot(dx,dy)||1; const ratio=newD/g.startD; const newZoom=Math.max(0.6, Math.min(4, g.startZoom*ratio)); const el=containerRef.current; const rect=el?el.getBoundingClientRect():{left:0,top:0,width:1,height:1}; const cx=(a.clientX+b.clientX)/2-rect.left; const cy=(a.clientY+b.clientY)/2-rect.top; const k=newZoom/g.startZoom; const offX=(cx-g.startMidX)+g.startPanX*k; const offY=(cy-g.startMidY)+g.startPanY*k; setZoom(newZoom); setPan(clampPan({x:offX,y:offY}, newZoom)); } else if (g.mode==='pan-touch' && touches.length===1) { const t0=touches[0]; const dx=t0.clientX-g.dragStartX; const dy=t0.clientY-g.dragStartY; setPan(clampPan({x:g.startPanX+dx, y:g.startPanY+dy}, zoom)); } }; const handleTouchEnd = (ev) => { const g = gestureRef.current; if (ev.touches.length===0) { g.active=false; g.mode=null; } else if (ev.touches.length===1 && g.mode==='pinch') { const t0=ev.touches[0]; g.mode = zoom>1?'pan-touch':null; g.active = g.mode==='pan-touch'; g.dragStartX=t0.clientX; g.dragStartY=t0.clientY; g.startPanX=pan.x; g.startPanY=pan.y; } }; const handleMouseDown = (ev) => { if (zoom<=1) return; const g=gestureRef.current; g.mode='pan-mouse'; g.active=true; g.dragStartX=ev.clientX; g.dragStartY=ev.clientY; g.startPanX=pan.x; g.startPanY=pan.y; fadeHint(); }; const handleMouseMove = (ev) => { const g=gestureRef.current; if (!g.active || g.mode!=='pan-mouse') return; const dx=ev.clientX-g.dragStartX, dy=ev.clientY-g.dragStartY; setPan(clampPan({x:g.startPanX+dx, y:g.startPanY+dy}, zoom)); }; const endMouse = () => { const g=gestureRef.current; if (g.mode==='pan-mouse'){ g.active=false; g.mode=null; } }; const handleWheel = (ev) => { if (ev.cancelable) ev.preventDefault(); fadeHint(); const el=containerRef.current; const rect=el?el.getBoundingClientRect():{left:0,top:0,width:1,height:1}; const cx=ev.clientX-rect.left-rect.width/2; const cy=ev.clientY-rect.top-rect.height/2; const delta=ev.deltaY>0?-0.1:0.1; const newZoom=Math.max(0.6, Math.min(4, zoom*(1+delta))); const k=newZoom/zoom; setZoom(newZoom); setPan(clampPan({x:cx-(cx-pan.x)*k, y:cy-(cy-pan.y)*k}, newZoom)); }; React.useEffect(() => { const el=containerRef.current; if (!el) return; const tm=(ev)=>handleTouchMove(ev); const wh=(ev)=>handleWheel(ev); el.addEventListener('touchmove', tm, {passive:false}); el.addEventListener('wheel', wh, {passive:false}); return () => { el.removeEventListener('touchmove', tm); el.removeEventListener('wheel', wh); }; }, [zoom, pan.x, pan.y, hintFaded]); // ── guard: missing unit ────────────────────────────────────── if (!unit) { return (
); } const st = STATUS[unit.status] || STATUS.sold; const flLabel = floorLabel(tower, floor); const sold = unit.status === 'sold'; const sheet = unitPriceSheet(unit); return (
{/* ── HEADER (back → floor//) ──────────────── */}
navigate('inventory')}, {label:tower.name, go:()=>navigate(`floors/${towerId}`)}, {label:flLabel, go:()=>navigate(`floor/${towerId}/${floor}`)}, {label:unit.no}, ]}/>
{unit.no}
{unit.isPenthouse ? 'Signature Penthouse' : '4 BHK home'}
navigate('home')} onKeyDown={ev=>{ if(ev.key==='Enter'||ev.key===' '){ ev.preventDefault(); navigate('home'); } }} onMouseEnter={ev=>{ ev.currentTarget.style.opacity='0.6'; ev.currentTarget.style.transform='scale(1.03)'; }} onMouseLeave={ev=>{ ev.currentTarget.style.opacity='1'; ev.currentTarget.style.transform='scale(1)'; }} style={{display:'flex', alignItems:'center', gap:22, cursor:'pointer', transformOrigin:'right center', transition:'opacity 200ms ease, transform 200ms ease'}}>
{/* ── BODY: plan viewer (left) + details (right) ─────────── */}
{/* PLAN VIEWER */}
1 ? (gestureRef.current.mode==='pan-mouse'?'grabbing':'grab') : 'default'}}> {`${unit.no}{ ev.currentTarget.style.display='none'; }}/>
{/* zoom controls */}
{Math.round(zoom*100)}%
{/* gesture hint */}
PINCH OR SCROLL TO ZOOM · DRAG TO PAN
{/* block tag */}
N BLOCK {unit.pair} · {unit.type.toUpperCase()}
{/* DETAILS PANEL */}
{/* status + headline */}
{st.label.toUpperCase()}
{tower.name.toUpperCase()} · {flLabel.toUpperCase()}
{unit.isPenthouse ? 'Signature Penthouse' : 'A 4 BHK home'}
{tower.cluster} · {unit.facing}
{/* spec grid */}
{/* PRICE SHEET */}
INDICATIVE COST SHEET {formatINR(sheet.rate)}/SQ.FT
ALL-IN TOTAL
INDICATIVE · TAXES INCLUDED
{formatINR(sheet.total)}
{/* ACTIONS — pinned to the panel foot so the column fills the taller canvas */}
); } function DetailCell({ label, value, dens = 0 }) { return (
{label.toUpperCase()}
{value}
); } function PriceRow({ label, sub, value, strong, dens = 0 }) { return (
{label}
{sub &&
{sub}
}
{value}
); } const udZoomBtn = { width:57, height:57, borderRadius:'50%', background:'rgba(20,16,11,0.88)', border:'1px solid rgba(232,215,168,0.30)', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color:'rgba(232,215,168,0.95)', }; window.UnitDetail = UnitDetail;