booking-section
FC-0002booking-flowblockv1.0.0Receptuur: kalender + aanvraag-formulier met gedeelde periodeselectie in twee richtingen (kalenderklik vult de velden, getypte datum kleurt de kalender).
Beschikbaarheid
september 2026
oktober 2026
Geselecteerd In behandeling14 Niet beschikbaar
blocks/booking/booking-section.tsx
/**
* BookingSection — de eerste composes-receptuur (V5): kalender + aanvraag-blok
* met gedeelde periodeselectie in twee richtingen. Een klik in de kalender vult
* de datumvelden; een getypte datum kleurt de kalender.
*/
"use client";
import { useState } from "react";
import type * as React from "react";
import { DateRangeCalendar, type DateRange, type UnavailablePeriod } from "../../primitives/ui/date-range-calendar";
import { BookingInquiry, type BookingInquiryProps } from "./booking-inquiry";
export interface BookingSectionProps
extends Omit<BookingInquiryProps, "dates" | "onDatesChange"> {
unavailable?: UnavailablePeriod[];
/** De gekozen periode raakt een onbeschikbare dag — het project kiest de melding. */
onInvalidRange?: () => void;
calendarTitle?: string;
locale?: string;
/** Voetnoot onder de kalender-legenda (bv. een schoonmaak-bufferregel). */
calendarFootnote?: React.ReactNode;
}
export function BookingSection({
unavailable = [],
onInvalidRange,
calendarTitle,
locale,
calendarFootnote,
...inquiry
}: BookingSectionProps) {
const [dates, setDates] = useState<DateRange>({ start: null, end: null });
return (
<div className="fp-booking-section">
<DateRangeCalendar
unavailable={unavailable}
value={dates}
onChange={({ start, end }) => setDates({ start, end })}
onInvalidRange={onInvalidRange}
title={calendarTitle}
locale={locale}
>
{calendarFootnote}
</DateRangeCalendar>
<BookingInquiry {...inquiry} dates={dates} onDatesChange={setDates} />
</div>
);
}
blocks/booking/booking.css
/**
* Booking-blocks — stylesheet. Uitsluitend contract-tokens (tokens/CONTRACT.md).
* Vereist primitives/ui/primitives.css + tokens/motion.css ernaast.
*/
.fp-booking {
display: flex; flex-direction: column; gap: 1rem;
background: var(--card);
color: var(--card-foreground);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.375rem;
}
.fp-booking__head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.fp-booking__title { margin: 0; font-size: 0.9375rem; font-weight: 600; }
.fp-booking__price { margin: 0; font-size: 0.8125rem; color: var(--muted-foreground); }
.fp-booking__price strong { font-size: 1.25rem; font-weight: 600; color: var(--foreground); }
.fp-booking__dates { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
@media (max-width: 420px) { .fp-booking__dates { grid-template-columns: 1fr; } }
.fp-booking__summary {
margin: 0; display: flex; flex-direction: column; gap: 0.5rem;
background: var(--muted); border-radius: var(--radius);
padding: 0.875rem 1rem; font-size: 0.8125rem;
}
.fp-booking__summary > div { display: flex; justify-content: space-between; gap: 1rem; }
.fp-booking__summary dt { color: var(--muted-foreground); }
.fp-booking__summary dd { margin: 0; font-variant-numeric: tabular-nums; }
.fp-booking__total {
padding-top: 0.5rem; border-top: 1px solid var(--border); font-weight: 600;
}
.fp-booking__total dt { color: var(--foreground); }
.fp-booking__guest { display: flex; flex-direction: column; gap: 0.75rem; padding-top: 0.25rem; }
.fp-booking__error { margin: 0; font-size: 0.8125rem; color: var(--danger); }
.fp-booking__submit { width: 100%; }
.fp-booking__footnote { font-size: 0.75rem; color: var(--muted-foreground); text-align: center; }
.fp-booking--success { text-align: center; }
.fp-booking--success .fp-booking__title { color: var(--success); }
.fp-booking__text { margin: 0; font-size: 0.875rem; color: var(--muted-foreground); }
/* ── Sectie-receptuur: kalender + formulier ─────────────────────────────── */
.fp-booking-section {
display: grid; grid-template-columns: 3fr 2fr; gap: 1rem; align-items: start;
}
@media (max-width: 860px) { .fp-booking-section { grid-template-columns: 1fr; } }