// 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 (
{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}
);
}
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 `
`;
}, [size, value]);
return ;
}
ReactDOM.createRoot(document.getElementById("root")).render();