Skip to content

BreakBlockForm

Quick-add form for creating breaks, personal time blocks, and unexpected closures. Used within the kiosk calendar to block out lunch breaks, staff meetings, or extended outages. Validates time ranges and supports recurring blocks across multiple days.

Terminal window
npx thebookingkit add break-block-form
import { BreakBlockForm, type BreakBlockFormValues } from "@/components/break-block-form";
import { useState } from "react";
export default function BreakCreator() {
const [showForm, setShowForm] = useState(false);
const [conflictWarning, setConflictWarning] = useState<string | undefined>();
const handleSubmit = async (values: BreakBlockFormValues) => {
const response = await fetch("/api/breaks", {
method: "POST",
body: JSON.stringify(values),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
setShowForm(false);
};
const handleCheckConflicts = async (startTime: string, endTime: string) => {
const res = await fetch(
`/api/bookings/conflicts?start=${startTime}&end=${endTime}`,
);
const { hasConflicts, count } = await res.json();
if (hasConflicts) {
setConflictWarning(`Warning: ${count} bookings will be affected.`);
} else {
setConflictWarning(undefined);
}
};
return (
<>
<button onClick={() => setShowForm(true)}>Add Break</button>
{showForm && (
<BreakBlockForm
defaultValues={{
date: new Date().toISOString().split("T")[0],
startTime: "12:00",
endTime: "13:00",
blockType: "break",
}}
onSubmit={handleSubmit}
onCancel={() => setShowForm(false)}
conflictWarning={conflictWarning}
/>
)}
</>
);
}
export type BlockType = "break" | "personal" | "meeting" | "closed";
export interface BreakBlockFormValues {
title: string;
date: string;
startTime: string;
endTime: string;
blockType: BlockType;
recurring: boolean;
}
export interface BreakBlockFormProps {
/** Called when the form is submitted */
onSubmit: (values: BreakBlockFormValues) => Promise<void>;
/** Called when the form is cancelled */
onCancel?: () => void;
/** Pre-filled values (e.g., from clicking an empty slot) */
defaultValues?: Partial<BreakBlockFormValues>;
/** Whether there are conflicting bookings (shown as warning) */
conflictWarning?: string;
/** Additional CSS class name */
className?: string;
/** Inline styles */
style?: React.CSSProperties;
}
import React from "react";
import { useForm } from "react-hook-form";
import { cn } from "../utils/cn.js";
/** Block type options */
export type BlockType = "break" | "personal" | "meeting" | "closed";
/** Values submitted by the break/block form */
export interface BreakBlockFormValues {
title: string;
date: string;
startTime: string;
endTime: string;
blockType: BlockType;
recurring: boolean;
}
/** Props for the BreakBlockForm component */
export interface BreakBlockFormProps {
/** Called when the form is submitted */
onSubmit: (values: BreakBlockFormValues) => Promise<void>;
/** Called when the form is cancelled */
onCancel?: () => void;
/** Pre-filled values (e.g., from clicking an empty slot) */
defaultValues?: Partial<BreakBlockFormValues>;
/** Whether there are conflicting bookings (shown as warning) */
conflictWarning?: string;
/** Additional CSS class name */
className?: string;
/** Inline styles */
style?: React.CSSProperties;
}
const BLOCK_TYPE_LABELS: Record<BlockType, string> = {
break: "Break",
personal: "Personal Time",
meeting: "Staff Meeting",
closed: "Closed",
};
/**
* Quick-add form for creating breaks and non-working time blocks.
*
* Used within the kiosk calendar to block out lunch breaks, personal time,
* or unexpected closures. Breaks are stored as availability overrides.
*
* @example
* ```tsx
* <BreakBlockForm
* defaultValues={{ date: "2026-03-10", startTime: "12:00", endTime: "13:00" }}
* onSubmit={async (values) => {
* await createBreak(values);
* toast.success("Break added");
* }}
* onCancel={() => setShowForm(false)}
* />
* ```
*/
export function BreakBlockForm({
onSubmit,
onCancel,
defaultValues,
conflictWarning,
className,
style,
}: BreakBlockFormProps) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
setError,
} = useForm<BreakBlockFormValues>({
defaultValues: {
title: "",
blockType: "break",
recurring: false,
...defaultValues,
},
});
const handleFormSubmit = async (values: BreakBlockFormValues) => {
// Validate end > start
if (values.endTime <= values.startTime) {
setError("endTime", { message: "End time must be after start time" });
return;
}
try {
await onSubmit(values);
} catch (err) {
setError("root", {
message:
err instanceof Error ? err.message : "Failed to create block.",
});
}
};
return (
<form
className={cn("tbk-break-form", className)}
style={style}
onSubmit={handleSubmit(handleFormSubmit)}
noValidate
>
<h2 className="tbk-form-title">Add Break / Block</h2>
{/* Title */}
<div className="tbk-field">
<label htmlFor="bb-title" className="tbk-label">
Title
</label>
<input
id="bb-title"
type="text"
className="tbk-input"
placeholder="Lunch Break"
{...register("title", { required: "Title is required" })}
/>
{errors.title ? (
<p className="tbk-error">{errors.title.message}</p>
) : null}
</div>
{/* Block Type */}
<div className="tbk-field">
<label htmlFor="bb-type" className="tbk-label">
Type
</label>
<select
id="bb-type"
className="tbk-select"
{...register("blockType")}
>
{(Object.entries(BLOCK_TYPE_LABELS) as [BlockType, string][]).map(
([value, label]) => (
<option key={value} value={value}>
{label}
</option>
),
)}
</select>
</div>
{/* Date */}
<div className="tbk-field">
<label htmlFor="bb-date" className="tbk-label">
Date <span aria-hidden="true">*</span>
</label>
<input
id="bb-date"
type="date"
className="tbk-input"
{...register("date", { required: "Date is required" })}
/>
{errors.date ? (
<p className="tbk-error">{errors.date.message}</p>
) : null}
</div>
{/* Time Range */}
<div className="tbk-field-row">
<div className="tbk-field">
<label htmlFor="bb-start" className="tbk-label">
Start <span aria-hidden="true">*</span>
</label>
<input
id="bb-start"
type="time"
className="tbk-input"
{...register("startTime", { required: "Start time is required" })}
/>
{errors.startTime ? (
<p className="tbk-error">{errors.startTime.message}</p>
) : null}
</div>
<div className="tbk-field">
<label htmlFor="bb-end" className="tbk-label">
End <span aria-hidden="true">*</span>
</label>
<input
id="bb-end"
type="time"
className="tbk-input"
{...register("endTime", { required: "End time is required" })}
/>
{errors.endTime ? (
<p className="tbk-error">{errors.endTime.message}</p>
) : null}
</div>
</div>
{/* Recurring */}
<div className="tbk-field">
<label className="tbk-checkbox-label">
<input type="checkbox" {...register("recurring")} />
Repeat daily for the rest of the week
</label>
</div>
{/* Conflict Warning */}
{conflictWarning && (
<div className="tbk-alert tbk-alert-warning" role="alert">
{conflictWarning}
</div>
)}
{errors.root ? (
<div className="tbk-alert tbk-alert-error" role="alert">
{errors.root.message}
</div>
) : null}
<div className="tbk-form-actions">
<button
type="submit"
className="tbk-button-primary"
disabled={isSubmitting}
>
{isSubmitting ? "Adding..." : "Add Block"}
</button>
{onCancel && (
<button
type="button"
className="tbk-button-secondary"
onClick={onCancel}
disabled={isSubmitting}
>
Cancel
</button>
)}
</div>
{/* Quick Actions */}
<div className="tbk-break-quick-actions">
<button
type="button"
className="tbk-button-secondary tbk-button-small"
onClick={() => {
const now = new Date();
const todayStr = now.toISOString().split("T")[0];
onSubmit({
title: "Closed for the day",
date: todayStr,
startTime: now.toTimeString().slice(0, 5),
endTime: "23:59",
blockType: "closed",
recurring: false,
});
}}
>
Block rest of day
</button>
</div>
</form>
);
}