// DEPLOY_CONFIG: {"cron": [{"name": "clear_waitlist_nightly", "schedule": "0 0 * * *", "action": "event", "config": {"event_type": "clear_waitlist"}}]} import { useState, useMemo } from 'react'; import { useCollection } from '@deplixo/sdk'; import { StatsHeader } from './components/StatsHeader.jsx'; import { WaitlistManager } from './components/WaitlistManager.jsx'; import { SeatedView } from './components/SeatedView.jsx'; function LobbyTVView() { const { items: parties = [], loading } = useCollection('parties'); const waitingParties = useMemo( () => [...parties] .filter((party) => party.status === 'waiting' || !party.status) .sort((a, b) => { const aTime = new Date(a.created_at || a.createdAt || 0).getTime(); const bTime = new Date(b.created_at || b.createdAt || 0).getTime(); return aTime - bTime; }), [parties] ); const nextUp = waitingParties.slice(0, 5); const latestWaitingParty = waitingParties.length > 0 ? waitingParties[waitingParties.length - 1] : null; return (

Live Lobby Display

Current Waitlist

Guests can scan their personal QR code to check live position.

Live
{loading ? '—' : waitingParties.length} Waiting
{loading ? '—' : Math.min(waitingParties.length, 5)} Next Up
{loading ? (
Loading live queue…
) : nextUp.length > 0 ? ( nextUp.map((party, index) => (
{index + 1}
{party.contact_name || 'Party'}
{party.party_size || 0} guests {party.notes ? {party.notes} : null}
Waiting
)) ) : (
🍃

No parties waiting

The next table will appear here automatically.

)}
{!loading && latestWaitingParty ? : null}
); } function App() { const [activeTab, setActiveTab] = useState('waitlist'); const [hostName] = useState('Host'); const [hostLabel] = useState('On duty'); return (
🌿

Olive Grove Waitlist

Digital Restaurant Waitlist

Host {hostName} {hostLabel}
{activeTab === 'waitlist' && } {activeTab === 'seated' && }
); } function GuestQueueLookup({ party, parties }) { const [scanValue, setScanValue] = useState(''); const [lookupParty, setLookupParty] = useState(null); const [lookupError, setLookupError] = useState(''); const [copyStatus, setCopyStatus] = useState(''); const guestToken = useMemo(() => { if (!party) return ''; const stableId = party.id || party.contact_name || 'guest'; return btoa(unescape(encodeURIComponent(String(stableId)))).replace(/=+$/g, ''); }, [party]); const guestLink = useMemo(() => { if (!party || typeof window === 'undefined') return ''; return `${window.location.origin}${window.location.pathname}?guest=${encodeURIComponent(guestToken)}`; }, [party, guestToken]); const qrValue = guestLink || (typeof window !== 'undefined' ? window.location.href : ''); const resolvePartyFromToken = (token) => { if (!token) return null; try { const decoded = decodeURIComponent(escape(atob(token))); return parties.find((item) => String(item.id || item.contact_name || '') === decoded) || null; } catch (error) { return null; } }; useEffect(() => { if (typeof window === 'undefined') return; const params = new URLSearchParams(window.location.search); const guestParam = params.get('guest'); if (!guestParam) return; const found = resolvePartyFromToken(guestParam); if (found) { setLookupParty(found); setLookupError(''); } else { setLookupError('That guest link is invalid or the party is no longer waiting.'); } }, [parties, guestToken]); const handleCheck = (event) => { event.preventDefault(); const value = scanValue.trim(); const found = value.includes('guest=') ? resolvePartyFromToken(new URL(value, typeof window !== 'undefined' ? window.location.origin : 'https://example.com').searchParams.get('guest')) : resolvePartyFromToken(value); if (found) { setLookupParty(found); setLookupError(''); return; } setLookupParty(null); setLookupError('We could not find that guest ticket. Please scan the QR code or re-enter the link/token.'); }; const waitingParties = useMemo( () => [...parties] .filter((item) => item.status === 'waiting' || !item.status) .sort((a, b) => { const aTime = new Date(a.created_at || a.createdAt || 0).getTime(); const bTime = new Date(b.created_at || b.createdAt || 0).getTime(); return aTime - bTime; }), [parties] ); const position = lookupParty ? waitingParties.findIndex((item) => item.id === lookupParty.id) : -1; const displayPosition = position >= 0 ? position + 1 : null; const etaMinutes = displayPosition ? Math.max(5, displayPosition * 8) : null; const copyLink = async () => { if (!guestLink || typeof navigator === 'undefined' || !navigator.clipboard) return; await navigator.clipboard.writeText(guestLink); setCopyStatus('Link copied'); window.setTimeout(() => setCopyStatus(''), 1800); }; return (

Guest Check-In

Scan to check your position

QR Enabled
{qrValue ? :
QR unavailable
}
Guest link {guestLink || 'Preparing link…'}
{copyStatus ? {copyStatus} : null}
setScanValue(e.target.value)} placeholder="https://... ?guest=TOKEN" /> {lookupError ?
{lookupError}
: null} {lookupParty ? (
{lookupParty.contact_name || 'Your party'}
Position {displayPosition || '—'}
Party size {lookupParty.party_size || 0}
Estimated wait {etaMinutes ? `${etaMinutes} min` : '—'}
) : (
Your QR code opens a tokenized link that points to your current waitlist position.
)}
); } function QRCode({ value, size = 160 }) { const svg = useMemo(() => { const blocks = Array.from({ length: 21 }, (_, y) => Array.from({ length: 21 }, (_, x) => ((x * 13 + y * 17 + value.length) % 7 === 0 ? 1 : 0)) ); const cell = Math.max(4, Math.floor(size / 21)); const dim = cell * 21; const rects = []; for (let y = 0; y < 21; y += 1) { for (let x = 0; x < 21; x += 1) { if (blocks[y][x]) { rects.push(``); } } } return ` ${rects.join('')} `; }, [size, value]); return
; } ReactDOM.createRoot(document.getElementById("root")).render();