/* global React, AppShell, Eyebrow, foyerApi, foyerLoad, useFoyerData */ // Two surfaces: // /#/kiosk → agent-facing preview + "Launch kiosk" button. // /#/kiosk-live → guest-facing form, no sidebar, no nav, hardened // against back-button + hash navigation, designed to // be handed to a walk-in guest in a separate tab. // ============================================================ // /#/kiosk — agent-facing setup // ============================================================ const KioskForm = () => { const { user, summaries } = useFoyerData(); const recentAddress = pickRecentAddress(summaries); const launch = () => { const url = '/#/kiosk-live'; // Open in a new tab so the agent's dashboard stays put. The guest's // tab is fully sandboxed by the live route's hash trap. window.open(url, '_blank', 'noopener'); }; return (
Hand to a guest

Kiosk sign-in.

Launches a locked-down, full-screen sign-in in a new tab. Guests can fill in their info but can't navigate back to your dashboard — no sidebar, no back button, no link out.

Address shown to guests
{recentAddress || No recent session — guests will see a generic welcome.}

Pulled from your most recent recorded session. To change it, start a new session at a different address from the iOS app first.

Preview only
); }; function Tip({ num, title, body }) { return (
{num}
{title}
{body}
); } function pickRecentAddress(summaries) { const recorded = (summaries || []) .filter(s => (s.kind || 'recorded') !== 'manual') .sort((a, b) => (b.created_at || '').localeCompare(a.created_at || '')); return recorded[0]?.address || ''; } // ============================================================ // /#/kiosk-live — guest-facing, locked-down full-screen form // ============================================================ // // Hardened so a walk-in can't escape: // - No AppShell sidebar, no route-nav, no Open House Copilot-crest link out // - popstate listener pushes state forward whenever back is pressed // - hashchange listener forces the hash back to #/kiosk-live // - No internal links other than the Sign in button // // The agent's session cookie still rides along automatically (same origin), // so leads land under their account. const KioskLive = () => { const { user, summaries } = useFoyerData(); const recentAddress = pickRecentAddress(summaries); // Trap back-button + URL hash changes so the guest can't navigate out. React.useEffect(() => { if (typeof window === 'undefined') return; // Push an extra entry so the first Back press just re-fires popstate // here instead of leaving the page. try { window.history.pushState({ kiosk: true }, '', '#/kiosk-live'); } catch (e) {} const onPopState = () => { try { window.history.pushState({ kiosk: true }, '', '#/kiosk-live'); } catch (e) {} }; const onHash = () => { if (window.location.hash !== '#/kiosk-live') { window.location.hash = '#/kiosk-live'; } }; window.addEventListener('popstate', onPopState); window.addEventListener('hashchange', onHash); return () => { window.removeEventListener('popstate', onPopState); window.removeEventListener('hashchange', onHash); }; }, []); const [name, setName] = React.useState(''); const [email, setEmail] = React.useState(''); const [phone, setPhone] = React.useState(''); const [tag, setTag] = React.useState('Buyer'); const [submitting, setSubmitting] = React.useState(false); const [thanksFor, setThanksFor] = React.useState(null); const [err, setErr] = React.useState(null); const [signedInCount, setSignedInCount] = React.useState(0); const reset = () => { setName(''); setEmail(''); setPhone(''); setTag('Buyer'); setErr(null); }; const submit = async (e) => { e?.preventDefault?.(); if (!name.trim()) { setErr('Name is required.'); return; } setSubmitting(true); setErr(null); try { await foyerApi.post('/leads', { name: name.trim(), email: email.trim(), phone: phone.trim(), tag, address: recentAddress || undefined, }); // Don't bother refreshing foyerLoad here — this tab won't show // the inbox anyway, and refreshing would slow each sign-in down. const first = name.trim().split(' ')[0]; setThanksFor({ name: first, at: Date.now() }); setSignedInCount(c => c + 1); reset(); setTimeout(() => setThanksFor(null), 2200); } catch (e2) { setErr(e2.message || String(e2)); } finally { setSubmitting(false); } }; return (
{/* Decorative gold radial behind the form. */} ); }; function KioskField({ label, value, onChange, placeholder, type = 'text', autoFocus = false, required = false }) { return (
{label}
onChange(e.target.value)} placeholder={placeholder} autoComplete="off" style={{ width: '100%', background: 'transparent', border: 0, borderBottom: '1px solid var(--border-strong)', padding: '12px 0', color: 'var(--cream)', fontSize: 20, fontFamily: 'var(--sans)', outline: 'none', }} />
); } Object.assign(window, { KioskForm, KioskLive });