← Files WixARCHIVED FILE
skills/wix-headless-templates/restaurants/app/components/restaurants/ReservationView.tsx
9.01 KB · Oct 8, 2026 · 12:02 UTC
// REFERENCE reservations surface: party/date/time query → AVAILABLE slot pills → hold (or, under
// manual approval, a plain pick) → the details form built from the location's form configuration →
// confirmed / pending, on the @theme tokens. Correct and complete; per the skill's model you design
// and build your own on useReservation (which owns ALL booking logic).
import { useReservation } from "../../hooks/restaurants/useReservation";
import { zonedDateTimeLabel } from "../../wix/restaurants/time-core";
const input =
"w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus:border-primary";
function Policy({ label, policy }: { label: string; policy: { url: string; text: string } }) {
return policy.url ? (
<a href={policy.url} target="_blank" rel="noreferrer" className="underline">{label}</a>
) : (
<details className="inline-block">
<summary className="cursor-pointer underline">{label}</summary>
<p className="mt-1 whitespace-pre-wrap text-xs text-muted-foreground">{policy.text}</p>
</details>
);
}
export default function ReservationView() {
const r = useReservation();
if (r.locations === null) {
return <div className="h-64 animate-pulse rounded-lg bg-secondary" aria-busy="true" />;
}
if (r.locations.length === 0 || !r.location) {
return (
<p className="py-16 text-center text-muted-foreground">
Table reservations aren't set up yet — call us to book.
</p>
);
}
if (!r.location.onlineReservationsEnabled) {
// Premium-gated toggle is off — slots/booking would fail; be honest, don't fake a form.
return (
<p className="py-16 text-center text-muted-foreground">
Online reservations aren't open yet — call us to book a table.
</p>
);
}
if (r.confirmed) {
const { outcome } = r.confirmed;
return (
<div className="mx-auto max-w-md rounded-lg border border-border p-8 text-center">
<p className="text-lg font-semibold">
{outcome === "confirmed" ? "Table reserved" : outcome === "pending" ? "Request sent" : "Reservation not completed"}
</p>
<p className="mt-2 text-sm text-muted-foreground">
{outcome === "confirmed"
? r.location.form.submitMessage || "See you soon — a confirmation is on its way."
: outcome === "pending"
? "The restaurant will confirm your reservation shortly."
: `Status: ${r.confirmed.status.toLowerCase().replace(/_/g, " ")} — please call us.`}
</p>
<button
type="button"
onClick={r.reset}
className="mt-6 rounded-control border border-border px-5 py-2 text-sm font-medium hover:bg-secondary"
>
Make another reservation
</button>
</div>
);
}
const form = r.location.form;
const picked = r.selectedSlot;
const holdMinutes = r.holdSecondsLeft !== null ? `${Math.floor(r.holdSecondsLeft / 60)}:${String(r.holdSecondsLeft % 60).padStart(2, "0")}` : "";
return (
<div className="mx-auto max-w-xl">
{r.locations.length > 1 && (
<label className="mb-4 block text-sm">
<span className="text-muted-foreground">Location</span>
<select value={r.location.id} onChange={(e) => r.setLocationId(e.target.value)} className={`mt-1 ${input}`}>
{r.locations.map((l) => (
<option key={l.id} value={l.id}>{l.name || l.address || "Restaurant"}</option>
))}
</select>
</label>
)}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<label className="block text-sm">
<span className="text-muted-foreground">Guests</span>
<input
type="number"
min={r.location.partySizeMin}
max={r.location.partySizeMax}
value={r.partySize}
onChange={(e) => r.setPartySize(Number(e.target.value))}
className={`mt-1 ${input}`}
/>
</label>
<label className="block text-sm">
<span className="text-muted-foreground">Date</span>
<input type="date" value={r.date} onChange={(e) => r.setDate(e.target.value)} className={`mt-1 ${input}`} />
</label>
<label className="block text-sm">
<span className="text-muted-foreground">Around</span>
<input type="time" value={r.time} onChange={(e) => r.setTime(e.target.value)} className={`mt-1 ${input}`} />
</label>
</div>
{r.approval === "MANUAL" && (
<p className="mt-2 text-xs text-muted-foreground">Reservations for this party size are confirmed by the restaurant.</p>
)}
<button
type="button"
disabled={r.loading}
onClick={() => void r.findSlots()}
className="mt-4 w-full rounded-control bg-primary py-3 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50"
>
{r.loading && r.slots === null ? "Finding times…" : "Find a table"}
</button>
{r.error && <p className="mt-4 text-sm text-destructive">{r.error}</p>}
{r.slots !== null && r.slots.length === 0 && !r.error && (
<p className="mt-6 text-center text-sm text-muted-foreground">
No tables around that time — try another time or party size.
</p>
)}
{r.slots !== null && r.slots.length > 0 && !picked && (
<div className="mt-6 flex flex-wrap gap-2" role="group" aria-label="Available times">
{r.slots.map((slot) => (
<button
key={slot.startIso}
type="button"
disabled={r.loading}
onClick={() => void r.holdSlot(slot)}
className="rounded-control border border-border px-4 py-1.5 text-sm font-medium transition-colors hover:bg-secondary disabled:opacity-40"
>
{slot.label}
</button>
))}
</div>
)}
{picked && (
<div className="mt-6 rounded-lg border border-border p-5">
<p className="text-sm font-medium">
{r.held ? "Holding" : "Requesting"} {zonedDateTimeLabel(new Date(picked.startIso), r.location.timeZone)} for {r.partySize}
{r.location.name ? ` at ${r.location.name}` : ""}
{r.held && holdMinutes ? ` — complete within ${holdMinutes}.` : r.held ? " — complete within 10 minutes." : "."}
</p>
<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
<input
placeholder="First name *"
value={r.reservee.firstName}
onChange={(e) => r.setReserveeField("firstName", e.target.value)}
className={input}
/>
<input
placeholder={form.lastNameRequired ? "Last name *" : "Last name"}
value={r.reservee.lastName ?? ""}
onChange={(e) => r.setReserveeField("lastName", e.target.value)}
className={input}
/>
<input
placeholder="Phone (+15551234567) *"
type="tel"
value={r.reservee.phone}
onChange={(e) => r.setReserveeField("phone", e.target.value)}
className={input}
/>
<input
placeholder={form.emailRequired ? "Email *" : "Email"}
type="email"
value={r.reservee.email ?? ""}
onChange={(e) => r.setReserveeField("email", e.target.value)}
className={input}
/>
{form.customFields.map((f) => (
<input
key={f.id}
placeholder={f.required ? `${f.name} *` : f.name}
value={r.reservee.customFields?.[f.id] ?? ""}
onChange={(e) => r.setCustomField(f.id, e.target.value)}
className={`${input} sm:col-span-2`}
/>
))}
</div>
{form.marketingCheckbox && (
<label className="mt-3 flex items-center gap-2 text-sm">
<input type="checkbox" checked={r.reservee.marketingConsent === true} onChange={(e) => r.setMarketingConsent(e.target.checked)} />
Send me news and offers by email
</label>
)}
{(form.terms || form.privacy) && (
<p className="mt-3 text-xs text-muted-foreground">
By reserving you agree to our{" "}
{form.terms && <Policy label="terms and conditions" policy={form.terms} />}
{form.terms && form.privacy && " and "}
{form.privacy && <Policy label="privacy policy" policy={form.privacy} />}.
</p>
)}
<button
type="button"
disabled={!r.canConfirm}
onClick={() => void r.confirm()}
className="mt-4 w-full rounded-control bg-primary py-3 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50"
>
{r.loading ? "Sending…" : r.held ? "Complete reservation" : "Request reservation"}
</button>
</div>
)}
</div>
);
}
SHA-256: 2c2bb0ddb975fc3d2295602293d36c01d6b5bedfeff5fb1051feade90fce6b2e