Zum Inhalt springen
UI Kit
Accent

Buchungsanfrage

Anfragen für Buchungen, die ein Mensch bestätigt, anders als sofort gebuchte Termine. Ein Raum für eine Feier mit Kapazität live, ein Ferienhaus mit festen oder flexiblen Daten, ein Paket mit mitlaufender Summe, ein Workshop an einem von drei Terminen.

Eine von 4 Components in Anfrageformulare

4 Variants

Variants

Location für Feiern

Art der Feier, Datum, Gäste und Extras. Neben dem Formular antworten die Räume live, ob sie passen, jeder Balken ist so lang wie der Raum groß ist und füllt sich mit den Gästen.

Im Studio öffnen

Flexible Daten

Feste Daten im Kalender über zwei Monate mit gezählten Nächten, oder flexibel nach Monaten und Dauer. Ein weiteres Kind öffnet ein Feld für sein Alter, jedes steigt auf.

Im Studio öffnen

Pakete

Paketkarten steigen nacheinander auf, die gewählte trägt einen Akzentring. Extras zeigen ihren Preis, und eine Summe neben dem Formular zählt mit jeder Wahl und jeder Person mit.

Im Studio öffnen

Wunschtermine

Bis zu drei Wunschtermine auf einer nummerierten Schiene, jeder gefüllte wird zum Haken. Dann der Ort, die Gruppengröße und die Firma für die Rechnung.

Im Studio öffnen

Details

Was Buchungsanfrage mitbringt, aus Registry und Quellcode gelesen.

Bereich
Content
Variants
4
Version
1.0.0 · seit 1.1.0
Datei
components/ui/booking-request.tsx
Bringt mit
utilskitbuttoniconrevealsectionmotionform-flowinputtextareanumber-inputswitchdate-input
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 57 eigene Texte

Props

Die Schnittstelle BookingRequestProps aus components/ui/booking-request.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • variantoptional
    BookingRequestVariant

    event-venueflex-datespackagesdate-options

  • eyebrowoptional
    React.ReactNode
  • titleoptional
    React.ReactNode
  • descriptionoptional
    React.ReactNode
  • kindsoptional
    { value: string; label: string; icon?: IconName }[]

    event-venue: kinds of event to pick

  • spacesoptional
    BookingSpace[]

    event-venue: the rooms and their capacity

  • packagesoptional
    BookingPackage[]

    packages: the packages

  • extrasoptional
    BookingExtra[]

    event-venue, packages: extras to add

  • monthsoptional
    string[]

    flex-dates: the months to offer for flexible dates

  • formatsoptional
    { value: string; label: string; icon?: IconName }[]

    date-options: formats, e.g. at our place, at yours, online

  • currencyoptional
    string

    Currency sign, e.g. € or $

  • todayoptional
    string

    The day the calendar treats as today, ISO, for stable server rendering

  • guestsoptional
    number

    Guests or people to start with

  • minGuestsoptional
    number
  • maxGuestsoptional
    number
  • stepsoptional
    DoneStep[]
  • submitLabeloptional
    string
  • doneoptional
    { title: string; text?: string }
  • onSubmitoptional
    (data: Record<string, unknown>) => void

Eingebaute Texte

Diese Texte schreibt Buchungsanfrage selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 57 Texte anzeigen
kindEnglisch What are you celebrating?Deutsch Was feierst du?
kindNeededEnglisch Please choose the kind of event, it decides the setup of the room.Deutsch Bitte wähle die Art der Feier, sie bestimmt den Aufbau des Raums.
dateEnglisch DateDeutsch Datum
dateNeededEnglisch Please choose a date, so we can check the rooms for you.Deutsch Bitte wähle ein Datum, damit wir die Räume für dich prüfen können.
flexibleEnglisch My date is flexibleDeutsch Mein Datum ist flexibel
flexibleTextEnglisch We suggest nearby dates if this one is takenDeutsch Ist es belegt, schlagen wir Tage in der Nähe vor
guestsEnglisch GuestsDeutsch Gäste
roomsEnglisch Rooms for {n} guestsDeutsch Räume für {n} Gäste
fitsEnglisch FitsDeutsch Passt
tooSmallEnglisch Too smallDeutsch Zu klein
upToEnglisch up to {n}Deutsch bis {n}
extrasEnglisch Add to itDeutsch Dazu buchen
nameEnglisch NameDeutsch Name
nameNeededEnglisch Please tell us your name so we know who we write to.Deutsch Bitte nenne uns deinen Namen, damit wir wissen, wem wir schreiben.
emailEnglisch EmailDeutsch E-Mail
emailNeededEnglisch This address looks incomplete. Please use a form like name@example.com so our offer reaches you.Deutsch Diese Adresse sieht unvollständig aus. Bitte nutze die Form name@beispiel.de, damit unser Angebot dich erreicht.
messageEnglisch Anything else we should know?Deutsch Sollen wir noch etwas wissen?
sendEnglisch Send requestDeutsch Anfrage senden
whenEnglisch When do you want to come?Deutsch Wann möchtest du kommen?
exactEnglisch Exact datesDeutsch Feste Daten
flexEnglisch I am flexibleDeutsch Ich bin flexibel
arrivalEnglisch ArrivalDeutsch Anreise
departureEnglisch DepartureDeutsch Abreise
datesNeededEnglisch Please choose arrival and departure, so we can check the house for you.Deutsch Bitte wähle Anreise und Abreise, damit wir das Haus für dich prüfen können.
nightsEnglisch {n} nightsDeutsch {n} Nächte
notChosenEnglisch Not chosen yetDeutsch Noch offen
fromPriceEnglisch from {p}Deutsch ab {p}
pickArrivalEnglisch Choose your arrivalDeutsch Wähle deine Anreise
pickDepartureEnglisch Now choose your departureDeutsch Jetzt die Abreise wählen
monthEnglisch Which month?Deutsch Welcher Monat?
monthNeededEnglisch Please choose at least one month, so we can suggest dates.Deutsch Bitte wähle mindestens einen Monat, damit wir Termine vorschlagen können.
lengthEnglisch How long?Deutsch Wie lange?
adultsEnglisch AdultsDeutsch Erwachsene
childrenEnglisch ChildrenDeutsch Kinder
childAgeEnglisch Age of child {n}Deutsch Alter von Kind {n}
childAgeHintEnglisch For beds and pricesDeutsch Für Betten und Preise
packageEnglisch PackageDeutsch Paket
packageNeededEnglisch Please choose a package, so we can prepare the right offer.Deutsch Bitte wähle ein Paket, damit wir das passende Angebot vorbereiten.
peopleEnglisch PeopleDeutsch Personen
perPersonEnglisch per personDeutsch pro Person
totalEnglisch Estimated totalDeutsch Voraussichtlich gesamt
totalNoteEnglisch Final price in the offer, nothing is charged nowDeutsch Endpreis im Angebot, jetzt wird nichts berechnet
includesEnglisch IncludesDeutsch Enthalten
preferredEnglisch Preferred datesDeutsch Wunschtermine
preferredHintEnglisch Up to three, we confirm the first one that worksDeutsch Bis zu drei, wir bestätigen den ersten, der passt
optionEnglisch Option {n}Deutsch Termin {n}
addDateEnglisch Add another dateDeutsch Weiteren Termin hinzufügen
removeDateEnglisch Remove option {n}Deutsch Termin {n} entfernen
optionsNeededEnglisch Please choose at least one date, so we can check the trainer.Deutsch Bitte wähle mindestens einen Termin, damit wir die Trainerin einplanen können.
formatEnglisch Where should it happen?Deutsch Wo soll es stattfinden?
formatNeededEnglisch Please choose where it happens, it changes travel and price.Deutsch Bitte wähle den Ort, er ändert Anreise und Preis.
participantsEnglisch ParticipantsDeutsch Teilnehmende
companyEnglisch CompanyDeutsch Firma
companyNeededEnglisch Please add your company, the invoice goes to it.Deutsch Bitte gib deine Firma an, die Rechnung geht an sie.
doneTitleEnglisch Thank you, your request is with usDeutsch Danke, deine Anfrage ist bei uns
doneTextEnglisch We check availability and send you an offer, usually within one working day.Deutsch Wir prüfen die Verfügbarkeit und schicken dir ein Angebot, meist innerhalb eines Werktags.
againEnglisch Start a new requestDeutsch Neue Anfrage starten

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Setzt aria-describedby, aria-invalid, aria-label und aria-live, damit Screenreader Zweck und Zustand ansagen.
  • Nutzt die Rollen group und radiogroup.
  • Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
  • Jedes Feld hat ein Label, sichtbar oder für Screenreader.
  • Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
  • Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
  • Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 3 Stellen im Code schalten Bewegung dann ab.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 735 Zeilen
components/ui/booking-request.tsx
'use client';

import { useId, useRef, useState } from 'react';
import { cn, round, useLocale, useTexts } from '@/lib/kit';
import { Button } from '@/components/ui/button';
import { Icon, type IconName } from '@/components/ui/icon';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { NumberInput } from '@/components/ui/number-input';
import { Switch } from '@/components/ui/switch';
import { Calendar, DateInput, formatDate } from '@/components/ui/date-input';
import { Reveal } from '@/components/ui/reveal';
import { Section } from '@/components/ui/section';
import { Stagger } from '@/components/ui/motion';
import { ChipGroup, DonePanel, ErrorSummary, FlowHead, isEmail, Unfold, useFocusFirstError, type DoneStep, summaryOf, useFormState } from '@/components/ui/form-flow';

/*
 * A request to book something that needs a person to confirm: a room for an
 * event, a holiday home on flexible dates, a package with extras, a workshop
 * on one of several dates. Unlike instant booking, it collects what the host
 * needs to make an offer. Sends everything to `onSubmit`.
 *
 *   event-venue   kind of event, date, guests, and the rooms that fit fill in live
 *   flex-dates    exact dates on a calendar or a flexible week, guests with children's ages
 *   packages      a package, extras and people, the total counts along
 *   date-options  up to three proposed dates, the format and the group
 */

export type BookingRequestVariant = 'event-venue' | 'flex-dates' | 'packages' | 'date-options';

export interface BookingSpace {
  name: string;
  /** Most guests it holds */
  capacity: number;
  note?: string;
}

export interface BookingPackage {
  value: string;
  name: string;
  /** Price per person or per booking, in whole units */
  price: number;
  perPerson?: boolean;
  text?: string;
  includes?: string[];
}

export interface BookingExtra {
  value: string;
  label: string;
  price?: number;
  perPerson?: boolean;
  icon?: IconName;
}

export interface BookingRequestProps {
  variant?: BookingRequestVariant;
  eyebrow?: React.ReactNode;
  title?: React.ReactNode;
  description?: React.ReactNode;
  /** event-venue: kinds of event to pick */
  kinds?: { value: string; label: string; icon?: IconName }[];
  /** event-venue: the rooms and their capacity */
  spaces?: BookingSpace[];
  /** packages: the packages */
  packages?: BookingPackage[];
  /** event-venue, packages: extras to add */
  extras?: BookingExtra[];
  /** flex-dates: the months to offer for flexible dates */
  months?: string[];
  /** date-options: formats, e.g. at our place, at yours, online */
  formats?: { value: string; label: string; icon?: IconName }[];
  /** Currency sign, e.g. € or $ */
  currency?: string;
  /** The day the calendar treats as today, ISO, for stable server rendering */
  today?: string;
  /** Guests or people to start with */
  guests?: number;
  minGuests?: number;
  maxGuests?: number;
  steps?: DoneStep[];
  submitLabel?: string;
  done?: { title: string; text?: string };
  onSubmit?: (data: Record<string, unknown>) => void;
  labels?: Partial<Texts>;
  className?: string;
}

const TEXTS = {
  en: {
    kind: 'What are you celebrating?',
    kindNeeded: 'Please choose the kind of event, it decides the setup of the room.',
    date: 'Date',
    dateNeeded: 'Please choose a date, so we can check the rooms for you.',
    flexible: 'My date is flexible',
    flexibleText: 'We suggest nearby dates if this one is taken',
    guests: 'Guests',
    rooms: 'Rooms for {n} guests',
    fits: 'Fits',
    tooSmall: 'Too small',
    upTo: 'up to {n}',
    extras: 'Add to it',
    name: 'Name',
    nameNeeded: 'Please tell us your name so we know who we write to.',
    email: 'Email',
    emailNeeded: 'This address looks incomplete. Please use a form like name@example.com so our offer reaches you.',
    message: 'Anything else we should know?',
    send: 'Send request',
    when: 'When do you want to come?',
    exact: 'Exact dates',
    flex: 'I am flexible',
    arrival: 'Arrival',
    departure: 'Departure',
    datesNeeded: 'Please choose arrival and departure, so we can check the house for you.',
    nights: '{n} nights',
    notChosen: 'Not chosen yet',
    fromPrice: 'from {p}',
    pickArrival: 'Choose your arrival',
    pickDeparture: 'Now choose your departure',
    month: 'Which month?',
    monthNeeded: 'Please choose at least one month, so we can suggest dates.',
    length: 'How long?',
    adults: 'Adults',
    children: 'Children',
    childAge: 'Age of child {n}',
    childAgeHint: 'For beds and prices',
    package: 'Package',
    packageNeeded: 'Please choose a package, so we can prepare the right offer.',
    people: 'People',
    perPerson: 'per person',
    total: 'Estimated total',
    totalNote: 'Final price in the offer, nothing is charged now',
    includes: 'Includes',
    preferred: 'Preferred dates',
    preferredHint: 'Up to three, we confirm the first one that works',
    option: 'Option {n}',
    addDate: 'Add another date',
    removeDate: 'Remove option {n}',
    optionsNeeded: 'Please choose at least one date, so we can check the trainer.',
    format: 'Where should it happen?',
    formatNeeded: 'Please choose where it happens, it changes travel and price.',
    participants: 'Participants',
    company: 'Company',
    companyNeeded: 'Please add your company, the invoice goes to it.',
    doneTitle: 'Thank you, your request is with us',
    doneText: 'We check availability and send you an offer, usually within one working day.',
    again: 'Start a new request',
  },
  de: {
    kind: 'Was feierst du?',
    kindNeeded: 'Bitte wähle die Art der Feier, sie bestimmt den Aufbau des Raums.',
    date: 'Datum',
    dateNeeded: 'Bitte wähle ein Datum, damit wir die Räume für dich prüfen können.',
    flexible: 'Mein Datum ist flexibel',
    flexibleText: 'Ist es belegt, schlagen wir Tage in der Nähe vor',
    guests: 'Gäste',
    rooms: 'Räume für {n} Gäste',
    fits: 'Passt',
    tooSmall: 'Zu klein',
    upTo: 'bis {n}',
    extras: 'Dazu buchen',
    name: 'Name',
    nameNeeded: 'Bitte nenne uns deinen Namen, damit wir wissen, wem wir schreiben.',
    email: 'E-Mail',
    emailNeeded: 'Diese Adresse sieht unvollständig aus. Bitte nutze die Form name@beispiel.de, damit unser Angebot dich erreicht.',
    message: 'Sollen wir noch etwas wissen?',
    send: 'Anfrage senden',
    when: 'Wann möchtest du kommen?',
    exact: 'Feste Daten',
    flex: 'Ich bin flexibel',
    arrival: 'Anreise',
    departure: 'Abreise',
    datesNeeded: 'Bitte wähle Anreise und Abreise, damit wir das Haus für dich prüfen können.',
    nights: '{n} Nächte',
    notChosen: 'Noch offen',
    fromPrice: 'ab {p}',
    pickArrival: 'Wähle deine Anreise',
    pickDeparture: 'Jetzt die Abreise wählen',
    month: 'Welcher Monat?',
    monthNeeded: 'Bitte wähle mindestens einen Monat, damit wir Termine vorschlagen können.',
    length: 'Wie lange?',
    adults: 'Erwachsene',
    children: 'Kinder',
    childAge: 'Alter von Kind {n}',
    childAgeHint: 'Für Betten und Preise',
    package: 'Paket',
    packageNeeded: 'Bitte wähle ein Paket, damit wir das passende Angebot vorbereiten.',
    people: 'Personen',
    perPerson: 'pro Person',
    total: 'Voraussichtlich gesamt',
    totalNote: 'Endpreis im Angebot, jetzt wird nichts berechnet',
    includes: 'Enthalten',
    preferred: 'Wunschtermine',
    preferredHint: 'Bis zu drei, wir bestätigen den ersten, der passt',
    option: 'Termin {n}',
    addDate: 'Weiteren Termin hinzufügen',
    removeDate: 'Termin {n} entfernen',
    optionsNeeded: 'Bitte wähle mindestens einen Termin, damit wir die Trainerin einplanen können.',
    format: 'Wo soll es stattfinden?',
    formatNeeded: 'Bitte wähle den Ort, er ändert Anreise und Preis.',
    participants: 'Teilnehmende',
    company: 'Firma',
    companyNeeded: 'Bitte gib deine Firma an, die Rechnung geht an sie.',
    doneTitle: 'Danke, deine Anfrage ist bei uns',
    doneText: 'Wir prüfen die Verfügbarkeit und schicken dir ein Angebot, meist innerhalb eines Werktags.',
    again: 'Neue Anfrage starten',
  },
};

type Texts = (typeof TEXTS)['en'];

const money = (n: number, c: string, locale: 'en' | 'de') => (locale === 'de' ? `${n.toLocaleString('de-DE')} ${c}` : `${c}${n.toLocaleString('en-US')}`);

/** Name, email and an optional message, shared by every variant */
function Contact({ id, t, f, who, setWho, cols = 2 }: { id: string; t: Texts; f: ReturnType<typeof useFormState>; who: { name: string; email: string; message: string }; setWho: (w: { name: string; email: string; message: string }) => void; cols?: 1 | 2 }) {
  return (
    <div className={cn('grid grid-cols-1 gap-5', cols === 2 && 'sm:grid-cols-2')}>
      <Input id={`${id}-name`} label={t.name} required autoComplete="name" value={who.name} onChange={(e) => (setWho({ ...who, name: e.target.value }), f.clear('name'))} error={f.errors.name} />
      <Input id={`${id}-email`} type="email" label={t.email} required autoComplete="email" value={who.email} onChange={(e) => (setWho({ ...who, email: e.target.value }), f.clear('email'))} error={f.errors.email} />
      <Textarea label={t.message} optional value={who.message} onChange={(e) => setWho({ ...who, message: e.target.value })} minRows={2} maxLength={800} className={cols === 2 ? 'sm:col-span-2' : undefined} />
    </div>
  );
}

function Done({ p, t, onAgain, className }: { p: BookingRequestProps; t: Texts; onAgain: () => void; className?: string }) {
  return (
    <div className={cn('kit-card mx-auto p-6 sm:p-10', className)}>
      <DonePanel
        title={p.done?.title ?? t.doneTitle}
        text={p.done?.text ?? t.doneText}
        steps={p.steps}
        actions={
          <Button variant="outline" iconLeft="rotate-ccw" onClick={onAgain}>
            {t.again}
          </Button>
        }
      />
    </div>
  );
}

const contactCheck = (t: Texts, who: { name: string; email: string }, found: Record<string, string>) => {
  if (!who.name.trim()) found.name = t.nameNeeded;
  if (!isEmail(who.email)) found.email = t.emailNeeded;
  return found;
};

/* ------------------------------------------------------------------ */
/* event-venue                                                         */
/* ------------------------------------------------------------------ */

function EventVenue({ p, t }: { p: BookingRequestProps; t: Texts }) {
  const id = useId();
  const f = useFormState();
  const [kind, setKind] = useState<string[]>([]);
  const [date, setDate] = useState<string | null>(null);
  const [flexible, setFlexible] = useState(false);
  const [guests, setGuests] = useState(p.guests ?? 60);
  const [extras, setExtras] = useState<string[]>([]);
  const [who, setWho] = useState({ name: '', email: '', message: '' });
  const formRef = useRef<HTMLFormElement>(null);
  const summaryRef = useRef<HTMLDivElement>(null);
  useFocusFirstError(formRef, f.attempt, summaryRef);
  const spaces = p.spaces ?? [];
  const biggest = Math.max(1, ...spaces.map((s) => s.capacity));
  const summary = summaryOf(f.errors, [
    ['kind', `${id}-kind`, t.kind],
    ['date', `${id}-date`, t.date],
    ['name', `${id}-name`, t.name],
    ['email', `${id}-email`, t.email],
  ]);
  if (f.done) return <Done p={p} t={t} onAgain={() => (setKind([]), setDate(null), f.setDone(false))} className="max-w-[48rem]" />;
  return (
    <Reveal effect="rise" className="mx-auto max-w-[64rem]">
      <form
        ref={formRef}
        noValidate
        className="kit-card grid grid-cols-1 overflow-hidden lg:grid-cols-[minmax(0,1fr)_20rem]"
        onSubmit={(e) => {
          e.preventDefault();
          const found: Record<string, string> = {};
          if (!kind.length) found.kind = t.kindNeeded;
          if (!date) found.date = t.dateNeeded;
          if (f.fail(contactCheck(t, who, found))) return;
          p.onSubmit?.({ kind: kind[0], date, flexible, guests, extras, ...who });
          f.setDone(true);
        }}
      >
        <div className="grid min-w-0 gap-6 p-6 sm:p-8">
          <ErrorSummary ref={summaryRef} errors={summary} />
          <ChipGroup id={`${id}-kind`} legend={t.kind} required options={(p.kinds ?? []).map((k) => ({ value: k.value, label: k.label, icon: k.icon }))} value={kind} onChange={(v) => (setKind(v), f.clear('kind'))} error={f.errors.kind} />
          <div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
            <DateInput id={`${id}-date`} label={t.date} required today={p.today} min={p.today} value={date} onValueChange={(v) => (setDate(v), f.clear('date'))} error={f.errors.date} />
            <NumberInput label={t.guests} variant="stepper" min={p.minGuests ?? 10} max={p.maxGuests ?? 300} step={5} value={guests} onValueChange={setGuests} />
            {/* Phones and tablets: the rooms answer right under the guests, the full list follows the form */}
            <p aria-hidden className="flex flex-wrap items-center gap-1.5 text-[13px] text-muted-foreground sm:col-span-2 lg:hidden">
              {spaces.map((s) => (
                <span key={s.name} className={cn('inline-flex items-center gap-1 rounded-full border px-2.5 py-1 transition-colors duration-300', s.capacity >= guests ? 'border-[color-mix(in_oklab,var(--success)_40%,var(--border))] text-foreground' : 'border-border text-subtle-foreground line-through')}>
                  {s.capacity >= guests && <Icon name="check" size={12} strokeWidth={2.4} className="text-success" />}
                  {s.name}
                </span>
              ))}
            </p>
            <Switch label={t.flexible} description={t.flexibleText} checked={flexible} onCheckedChange={setFlexible} className="sm:col-span-2" />
          </div>
          {p.extras && p.extras.length > 0 && <ChipGroup legend={t.extras} multiple options={p.extras.map((x) => ({ value: x.value, label: x.label, icon: x.icon }))} value={extras} onChange={setExtras} />}
          <div className="border-t border-border pt-6">
            <Contact id={id} t={t} f={f} who={who} setWho={setWho} />
          </div>
          <div className="flex justify-end">
            <Button type="submit" icon="send" magnetic className="max-sm:w-full">
              {p.submitLabel ?? t.send}
            </Button>
          </div>
        </div>
        {/* The rooms with a bar per capacity, each answers live whether it fits the guests */}
        <aside className="border-t border-border bg-surface p-6 sm:p-8 lg:border-s lg:border-t-0">
          <p aria-live="polite" className="text-[14px] font-semibold text-foreground">{t.rooms.replace('{n}', String(guests))}</p>
          <Stagger step={90} as="ul" className="mt-5 grid gap-4">
            {spaces.map((s) => {
              const fits = s.capacity >= guests;
              const share = Math.min(1, guests / s.capacity);
              return (
                <li key={s.name} className={cn('rounded-xl border bg-card p-4 transition-[border-color,opacity] duration-500', fits ? 'border-border' : 'border-border opacity-60')}>
                  <div className="flex items-baseline justify-between gap-3">
                    <span className="text-[14px] font-medium text-foreground">{s.name}</span>
                    <span className={cn('inline-flex items-center gap-1 text-[12px] font-medium transition-colors', fits ? 'text-success-foreground' : 'text-muted-foreground')}>
                      <Icon name={fits ? 'check' : 'minus'} size={13} strokeWidth={2.2} />
                      {fits ? t.fits : t.tooSmall}
                    </span>
                  </div>
                  <p className="mt-0.5 text-[12.5px] text-muted-foreground">
                    {t.upTo.replace('{n}', String(s.capacity))}
                    {s.note ? ` · ${s.note}` : ''}
                  </p>
                  {/* Bar length is the room size, the fill is how full it would be */}
                  <div className="mt-3 h-1.5 overflow-hidden rounded-full bg-[color-mix(in_oklab,var(--foreground)_8%,transparent)]" style={{ width: `${round((s.capacity / biggest) * 100, 2)}%` }}>
                    <div className={cn('h-full origin-left rounded-full transition-[transform,background-color] duration-700 ease-[var(--ease-kit)] motion-reduce:transition-none', fits ? 'bg-primary' : 'bg-[color-mix(in_oklab,var(--destructive)_70%,var(--muted))]')} style={{ transform: `scaleX(${round(share, 3)})` }} />
                  </div>
                </li>
              );
            })}
          </Stagger>
        </aside>
      </form>
    </Reveal>
  );
}

/* ------------------------------------------------------------------ */
/* flex-dates                                                          */
/* ------------------------------------------------------------------ */

function FlexDates({ p, t }: { p: BookingRequestProps; t: Texts }) {
  const id = useId();
  const locale = useLocale();
  const f = useFormState();
  const [mode, setMode] = useState<'exact' | 'flex'>('exact');
  const [range, setRange] = useState<[string | null, string | null]>([null, null]);
  const [months, setMonths] = useState<string[]>([]);
  const [length, setLength] = useState<string[]>([]);
  const [adults, setAdults] = useState(2);
  const [children, setChildren] = useState(0);
  const [ages, setAges] = useState<string[]>([]);
  const [who, setWho] = useState({ name: '', email: '', message: '' });
  const formRef = useRef<HTMLFormElement>(null);
  const summaryRef = useRef<HTMLDivElement>(null);
  useFocusFirstError(formRef, f.attempt, summaryRef);
  const nights = range[0] && range[1] ? Math.round((Date.parse(range[1]) - Date.parse(range[0])) / 86400000) : 0;
  const lengths = locale === 'de' ? ['Wochenende', 'Eine Woche', 'Zwei Wochen'] : ['A weekend', 'One week', 'Two weeks'];
  const summary = summaryOf(f.errors, [
    ['dates', mode === 'exact' ? `${id}-cal` : `${id}-months`, mode === 'exact' ? `${t.arrival} · ${t.departure}` : t.month],
    ['name', `${id}-name`, t.name],
    ['email', `${id}-email`, t.email],
  ]);
  if (f.done) return <Done p={p} t={t} onAgain={() => (setRange([null, null]), setMonths([]), f.setDone(false))} className="max-w-[48rem]" />;
  return (
    <Reveal effect="rise" className="mx-auto max-w-[58rem]">
      <form
        ref={formRef}
        noValidate
        className="kit-card grid gap-7 p-6 sm:p-8"
        onSubmit={(e) => {
          e.preventDefault();
          const found: Record<string, string> = {};
          if (mode === 'exact' && !(range[0] && range[1])) found.dates = t.datesNeeded;
          if (mode === 'flex' && !months.length) found.dates = t.monthNeeded;
          if (f.fail(contactCheck(t, who, found))) return;
          p.onSubmit?.({ mode, range, months, length: length[0], adults, children, ages, ...who });
          f.setDone(true);
        }}
      >
        <ErrorSummary ref={summaryRef} errors={summary} />
        {/* Exact or flexible, a switch with a sliding tile */}
        <div role="radiogroup" aria-label={t.when} className="relative grid grid-cols-2 rounded-xl bg-[color-mix(in_oklab,var(--foreground)_6%,transparent)] p-1 sm:max-w-[26rem]">
          <span aria-hidden className={cn('absolute inset-y-1 start-1 w-[calc(50%-0.25rem)] rounded-lg bg-card shadow-[var(--shadow-sm)] transition-[translate] duration-[420ms] ease-[cubic-bezier(0.3,1.25,0.5,1)] motion-reduce:transition-none', mode === 'flex' && 'translate-x-full rtl:-translate-x-full')} />
          {(['exact', 'flex'] as const).map((m) => (
            <label key={m} htmlFor={`${id}-${m}`} className="relative z-10 flex min-h-11 cursor-pointer items-center justify-center gap-2 rounded-lg text-[14px] font-medium text-muted-foreground transition-colors has-[:checked]:text-foreground has-[:focus-visible]:shadow-[0_0_0_2px_color-mix(in_oklab,var(--primary)_45%,transparent)]">
              <input id={`${id}-${m}`} type="radio" name={`${id}-mode`} checked={mode === m} onChange={() => (setMode(m), f.clear('dates'))} className="sr-only" />
              <Icon name={m === 'exact' ? 'calendar' : 'calendar-check'} size={16} />
              {m === 'exact' ? t.exact : t.flex}
            </label>
          ))}
        </div>
        <div key={mode} className="kit-rise">
          {mode === 'exact' ? (
            <div className="grid grid-cols-1 gap-6 lg:grid-cols-[auto_minmax(0,1fr)] lg:items-start">
              <div id={`${id}-cal`} role="group" aria-label={`${t.arrival} · ${t.departure}`} aria-describedby={f.errors.dates ? `${id}-cal-err` : undefined} className={cn('rounded-xl border p-3 sm:p-4', f.errors.dates ? 'border-[color-mix(in_oklab,var(--destructive)_50%,var(--border))]' : 'border-border')}>
                <Calendar mode="range" months={2} today={p.today} min={p.today} month={p.today} range={range} onRangeChange={(r) => (setRange(r), f.clear('dates'))} />
              </div>
              <div className="grid grid-cols-2 gap-3 lg:grid-cols-1">
                {[
                  [t.arrival, range[0]],
                  [t.departure, range[1]],
                ].map(([label, v]) => (
                  <div key={label as string} className="rounded-xl border border-border bg-surface px-4 py-3">
                    <p className="text-[12px] text-muted-foreground">{label}</p>
                    <p className={cn('mt-0.5 tabular-nums', v ? 'text-[15px] font-semibold text-foreground' : 'text-[14px] text-subtle-foreground')}>{v ? formatDate(v as string, locale) : t.notChosen}</p>
                  </div>
                ))}
                <p aria-live="polite" className="col-span-2 text-[13px] text-muted-foreground lg:col-span-1">
                  {nights > 0 ? t.nights.replace('{n}', String(nights)) : range[0] ? t.pickDeparture : t.pickArrival}
                </p>
                {f.errors.dates && (
                  <p id={`${id}-cal-err`} className="col-span-2 flex items-start gap-1.5 text-[13px] font-medium text-destructive lg:col-span-1">
                    <Icon name="alert-circle" size={15} strokeWidth={1.8} className="mt-[2.5px] shrink-0" />
                    {f.errors.dates}
                  </p>
                )}
              </div>
            </div>
          ) : (
            <div className="grid gap-6">
              <ChipGroup id={`${id}-months`} legend={t.month} required multiple options={(p.months ?? []).map((m) => ({ value: m, label: m }))} value={months} onChange={(v) => (setMonths(v), f.clear('dates'))} error={f.errors.dates} layout={4} />
              <ChipGroup legend={t.length} options={lengths.map((x) => ({ value: x, label: x }))} value={length} onChange={setLength} />
            </div>
          )}
        </div>
        <div className="border-t border-border pt-6">
          <div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
          <NumberInput label={t.adults} variant="stepper" min={1} max={12} value={adults} onValueChange={setAdults} />
          <NumberInput
            label={t.children}
            variant="stepper"
            min={0}
            max={6}
            value={children}
            onValueChange={(n) => {
              setChildren(n);
              setAges((a) => (n > a.length ? [...a, ...Array(n - a.length).fill('')] : a.slice(0, n)));
            }}
          />
          </div>
          {/* One age field per child, each rises in as it is added; closed, it takes no room at all */}
          <Unfold open={children > 0}>
            <div className="grid grid-cols-2 gap-3 pt-5 sm:grid-cols-3">
              {ages.map((a, i) => (
                <Input key={i} type="number" inputMode="numeric" min={0} max={17} label={t.childAge.replace('{n}', String(i + 1))} value={a} onChange={(e) => setAges((x) => x.map((y, j) => (j === i ? e.target.value : y)))} className="kit-rise" />
              ))}
            </div>
            <p className="mt-2 text-[13px] text-muted-foreground">{t.childAgeHint}</p>
          </Unfold>
        </div>
        <div className="border-t border-border pt-6">
          <Contact id={id} t={t} f={f} who={who} setWho={setWho} />
        </div>
        <div className="flex justify-end">
          <Button type="submit" icon="send" className="max-sm:w-full">
            {p.submitLabel ?? t.send}
          </Button>
        </div>
      </form>
    </Reveal>
  );
}

/* ------------------------------------------------------------------ */
/* packages                                                            */
/* ------------------------------------------------------------------ */

function Packages({ p, t }: { p: BookingRequestProps; t: Texts }) {
  const id = useId();
  const locale = useLocale();
  const f = useFormState();
  const c = p.currency ?? '€';
  const pkgs = p.packages ?? [];
  const [pkg, setPkg] = useState<string>('');
  const [extras, setExtras] = useState<string[]>([]);
  const [people, setPeople] = useState(p.guests ?? 2);
  const [date, setDate] = useState<string | null>(null);
  const [who, setWho] = useState({ name: '', email: '', message: '' });
  const formRef = useRef<HTMLFormElement>(null);
  const summaryRef = useRef<HTMLDivElement>(null);
  useFocusFirstError(formRef, f.attempt, summaryRef);
  const chosen = pkgs.find((x) => x.value === pkg);
  const extraSum = (p.extras ?? []).filter((x) => extras.includes(x.value)).reduce((s, x) => s + (x.price ?? 0) * (x.perPerson ? people : 1), 0);
  const total = (chosen ? chosen.price * (chosen.perPerson ? people : 1) : 0) + extraSum;
  const summary = summaryOf(f.errors, [
    ['pkg', `${id}-pkg-0`, t.package],
    ['name', `${id}-name`, t.name],
    ['email', `${id}-email`, t.email],
  ]);
  if (f.done) return <Done p={p} t={t} onAgain={() => (setPkg(''), setExtras([]), f.setDone(false))} className="max-w-[48rem]" />;
  return (
    <form
      ref={formRef}
      noValidate
      className="mx-auto grid max-w-[68rem] grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_21rem] lg:items-start lg:gap-8"
      onSubmit={(e) => {
        e.preventDefault();
        const found: Record<string, string> = {};
        if (!pkg) found.pkg = t.packageNeeded;
        if (f.fail(contactCheck(t, who, found))) return;
        p.onSubmit?.({ package: pkg, extras, people, date, total, ...who });
        f.setDone(true);
      }}
    >
      <div className="grid min-w-0 gap-6">
        <ErrorSummary ref={summaryRef} errors={summary} />
        <fieldset className="min-w-0" aria-describedby={f.errors.pkg ? `${id}-pkg-err` : undefined}>
          <legend className="text-[15px] font-semibold text-foreground">{t.package}</legend>
          <Stagger step={90} as="div" className="mt-4 grid grid-cols-1 gap-3 md:grid-cols-3">
            {pkgs.map((x, i) => {
              const on = pkg === x.value;
              return (
                <label key={x.value} htmlFor={`${id}-pkg-${i}`} className={cn('group/pkg relative flex h-full cursor-pointer flex-col rounded-2xl border bg-card p-5 shadow-[var(--shadow-sm)] transition-[border-color,box-shadow,transform] duration-300 hover:-translate-y-0.5 motion-reduce:hover:translate-y-0', on ? 'border-[color-mix(in_oklab,var(--primary)_60%,var(--border))] shadow-[0_0_0_1px_color-mix(in_oklab,var(--primary)_55%,var(--border)),var(--shadow-md)]' : f.errors.pkg ? 'border-[color-mix(in_oklab,var(--destructive)_45%,var(--border))]' : 'border-border hover:border-border-strong', 'has-[:focus-visible]:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_28%,transparent)]')}>
                  <input id={`${id}-pkg-${i}`} type="radio" name={`${id}-pkg`} value={x.value} checked={on} onChange={() => (setPkg(x.value), f.clear('pkg'))} aria-invalid={f.errors.pkg ? true : undefined} className="sr-only" />
                  <span className="flex items-start justify-between gap-3">
                    <span className="text-[16px] font-semibold text-foreground">{x.name}</span>
                    <span aria-hidden className={cn('grid size-5 shrink-0 place-items-center rounded-full border transition-colors', on ? 'border-primary bg-primary' : 'border-[color-mix(in_oklab,var(--foreground)_38%,var(--border-strong))]')}>
                      <span className={cn('size-2 rounded-full bg-primary-foreground transition-[scale] duration-300 ease-[cubic-bezier(0.34,1.7,0.64,1)]', on ? 'scale-100' : 'scale-0')} />
                    </span>
                  </span>
                  <span className="mt-2 text-[22px] font-semibold tabular-nums tracking-[-0.02em] text-foreground">
                    {money(x.price, c, locale)}
                    {x.perPerson && <span className="ms-1.5 text-[13px] font-normal tracking-normal text-muted-foreground">{t.perPerson}</span>}
                  </span>
                  {x.text && <span className="mt-2 text-pretty text-[13.5px] leading-relaxed text-muted-foreground">{x.text}</span>}
                  {x.includes && x.includes.length > 0 && (
                    <ul className="mt-4 grid gap-1.5 border-t border-border pt-4">
                      {x.includes.map((inc) => (
                        <li key={inc} className="flex items-start gap-2 text-[13px] text-foreground">
                          <Icon name="check" size={14} className="mt-0.5 shrink-0 text-success" />
                          {inc}
                        </li>
                      ))}
                    </ul>
                  )}
                </label>
              );
            })}
          </Stagger>
          {f.errors.pkg && (
            <p id={`${id}-pkg-err`} className="kit-rise mt-3 flex items-start gap-1.5 text-[13px] font-medium text-destructive">
              <Icon name="alert-circle" size={15} strokeWidth={1.8} className="mt-[2.5px] shrink-0" />
              {f.errors.pkg}
            </p>
          )}
        </fieldset>
        <div className="kit-card grid gap-6 p-6 sm:p-8">
          {p.extras && p.extras.length > 0 && (
            <ChipGroup legend={t.extras} multiple options={p.extras.map((x) => ({ value: x.value, label: x.label, meta: x.price ? `+${money(x.price, c, locale)}${x.perPerson ? ` ${t.perPerson}` : ''}` : undefined, icon: x.icon }))} value={extras} onChange={setExtras} />
          )}
          <div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
            <NumberInput label={t.people} variant="stepper" min={p.minGuests ?? 1} max={p.maxGuests ?? 12} value={people} onValueChange={setPeople} />
            <DateInput label={t.date} optional today={p.today} min={p.today} value={date} onValueChange={setDate} />
          </div>
          <div className="border-t border-border pt-6">
            <Contact id={id} t={t} f={f} who={who} setWho={setWho} />
          </div>
        </div>
      </div>
      {/* The total stays in view on desktop and sits above the button on phones */}
      <aside className="kit-card p-6 lg:sticky lg:top-24 lg:mt-9">
        <p className="text-[13px] text-muted-foreground">{t.total}</p>
        <p aria-live="polite" className="mt-1 text-[32px] font-semibold tabular-nums tracking-[-0.03em] text-foreground">
          <span key={total} className="inline-block motion-safe:animate-[kit-rise_0.4s_var(--ease-kit)]">
            {chosen ? money(total, c, locale) : t.fromPrice.replace('{p}', money(Math.min(...pkgs.map((x) => x.price)), c, locale))}
          </span>
        </p>
        <dl className="mt-4 grid gap-2 border-t border-border pt-4 text-[13.5px]">
          <div className="flex justify-between gap-3">
            <dt className="text-muted-foreground">{chosen?.name ?? t.package}</dt>
            <dd className={cn('tabular-nums', chosen ? 'text-foreground' : 'text-subtle-foreground')}>{chosen ? money(chosen.price * (chosen.perPerson ? people : 1), c, locale) : t.notChosen}</dd>
          </div>
          {(p.extras ?? [])
            .filter((x) => extras.includes(x.value))
            .map((x) => (
              <div key={x.value} className="kit-rise flex justify-between gap-3">
                <dt className="text-muted-foreground">{x.label}</dt>
                <dd className="tabular-nums text-foreground">{money((x.price ?? 0) * (x.perPerson ? people : 1), c, locale)}</dd>
              </div>
            ))}
          <div className="flex justify-between gap-3">
            <dt className="text-muted-foreground">{t.people}</dt>
            <dd className="tabular-nums text-foreground">{people}</dd>
          </div>
        </dl>
        <Button type="submit" icon="send" className="mt-6 w-full">
          {p.submitLabel ?? t.send}
        </Button>
        <p className="mt-3 text-center text-[12.5px] text-muted-foreground">{t.totalNote}</p>
      </aside>
    </form>
  );
}

/* ------------------------------------------------------------------ */
/* date-options                                                        */
/* ------------------------------------------------------------------ */

function DateOptions({ p, t }: { p: BookingRequestProps; t: Texts }) {
  const id = useId();
  const f = useFormState();
  const [dates, setDates] = useState<(string | null)[]>([null]);
  const [format, setFormat] = useState<string[]>([]);
  const [people, setPeople] = useState(p.guests ?? 12);
  const [company, setCompany] = useState('');
  const [who, setWho] = useState({ name: '', email: '', message: '' });
  const formRef = useRef<HTMLFormElement>(null);
  const summaryRef = useRef<HTMLDivElement>(null);
  useFocusFirstError(formRef, f.attempt, summaryRef);
  const summary = summaryOf(f.errors, [
    ['dates', `${id}-d0`, t.preferred],
    ['format', `${id}-format`, t.format],
    ['company', `${id}-company`, t.company],
    ['name', `${id}-name`, t.name],
    ['email', `${id}-email`, t.email],
  ]);
  if (f.done) return <Done p={p} t={t} onAgain={() => (setDates([null]), setFormat([]), f.setDone(false))} className="max-w-[48rem]" />;
  return (
    <Reveal effect="rise" className="mx-auto max-w-[52rem]">
      <form
        ref={formRef}
        noValidate
        className="kit-card grid gap-7 p-6 sm:p-8"
        onSubmit={(e) => {
          e.preventDefault();
          const found: Record<string, string> = {};
          if (!dates.some(Boolean)) found.dates = t.optionsNeeded;
          if (!format.length) found.format = t.formatNeeded;
          if (!company.trim()) found.company = t.companyNeeded;
          if (f.fail(contactCheck(t, who, found))) return;
          p.onSubmit?.({ dates: dates.filter(Boolean), format: format[0], people, company, ...who });
          f.setDone(true);
        }}
      >
        <ErrorSummary ref={summaryRef} errors={summary} />
        <fieldset className="min-w-0">
          <legend className="text-[15px] font-semibold text-foreground">
            {t.preferred}
            <span aria-hidden className="ms-0.5 text-[color-mix(in_oklab,var(--destructive)_85%,var(--foreground))]">
              *
            </span>
          </legend>
          <p className="mt-1 text-[13px] text-muted-foreground">{t.preferredHint}</p>
          {/* Options on a numbered rail, each new one rises in */}
          <ol className="relative mt-5 grid gap-4 ps-11">
            {dates.length > 1 && <span aria-hidden className="absolute bottom-[22px] start-[15px] top-[50px] w-px bg-border" />}
            {dates.map((d, i) => (
              <li key={i} className="kit-rise relative">
                <span aria-hidden className={cn('absolute -start-11 top-[34px] grid size-8 place-items-center rounded-full border text-[13px] font-semibold tabular-nums transition-colors', d ? 'border-primary bg-primary text-primary-foreground' : 'border-border bg-card text-muted-foreground')}>
                  {d ? <Icon name="check" size={15} strokeWidth={2.4} /> : i + 1}
                </span>
                <div className="flex items-end gap-2">
                  <DateInput
                    id={`${id}-d${i}`}
                    label={t.option.replace('{n}', String(i + 1))}
                    today={p.today}
                    min={p.today}
                    value={d}
                    error={i === 0 ? f.errors.dates : undefined}
                    onValueChange={(v) => {
                      setDates((x) => x.map((y, j) => (j === i ? v : y)));
                      f.clear('dates');
                    }}
                    className="min-w-0 flex-1"
                  />
                  {dates.length > 1 && (
                    <button type="button" aria-label={t.removeDate.replace('{n}', String(i + 1))} onClick={() => setDates((x) => x.filter((_, j) => j !== i))} className="grid size-11 shrink-0 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-[color-mix(in_oklab,var(--foreground)_6%,transparent)] hover:text-foreground focus-visible:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_28%,transparent)] focus-visible:outline-none">
                      <Icon name="trash" size={17} />
                    </button>
                  )}
                </div>
              </li>
            ))}
          </ol>
          {dates.length < 3 && (
            <Button type="button" variant="ghost" size="sm" iconLeft="plus" className="ms-[1.875rem] mt-3" onClick={() => setDates((x) => [...x, null])}>
              {t.addDate}
            </Button>
          )}
        </fieldset>
        <div className="grid grid-cols-1 gap-6 border-t border-border pt-6">
          <ChipGroup id={`${id}-format`} legend={t.format} required options={(p.formats ?? []).map((x) => ({ value: x.value, label: x.label, icon: x.icon }))} value={format} onChange={(v) => (setFormat(v), f.clear('format'))} error={f.errors.format} layout={(p.formats?.length ?? 0) === 3 ? 3 : 'wrap'} />
          <div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
            <NumberInput label={t.participants} variant="stepper" min={p.minGuests ?? 4} max={p.maxGuests ?? 40} value={people} onValueChange={setPeople} />
            <Input id={`${id}-company`} label={t.company} required autoComplete="organization" value={company} onChange={(e) => (setCompany(e.target.value), f.clear('company'))} error={f.errors.company} />
          </div>
        </div>
        <div className="border-t border-border pt-6">
          <Contact id={id} t={t} f={f} who={who} setWho={setWho} />
        </div>
        <div className="flex justify-end">
          <Button type="submit" icon="send" className="max-sm:w-full">
            {p.submitLabel ?? t.send}
          </Button>
        </div>
      </form>
    </Reveal>
  );
}

/* ------------------------------------------------------------------ */
/* Section                                                             */
/* ------------------------------------------------------------------ */

/**
 * A request to book something a person confirms.
 *   <BookingRequest variant="event-venue" kinds={[…]} spaces={[{ name, capacity }]} today="2026-10-21" />
 *   <BookingRequest variant="packages" packages={[{ value, name, price, perPerson }]} extras={[…]} currency="€" />
 */
export function BookingRequest(props: BookingRequestProps) {
  const { variant = 'event-venue', eyebrow, title, description, labels, className } = props;
  const t = useTexts(TEXTS, labels);
  return (
    <Section className={className}>
      <FlowHead eyebrow={eyebrow} title={title} description={description} align="center" className="mb-10 sm:mb-14" />
      {variant === 'event-venue' && <EventVenue p={props} t={t} />}
      {variant === 'flex-dates' && <FlexDates p={props} t={t} />}
      {variant === 'packages' && <Packages p={props} t={t} />}
      {variant === 'date-options' && <DateOptions p={props} t={t} />}
    </Section>
  );
}

Häufige Fragen

Wie installiere ich Buchungsanfrage?

Mit aiw add @ai-website/booking-request. Der Befehl legt die Datei components/ui/booking-request.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button, icon, reveal, section, motion, form-flow, input, textarea, number-input, switch und date-input. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Buchungsanfrage?

4 Varianten, Location für Feiern, Flexible Daten, Pakete und Wunschtermine. Du wählst sie mit der Prop variant, zum Beispiel variant="flex-dates".

Kann ich die Texte von Buchungsanfrage übersetzen?

Ja. Buchungsanfrage bringt 57 eigene Texte auf Deutsch und Englisch mit, etwa „Was feierst du?“, „Bitte wähle die Art der Feier, sie bestimmt den Aufbau des Raums.“ und „Datum“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Buchungsanfrage einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Buchungsanfrage mit aiw_components und installieren sie mit aiw_add @ai-website/booking-request. Nenn im Prompt die Variante, etwa variant="event-venue", dann schreibt der Assistent deine Inhalte direkt in die Props.

Kann ich Buchungsanfrage ohne Code anpassen?

Ja, im Studio. Alle 4 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden BookingRequest-Code zum Kopieren.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert