QueueDisplay
A customer-facing component for displaying the current queue on screens or wall-mounted displays. Automatically refreshes at a configurable interval, shows who is being served now, upcoming positions, and estimated wait times. Designed for visibility in reception areas.
Install
Section titled “Install”npx thebookingkit add queue-displayimport { QueueDisplay } from "@/components/queue-display";import { useEffect, useState } from "react";
export function PublicQueueScreen() { const [entries, setEntries] = useState([]);
const fetchQueueEntries = async () => { const response = await fetch("/api/queue/public"); const data = await response.json(); return data.entries; };
useEffect(() => { fetchQueueEntries(); }, []);
return ( <QueueDisplay entries={entries} isAccepting={true} providerName="Main Reception" onRefresh={fetchQueueEntries} refreshInterval={15} /> );}export interface QueueDisplayProps { /** Current queue entries */ entries: QueueDisplayEntry[]; /** Whether the provider is accepting walk-ins */ isAccepting: boolean; /** Provider display name */ providerName?: string; /** Callback to refresh queue data */ onRefresh?: () => Promise<QueueDisplayEntry[]>; /** Auto-refresh interval in seconds (default: 30) */ refreshInterval?: number; /** Additional CSS class name */ className?: string; /** Inline styles */ style?: React.CSSProperties;}
export interface QueueDisplayEntry { /** Queue entry ID */ id: string; /** Queue position (1-based) */ position: number; /** Customer first name (last name hidden for privacy) */ customerFirstName: string; /** Service name */ serviceName: string; /** Estimated wait in minutes */ estimatedWaitMinutes: number; /** Current status */ status: "queued" | "in_service";}Source
Section titled “Source”import React, { useEffect, useState } from "react";import { cn } from "../utils/cn.js";
/** A queue entry for display */export interface QueueDisplayEntry { /** Queue entry ID */ id: string; /** Queue position (1-based) */ position: number; /** Customer first name (last name hidden for privacy) */ customerFirstName: string; /** Service name */ serviceName: string; /** Estimated wait in minutes */ estimatedWaitMinutes: number; /** Current status */ status: "queued" | "in_service";}
/** Props for the QueueDisplay component */export interface QueueDisplayProps { /** Current queue entries */ entries: QueueDisplayEntry[]; /** Whether the provider is accepting walk-ins */ isAccepting: boolean; /** Provider display name */ providerName?: string; /** Callback to refresh queue data */ onRefresh?: () => Promise<QueueDisplayEntry[]>; /** Auto-refresh interval in seconds (default: 30) */ refreshInterval?: number; /** Additional CSS class name */ className?: string; /** Inline styles */ style?: React.CSSProperties;}
/** * Public-facing auto-refreshing queue display. * * Shows current queue with positions, service types, and estimated wait times. * Designed for wall-mounted displays or customer-facing screens. * * @example * ```tsx * <QueueDisplay * entries={queueEntries} * isAccepting={true} * providerName="Downtown Barber Shop" * onRefresh={fetchQueueEntries} * refreshInterval={15} * /> * ``` */export function QueueDisplay({ entries: initialEntries, isAccepting, providerName, onRefresh, refreshInterval = 30, className, style,}: QueueDisplayProps) { const [entries, setEntries] = useState(initialEntries); const [lastUpdated, setLastUpdated] = useState(new Date());
// Sync with prop changes useEffect(() => { setEntries(initialEntries); }, [initialEntries]);
// Auto-refresh useEffect(() => { if (!onRefresh || refreshInterval <= 0) return;
const interval = setInterval(async () => { try { const updated = await onRefresh(); setEntries(updated); setLastUpdated(new Date()); } catch { // Silently fail on refresh errors } }, refreshInterval * 1000);
return () => clearInterval(interval); }, [onRefresh, refreshInterval]);
const queuedEntries = entries.filter((e) => e.status === "queued"); const inService = entries.find((e) => e.status === "in_service");
return ( <div className={cn("tbk-queue-display", className)} style={style}> {/* Header */} <div className="tbk-queue-header"> {providerName && ( <h2 className="tbk-queue-title">{providerName}</h2> )} <div className={cn( "tbk-queue-status-indicator", isAccepting ? "tbk-queue-accepting" : "tbk-queue-not-accepting", )} > <span className="tbk-queue-status-dot" aria-hidden="true" /> {isAccepting ? "Accepting Walk-Ins" : "Not Accepting Walk-Ins"} </div> </div>
{/* Now Serving */} {inService && ( <div className="tbk-queue-now-serving"> <span className="tbk-queue-now-label">Now Serving</span> <span className="tbk-queue-now-name"> {inService.customerFirstName} </span> <span className="tbk-queue-now-service"> {inService.serviceName} </span> </div> )}
{/* Queue List */} {queuedEntries.length > 0 ? ( <div className="tbk-queue-list" role="list"> <div className="tbk-queue-list-header"> <span>#</span> <span>Customer</span> <span>Service</span> <span>Est. Wait</span> </div> {queuedEntries.map((entry) => ( <div key={entry.id} className={cn( "tbk-queue-list-row", entry.estimatedWaitMinutes > 60 && "tbk-queue-overdue", )} role="listitem" > <span className="tbk-queue-position">{entry.position}</span> <span className="tbk-queue-name"> {entry.customerFirstName} </span> <span className="tbk-queue-service"> {entry.serviceName} </span> <span className="tbk-queue-wait"> ~{entry.estimatedWaitMinutes} min </span> </div> ))} </div> ) : ( <div className="tbk-queue-empty"> {isAccepting ? "No one in the queue — walk right in!" : "Queue is currently closed."} </div> )}
{/* Footer */} <div className="tbk-queue-footer"> <span className="tbk-queue-count"> {queuedEntries.length} in queue </span> <span className="tbk-queue-updated"> Updated {lastUpdated.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", })} </span> </div> </div> );}