@bookinglab/booking-ui-react 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/dist/index.cjs +360 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +358 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 BookingLab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# @bookinglab/booking-ui-react
|
|
2
|
+
|
|
3
|
+
React UI components for BookingLab booking journeys. Build dynamic, accessible booking forms with conditional logic, validation, and full styling customization.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @bookinglab/booking-ui-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { BookingForm, Question } from '@bookinglab/booking-ui-react';
|
|
15
|
+
|
|
16
|
+
const questions: Question[] = [
|
|
17
|
+
{ id: 1, name: 'Personal Details', detail_type: 'heading' },
|
|
18
|
+
{ id: 2, name: 'Full Name', detail_type: 'text_field', required: true },
|
|
19
|
+
{ id: 3, name: 'Email', detail_type: 'text_field', required: true },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function App() {
|
|
23
|
+
const handleSubmit = (values) => {
|
|
24
|
+
console.log('Form submitted:', values);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<BookingForm
|
|
29
|
+
questions={questions}
|
|
30
|
+
onSubmit={handleSubmit}
|
|
31
|
+
submitLabel="Book Now"
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Components
|
|
38
|
+
|
|
39
|
+
### BookingForm
|
|
40
|
+
|
|
41
|
+
A dynamic form component that renders form fields based on an array of `Question` objects.
|
|
42
|
+
|
|
43
|
+
#### Props
|
|
44
|
+
|
|
45
|
+
| Prop | Type | Default | Description |
|
|
46
|
+
|------|------|---------|-------------|
|
|
47
|
+
| `questions` | `Question[]` | required | Array of question objects defining the form fields |
|
|
48
|
+
| `onSubmit` | `(values: FormValues) => void` | required | Callback fired when form is submitted with valid values |
|
|
49
|
+
| `submitLabel` | `string` | `"Submit"` | Text for the submit button |
|
|
50
|
+
| `className` | `string` | `""` | Class name for the form element |
|
|
51
|
+
| `classNames` | `BookingFormClassNames` | `undefined` | Custom class names for styling individual elements |
|
|
52
|
+
|
|
53
|
+
#### Question Types
|
|
54
|
+
|
|
55
|
+
The `detail_type` property determines how each question is rendered:
|
|
56
|
+
|
|
57
|
+
| Type | Description | Renders |
|
|
58
|
+
|------|-------------|---------|
|
|
59
|
+
| `heading` | Section heading | `<h3>` element |
|
|
60
|
+
| `text_field` | Single-line text | `<input type="text">` |
|
|
61
|
+
| `text_area` | Multi-line text | `<textarea>` |
|
|
62
|
+
| `select` | Dropdown selection | `<select>` with options |
|
|
63
|
+
| `date` | Date picker | `<input type="date">` |
|
|
64
|
+
| `number` | Numeric input | `<input type="number">` |
|
|
65
|
+
| `check` | Checkbox | `<input type="checkbox">` |
|
|
66
|
+
|
|
67
|
+
#### Question Interface
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
interface Question {
|
|
71
|
+
id: number;
|
|
72
|
+
name: string;
|
|
73
|
+
detail_type: 'heading' | 'text_field' | 'text_area' | 'select' | 'date' | 'number' | 'check';
|
|
74
|
+
required?: boolean;
|
|
75
|
+
help_text?: string;
|
|
76
|
+
options?: QuestionOption[]; // For select type
|
|
77
|
+
settings?: QuestionSettings;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface QuestionOption {
|
|
81
|
+
id: number;
|
|
82
|
+
name: string;
|
|
83
|
+
price?: number;
|
|
84
|
+
is_default?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface QuestionSettings {
|
|
88
|
+
conditional_question?: string;
|
|
89
|
+
conditional_answers?: Record<string, string>;
|
|
90
|
+
min?: number;
|
|
91
|
+
max?: number;
|
|
92
|
+
placeholder?: string;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Conditional Questions
|
|
97
|
+
|
|
98
|
+
Show or hide questions based on answers to other questions using `conditional_answers`:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
const questions: Question[] = [
|
|
102
|
+
{
|
|
103
|
+
id: 1,
|
|
104
|
+
name: 'Service Type',
|
|
105
|
+
detail_type: 'select',
|
|
106
|
+
required: true,
|
|
107
|
+
options: [
|
|
108
|
+
{ id: 1, name: 'Consultation' },
|
|
109
|
+
{ id: 2, name: 'Follow-up' },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 2,
|
|
114
|
+
name: 'First time visiting?',
|
|
115
|
+
detail_type: 'check',
|
|
116
|
+
settings: {
|
|
117
|
+
// Only show when "Consultation" (option id: 1) is selected
|
|
118
|
+
conditional_answers: { '1': '1' },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Styling
|
|
125
|
+
|
|
126
|
+
### Custom Class Names
|
|
127
|
+
|
|
128
|
+
Override default styles using the `classNames` prop:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
<BookingForm
|
|
132
|
+
questions={questions}
|
|
133
|
+
onSubmit={handleSubmit}
|
|
134
|
+
classNames={{
|
|
135
|
+
fieldWrapper: 'mb-6',
|
|
136
|
+
label: 'text-white font-bold',
|
|
137
|
+
heading: 'text-2xl text-primary',
|
|
138
|
+
input: 'border-2 border-gray-400 rounded-lg p-3',
|
|
139
|
+
inputError: 'border-red-500',
|
|
140
|
+
checkbox: 'w-5 h-5',
|
|
141
|
+
helpText: 'text-gray-400 text-sm',
|
|
142
|
+
errorText: 'text-red-400',
|
|
143
|
+
button: 'bg-primary text-white py-3 px-6 rounded-lg',
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Available Class Name Keys
|
|
149
|
+
|
|
150
|
+
| Key | Description |
|
|
151
|
+
|-----|-------------|
|
|
152
|
+
| `fieldWrapper` | Container for each form field |
|
|
153
|
+
| `label` | All label elements |
|
|
154
|
+
| `heading` | Heading elements (`detail_type: 'heading'`) |
|
|
155
|
+
| `input` | All input elements (text, textarea, select, date, number) |
|
|
156
|
+
| `inputError` | Additional classes for inputs in error state |
|
|
157
|
+
| `checkbox` | Checkbox input specifically |
|
|
158
|
+
| `helpText` | Help text below fields |
|
|
159
|
+
| `errorText` | Error message text |
|
|
160
|
+
| `button` | Submit button |
|
|
161
|
+
|
|
162
|
+
## Form Values
|
|
163
|
+
|
|
164
|
+
The `onSubmit` callback receives a `FormValues` object mapping question IDs to their values:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
type FormValues = Record<number, string | number | boolean>;
|
|
168
|
+
|
|
169
|
+
// Example output:
|
|
170
|
+
{
|
|
171
|
+
2: "John Doe", // text_field
|
|
172
|
+
3: "john@example.com", // text_field
|
|
173
|
+
4: 1, // select (option id)
|
|
174
|
+
5: true, // check
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Validation
|
|
179
|
+
|
|
180
|
+
- Required fields are automatically validated
|
|
181
|
+
- Number fields respect `min` and `max` settings
|
|
182
|
+
- Error messages display after field is touched
|
|
183
|
+
- Hidden conditional fields are excluded from validation
|
|
184
|
+
|
|
185
|
+
## Requirements
|
|
186
|
+
|
|
187
|
+
- React 17.0.0 or higher
|
|
188
|
+
- React DOM 17.0.0 or higher
|
|
189
|
+
|
|
190
|
+
## TypeScript
|
|
191
|
+
|
|
192
|
+
All types are exported for TypeScript users:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import type {
|
|
196
|
+
Question,
|
|
197
|
+
QuestionOption,
|
|
198
|
+
QuestionSettings,
|
|
199
|
+
FormValues,
|
|
200
|
+
FormErrors,
|
|
201
|
+
BookingFormProps,
|
|
202
|
+
BookingFormClassNames,
|
|
203
|
+
} from '@bookinglab/booking-ui-react';
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/components/BookingForm.tsx
|
|
7
|
+
var cx = (...classes) => classes.filter(Boolean).join(" ");
|
|
8
|
+
function FormField({
|
|
9
|
+
question,
|
|
10
|
+
value,
|
|
11
|
+
error,
|
|
12
|
+
onChange,
|
|
13
|
+
classNames
|
|
14
|
+
}) {
|
|
15
|
+
const inputId = `question-${question.id}`;
|
|
16
|
+
const errorId = `${inputId}-error`;
|
|
17
|
+
const hasError = !!error;
|
|
18
|
+
const defaultFieldWrapper = "mb-4";
|
|
19
|
+
const defaultLabel = "block text-sm font-medium mb-1 text-gray-700";
|
|
20
|
+
const defaultHeading = "text-lg font-semibold text-gray-900 mt-4 mb-2 first:mt-0";
|
|
21
|
+
const defaultInput = "w-full px-3 py-2 border rounded-md text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 border-gray-300 focus:ring-blue-500 focus:border-blue-500";
|
|
22
|
+
const defaultInputError = "border-red-500 focus:ring-red-500";
|
|
23
|
+
const defaultCheckbox = "mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500";
|
|
24
|
+
const defaultHelpText = "mt-1 text-xs text-gray-500";
|
|
25
|
+
const defaultErrorText = "mt-1 text-xs text-red-600";
|
|
26
|
+
const inputClasses = hasError ? cx(classNames?.input ?? defaultInput, classNames?.inputError ?? defaultInputError) : cx(classNames?.input ?? defaultInput);
|
|
27
|
+
const labelClasses = classNames?.label ?? defaultLabel;
|
|
28
|
+
const checkboxLabelClasses = cx("text-sm", classNames?.label ?? "text-gray-700");
|
|
29
|
+
const renderLabel = () => {
|
|
30
|
+
if (question.detail_type === "heading") return null;
|
|
31
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: inputId, className: labelClasses, children: [
|
|
32
|
+
question.name,
|
|
33
|
+
question.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
|
|
34
|
+
] });
|
|
35
|
+
};
|
|
36
|
+
const renderHelpText = () => {
|
|
37
|
+
if (!question.help_text) return null;
|
|
38
|
+
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: classNames?.helpText ?? defaultHelpText, children: question.help_text });
|
|
39
|
+
};
|
|
40
|
+
const renderError = () => {
|
|
41
|
+
if (!error) return null;
|
|
42
|
+
return /* @__PURE__ */ jsxRuntime.jsx("p", { id: errorId, className: classNames?.errorText ?? defaultErrorText, role: "alert", children: error });
|
|
43
|
+
};
|
|
44
|
+
const ariaProps = {
|
|
45
|
+
"aria-invalid": hasError ? true : void 0,
|
|
46
|
+
"aria-describedby": hasError ? errorId : void 0,
|
|
47
|
+
"aria-required": question.required || void 0
|
|
48
|
+
};
|
|
49
|
+
switch (question.detail_type) {
|
|
50
|
+
case "heading":
|
|
51
|
+
return /* @__PURE__ */ jsxRuntime.jsx("h3", { className: classNames?.heading ?? defaultHeading, children: question.name });
|
|
52
|
+
case "text_field":
|
|
53
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
54
|
+
renderLabel(),
|
|
55
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
56
|
+
"input",
|
|
57
|
+
{
|
|
58
|
+
id: inputId,
|
|
59
|
+
type: "text",
|
|
60
|
+
value: value || "",
|
|
61
|
+
onChange: (e) => onChange(e.target.value),
|
|
62
|
+
placeholder: question.settings?.placeholder,
|
|
63
|
+
className: inputClasses,
|
|
64
|
+
...ariaProps
|
|
65
|
+
}
|
|
66
|
+
),
|
|
67
|
+
renderHelpText(),
|
|
68
|
+
renderError()
|
|
69
|
+
] });
|
|
70
|
+
case "text_area":
|
|
71
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
72
|
+
renderLabel(),
|
|
73
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
74
|
+
"textarea",
|
|
75
|
+
{
|
|
76
|
+
id: inputId,
|
|
77
|
+
value: value || "",
|
|
78
|
+
onChange: (e) => onChange(e.target.value),
|
|
79
|
+
placeholder: question.settings?.placeholder,
|
|
80
|
+
rows: 4,
|
|
81
|
+
className: inputClasses,
|
|
82
|
+
...ariaProps
|
|
83
|
+
}
|
|
84
|
+
),
|
|
85
|
+
renderHelpText(),
|
|
86
|
+
renderError()
|
|
87
|
+
] });
|
|
88
|
+
case "select":
|
|
89
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
90
|
+
renderLabel(),
|
|
91
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
92
|
+
"select",
|
|
93
|
+
{
|
|
94
|
+
id: inputId,
|
|
95
|
+
value: value ?? "",
|
|
96
|
+
onChange: (e) => onChange(e.target.value),
|
|
97
|
+
className: inputClasses,
|
|
98
|
+
...ariaProps,
|
|
99
|
+
children: [
|
|
100
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select an option" }),
|
|
101
|
+
question.options?.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.id, children: option.name }, option.id))
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
renderHelpText(),
|
|
106
|
+
renderError()
|
|
107
|
+
] });
|
|
108
|
+
case "date":
|
|
109
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
110
|
+
renderLabel(),
|
|
111
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
112
|
+
"input",
|
|
113
|
+
{
|
|
114
|
+
id: inputId,
|
|
115
|
+
type: "date",
|
|
116
|
+
value: value || "",
|
|
117
|
+
onChange: (e) => onChange(e.target.value),
|
|
118
|
+
min: question.settings?.min?.toString(),
|
|
119
|
+
max: question.settings?.max?.toString(),
|
|
120
|
+
className: inputClasses,
|
|
121
|
+
...ariaProps
|
|
122
|
+
}
|
|
123
|
+
),
|
|
124
|
+
renderHelpText(),
|
|
125
|
+
renderError()
|
|
126
|
+
] });
|
|
127
|
+
case "number":
|
|
128
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
129
|
+
renderLabel(),
|
|
130
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
131
|
+
"input",
|
|
132
|
+
{
|
|
133
|
+
id: inputId,
|
|
134
|
+
type: "number",
|
|
135
|
+
value: value ?? "",
|
|
136
|
+
onChange: (e) => onChange(e.target.value ? Number(e.target.value) : ""),
|
|
137
|
+
min: question.settings?.min,
|
|
138
|
+
max: question.settings?.max,
|
|
139
|
+
placeholder: question.settings?.placeholder,
|
|
140
|
+
className: inputClasses,
|
|
141
|
+
...ariaProps
|
|
142
|
+
}
|
|
143
|
+
),
|
|
144
|
+
renderHelpText(),
|
|
145
|
+
renderError()
|
|
146
|
+
] });
|
|
147
|
+
case "check":
|
|
148
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
149
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
|
|
150
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
151
|
+
"input",
|
|
152
|
+
{
|
|
153
|
+
id: inputId,
|
|
154
|
+
type: "checkbox",
|
|
155
|
+
checked: !!value,
|
|
156
|
+
onChange: (e) => onChange(e.target.checked),
|
|
157
|
+
className: classNames?.checkbox ?? defaultCheckbox,
|
|
158
|
+
...ariaProps
|
|
159
|
+
}
|
|
160
|
+
),
|
|
161
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: inputId, className: checkboxLabelClasses, children: [
|
|
162
|
+
question.name,
|
|
163
|
+
question.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
|
|
164
|
+
] })
|
|
165
|
+
] }),
|
|
166
|
+
renderHelpText(),
|
|
167
|
+
renderError()
|
|
168
|
+
] });
|
|
169
|
+
default:
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function BookingForm({
|
|
174
|
+
questions,
|
|
175
|
+
onSubmit,
|
|
176
|
+
submitLabel = "Submit",
|
|
177
|
+
className = "",
|
|
178
|
+
labelClassName,
|
|
179
|
+
classNames: classNamesProp
|
|
180
|
+
}) {
|
|
181
|
+
const classNames = {
|
|
182
|
+
...classNamesProp,
|
|
183
|
+
label: classNamesProp?.label ?? labelClassName
|
|
184
|
+
};
|
|
185
|
+
const [values, setValues] = react.useState({});
|
|
186
|
+
const [errors, setErrors] = react.useState({});
|
|
187
|
+
const [touched, setTouched] = react.useState({});
|
|
188
|
+
const isQuestionVisible = react.useCallback(
|
|
189
|
+
(question) => {
|
|
190
|
+
const { settings } = question;
|
|
191
|
+
if (!settings?.conditional_answers) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
const conditionEntries = Object.entries(settings.conditional_answers);
|
|
195
|
+
if (conditionEntries.length === 0) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
return conditionEntries.some(([questionIdStr, expectedAnswer]) => {
|
|
199
|
+
const questionId = Number(questionIdStr);
|
|
200
|
+
const currentValue = values[String(questionId)];
|
|
201
|
+
if (currentValue === void 0 || currentValue === "" || currentValue === null) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
const normalizedCurrent = typeof currentValue === "object" && currentValue !== null && "id" in currentValue ? currentValue : currentValue;
|
|
205
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
206
|
+
const addCandidate = (v) => {
|
|
207
|
+
if (v === void 0 || v === null) return;
|
|
208
|
+
const s = String(v).trim();
|
|
209
|
+
if (s) candidates.add(s);
|
|
210
|
+
};
|
|
211
|
+
const sourceQuestion = questions.find((q) => q.id === questionId);
|
|
212
|
+
if (typeof normalizedCurrent === "object" && normalizedCurrent !== null) {
|
|
213
|
+
addCandidate(normalizedCurrent.id);
|
|
214
|
+
addCandidate(normalizedCurrent.name);
|
|
215
|
+
} else {
|
|
216
|
+
addCandidate(normalizedCurrent);
|
|
217
|
+
}
|
|
218
|
+
if (sourceQuestion?.detail_type === "select" && sourceQuestion.options?.length) {
|
|
219
|
+
const currentStr = [...candidates][0] ?? "";
|
|
220
|
+
const optionById = sourceQuestion.options.find((o) => String(o.id) === currentStr);
|
|
221
|
+
const optionByName = sourceQuestion.options.find((o) => String(o.name).trim() === currentStr);
|
|
222
|
+
const opt = optionById ?? optionByName;
|
|
223
|
+
if (opt) {
|
|
224
|
+
addCandidate(opt.id);
|
|
225
|
+
addCandidate(opt.name);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const matchesScalar = (expected) => {
|
|
229
|
+
const expectedStr = String(expected).trim();
|
|
230
|
+
return candidates.has(expectedStr);
|
|
231
|
+
};
|
|
232
|
+
if (Array.isArray(expectedAnswer)) {
|
|
233
|
+
return expectedAnswer.some((v) => matchesScalar(v));
|
|
234
|
+
}
|
|
235
|
+
if (expectedAnswer && typeof expectedAnswer === "object") {
|
|
236
|
+
const expectedMap = expectedAnswer;
|
|
237
|
+
for (const c of candidates) {
|
|
238
|
+
if (Object.prototype.hasOwnProperty.call(expectedMap, c)) {
|
|
239
|
+
const flag = expectedMap[c];
|
|
240
|
+
return flag === void 0 ? true : !!flag;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
return matchesScalar(expectedAnswer);
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
[values, questions]
|
|
249
|
+
);
|
|
250
|
+
const visibleQuestions = questions.filter(isQuestionVisible);
|
|
251
|
+
const validateField = react.useCallback((question, value) => {
|
|
252
|
+
if (question.detail_type === "heading") return null;
|
|
253
|
+
if (question.required) {
|
|
254
|
+
if (value === void 0 || value === "" || value === null) {
|
|
255
|
+
return `${question.name} is required`;
|
|
256
|
+
}
|
|
257
|
+
if (question.detail_type === "check" && !value) {
|
|
258
|
+
return `${question.name} must be checked`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (question.detail_type === "number" && value !== "" && value !== void 0) {
|
|
262
|
+
const numValue = Number(value);
|
|
263
|
+
if (question.settings?.min !== void 0 && numValue < question.settings.min) {
|
|
264
|
+
return `Minimum value is ${question.settings.min}`;
|
|
265
|
+
}
|
|
266
|
+
if (question.settings?.max !== void 0 && numValue > question.settings.max) {
|
|
267
|
+
return `Maximum value is ${question.settings.max}`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}, []);
|
|
272
|
+
const validateAll = react.useCallback(() => {
|
|
273
|
+
const newErrors = {};
|
|
274
|
+
let isValid = true;
|
|
275
|
+
visibleQuestions.forEach((question) => {
|
|
276
|
+
const error = validateField(question, values[question.id]);
|
|
277
|
+
if (error) {
|
|
278
|
+
newErrors[question.id] = error;
|
|
279
|
+
isValid = false;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
setErrors(newErrors);
|
|
283
|
+
return isValid;
|
|
284
|
+
}, [visibleQuestions, values, validateField]);
|
|
285
|
+
react.useEffect(() => {
|
|
286
|
+
const visibleIds = new Set(visibleQuestions.map((q) => q.id));
|
|
287
|
+
setErrors((prev) => {
|
|
288
|
+
let changed = false;
|
|
289
|
+
const next = { ...prev };
|
|
290
|
+
Object.keys(next).forEach((idStr) => {
|
|
291
|
+
const id = Number(idStr);
|
|
292
|
+
if (!visibleIds.has(id)) {
|
|
293
|
+
delete next[id];
|
|
294
|
+
changed = true;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
return changed ? next : prev;
|
|
298
|
+
});
|
|
299
|
+
}, [visibleQuestions]);
|
|
300
|
+
const handleChange = (questionId, value) => {
|
|
301
|
+
setValues((prev) => ({ ...prev, [questionId]: value }));
|
|
302
|
+
setTouched((prev) => ({ ...prev, [questionId]: true }));
|
|
303
|
+
if (touched[questionId]) {
|
|
304
|
+
const question = questions.find((q) => q.id === questionId);
|
|
305
|
+
if (question) {
|
|
306
|
+
const error = validateField(question, value);
|
|
307
|
+
setErrors((prev) => {
|
|
308
|
+
if (error) {
|
|
309
|
+
return { ...prev, [questionId]: error };
|
|
310
|
+
}
|
|
311
|
+
const { [questionId]: _, ...rest } = prev;
|
|
312
|
+
return rest;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const handleSubmit = (e) => {
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
const allTouched = {};
|
|
320
|
+
visibleQuestions.forEach((q) => {
|
|
321
|
+
allTouched[q.id] = true;
|
|
322
|
+
});
|
|
323
|
+
setTouched(allTouched);
|
|
324
|
+
if (validateAll()) {
|
|
325
|
+
const visibleValues = {};
|
|
326
|
+
visibleQuestions.forEach((q) => {
|
|
327
|
+
if (q.detail_type !== "heading" && values[q.id] !== void 0) {
|
|
328
|
+
visibleValues[q.id] = values[q.id];
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
onSubmit(visibleValues);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const defaultButton = "w-full mt-4 px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors";
|
|
335
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className, noValidate: true, children: [
|
|
336
|
+
visibleQuestions.map((question) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
337
|
+
FormField,
|
|
338
|
+
{
|
|
339
|
+
question,
|
|
340
|
+
value: values[question.id],
|
|
341
|
+
error: touched[question.id] ? errors[question.id] : void 0,
|
|
342
|
+
onChange: (value) => handleChange(question.id, value),
|
|
343
|
+
classNames
|
|
344
|
+
},
|
|
345
|
+
question.id
|
|
346
|
+
)),
|
|
347
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
348
|
+
"button",
|
|
349
|
+
{
|
|
350
|
+
type: "submit",
|
|
351
|
+
className: classNames?.button ?? defaultButton,
|
|
352
|
+
children: submitLabel
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
] });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
exports.BookingForm = BookingForm;
|
|
359
|
+
//# sourceMappingURL=index.cjs.map
|
|
360
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/BookingForm.tsx"],"names":["jsxs","jsx","useState","useCallback","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import React, { useState, useEffect, useCallback } from 'react';\nimport type { Question, FormValues, FormErrors, BookingFormProps, BookingFormClassNames } from '../types';\n\n/** Merge base classes with optional custom classes */\nconst cx = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ');\n\n/**\n * Renders a single form field based on question type\n */\nfunction FormField({\n question,\n value,\n error,\n onChange,\n classNames,\n}: {\n question: Question;\n value: string | number | boolean | undefined;\n error?: string;\n onChange: (value: string | number | boolean) => void;\n classNames?: BookingFormClassNames;\n}) {\n const inputId = `question-${question.id}`;\n const errorId = `${inputId}-error`;\n const hasError = !!error;\n\n // Default classes (can be overridden via classNames prop)\n const defaultFieldWrapper = 'mb-4';\n const defaultLabel = 'block text-sm font-medium mb-1 text-gray-700';\n const defaultHeading = 'text-lg font-semibold text-gray-900 mt-4 mb-2 first:mt-0';\n const defaultInput = 'w-full px-3 py-2 border rounded-md text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 border-gray-300 focus:ring-blue-500 focus:border-blue-500';\n const defaultInputError = 'border-red-500 focus:ring-red-500';\n const defaultCheckbox = 'mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500';\n const defaultHelpText = 'mt-1 text-xs text-gray-500';\n const defaultErrorText = 'mt-1 text-xs text-red-600';\n\n const inputClasses = hasError\n ? cx(classNames?.input ?? defaultInput, classNames?.inputError ?? defaultInputError)\n : cx(classNames?.input ?? defaultInput);\n\n const labelClasses = classNames?.label ?? defaultLabel;\n const checkboxLabelClasses = cx('text-sm', classNames?.label ?? 'text-gray-700');\n\n const renderLabel = () => {\n if (question.detail_type === 'heading') return null;\n return (\n <label htmlFor={inputId} className={labelClasses}>\n {question.name}\n {question.required && <span className=\"text-red-500 ml-1\" aria-hidden=\"true\">*</span>}\n </label>\n );\n };\n\n const renderHelpText = () => {\n if (!question.help_text) return null;\n return <p className={classNames?.helpText ?? defaultHelpText}>{question.help_text}</p>;\n };\n\n const renderError = () => {\n if (!error) return null;\n return (\n <p id={errorId} className={classNames?.errorText ?? defaultErrorText} role=\"alert\">\n {error}\n </p>\n );\n };\n\n const ariaProps = {\n 'aria-invalid': hasError ? true : undefined,\n 'aria-describedby': hasError ? errorId : undefined,\n 'aria-required': question.required || undefined,\n };\n\n switch (question.detail_type) {\n case 'heading':\n return (\n <h3 className={classNames?.heading ?? defaultHeading}>\n {question.name}\n </h3>\n );\n\n case 'text_field':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"text\"\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n placeholder={question.settings?.placeholder}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'text_area':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <textarea\n id={inputId}\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n placeholder={question.settings?.placeholder}\n rows={4}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'select':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <select\n id={inputId}\n value={(value as string | number) ?? ''}\n onChange={(e) => onChange(e.target.value)}\n className={inputClasses}\n {...ariaProps}\n >\n <option value=\"\">Select an option</option>\n {question.options?.map((option) => (\n <option key={option.id} value={option.id}>\n {option.name}\n </option>\n ))}\n </select>\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'date':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"date\"\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n min={question.settings?.min?.toString()}\n max={question.settings?.max?.toString()}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'number':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"number\"\n value={(value as number) ?? ''}\n onChange={(e) => onChange(e.target.value ? Number(e.target.value) : '')}\n min={question.settings?.min}\n max={question.settings?.max}\n placeholder={question.settings?.placeholder}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'check':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n <div className=\"flex items-start gap-2\">\n <input\n id={inputId}\n type=\"checkbox\"\n checked={!!value}\n onChange={(e) => onChange(e.target.checked)}\n className={classNames?.checkbox ?? defaultCheckbox}\n {...ariaProps}\n />\n <label htmlFor={inputId} className={checkboxLabelClasses}>\n {question.name}\n {question.required && <span className=\"text-red-500 ml-1\" aria-hidden=\"true\">*</span>}\n </label>\n </div>\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n default:\n return null;\n }\n}\n\n/**\n * BookingForm - A dynamic form component for booking questions\n *\n * @example\n * ```tsx\n * const questions: Question[] = [\n * { id: 1, name: 'Personal Details', detail_type: 'heading' },\n * { id: 2, name: 'Full Name', detail_type: 'text_field', required: true },\n * { id: 3, name: 'Email', detail_type: 'text_field', required: true },\n * ];\n *\n * <BookingForm\n * questions={questions}\n * onSubmit={(values) => console.log(values)}\n * />\n * ```\n */\nexport function BookingForm({\n questions,\n onSubmit,\n submitLabel = 'Submit',\n className = '',\n labelClassName,\n classNames: classNamesProp,\n}: BookingFormProps) {\n // Merge deprecated labelClassName into classNames for backwards compatibility\n const classNames: BookingFormClassNames = {\n ...classNamesProp,\n label: classNamesProp?.label ?? labelClassName,\n };\n const [values, setValues] = useState<FormValues>({});\n const [errors, setErrors] = useState<FormErrors>({});\n const [touched, setTouched] = useState<Record<number, boolean>>({});\n\n\n // Check if a question should be visible based on conditional settings\n const isQuestionVisible = useCallback(\n (question: Question): boolean => {\n const { settings } = question;\n if (!settings?.conditional_answers) {\n return true;\n }\n\n // Check all conditions in conditional_answers\n // Each entry is { questionId: expectedAnswerValue }\n const conditionEntries = Object.entries(settings.conditional_answers as Record<string, unknown>);\n if (conditionEntries.length === 0) {\n return true;\n }\n\n // Question is visible if ANY condition is met\n return conditionEntries.some(([questionIdStr, expectedAnswer]) => {\n const questionId = Number(questionIdStr);\n const currentValue = (values as Record<string, unknown>)[String(questionId)];\n\n if (currentValue === undefined || currentValue === '' || currentValue === null) {\n return false;\n }\n\n // Selects typically store the option id (as string), but allow `{ id, name }` objects too\n const normalizedCurrent =\n typeof currentValue === 'object' && currentValue !== null && 'id' in (currentValue as any)\n ? (currentValue as any)\n : currentValue;\n\n // Build a set of candidate strings for comparison.\n // For select questions we support matching by BOTH option id and option name.\n const candidates = new Set<string>();\n\n const addCandidate = (v: unknown) => {\n if (v === undefined || v === null) return;\n const s = String(v).trim();\n if (s) candidates.add(s);\n };\n\n const sourceQuestion = questions.find((q) => q.id === questionId);\n\n if (typeof normalizedCurrent === 'object' && normalizedCurrent !== null) {\n addCandidate((normalizedCurrent as any).id);\n addCandidate((normalizedCurrent as any).name);\n } else {\n addCandidate(normalizedCurrent);\n }\n\n if (sourceQuestion?.detail_type === 'select' && sourceQuestion.options?.length) {\n // Try to resolve the selected option via id or name.\n const currentStr = [...candidates][0] ?? '';\n const optionById = sourceQuestion.options.find((o) => String(o.id) === currentStr);\n const optionByName = sourceQuestion.options.find((o) => String(o.name).trim() === currentStr);\n const opt = optionById ?? optionByName;\n if (opt) {\n addCandidate(opt.id);\n addCandidate(opt.name);\n }\n }\n\n const matchesScalar = (expected: unknown) => {\n const expectedStr = String(expected).trim();\n return candidates.has(expectedStr);\n };\n\n // Support multiple expected formats:\n // - scalar: \"Consultation\" / \"1\" / 1 / true\n // - array: [\"Consultation\", \"Follow-up\"]\n // - map/object: { \"Consultation\": true }\n if (Array.isArray(expectedAnswer)) {\n return expectedAnswer.some((v) => matchesScalar(v));\n }\n\n if (expectedAnswer && typeof expectedAnswer === 'object') {\n const expectedMap = expectedAnswer as Record<string, unknown>;\n for (const c of candidates) {\n if (Object.prototype.hasOwnProperty.call(expectedMap, c)) {\n const flag = expectedMap[c];\n return flag === undefined ? true : !!flag;\n }\n }\n return false;\n }\n\n return matchesScalar(expectedAnswer);\n });\n },\n [values, questions]\n );\n\n // Get visible questions\n const visibleQuestions = questions.filter(isQuestionVisible);\n\n\n // Validate a single field\n const validateField = useCallback((question: Question, value: FormValues[number]): string | null => {\n if (question.detail_type === 'heading') return null;\n\n if (question.required) {\n if (value === undefined || value === '' || value === null) {\n return `${question.name} is required`;\n }\n if (question.detail_type === 'check' && !value) {\n return `${question.name} must be checked`;\n }\n }\n\n if (question.detail_type === 'number' && value !== '' && value !== undefined) {\n const numValue = Number(value);\n if (question.settings?.min !== undefined && numValue < question.settings.min) {\n return `Minimum value is ${question.settings.min}`;\n }\n if (question.settings?.max !== undefined && numValue > question.settings.max) {\n return `Maximum value is ${question.settings.max}`;\n }\n }\n\n return null;\n }, []);\n\n // Validate all visible fields\n const validateAll = useCallback((): boolean => {\n const newErrors: FormErrors = {};\n let isValid = true;\n\n visibleQuestions.forEach((question) => {\n const error = validateField(question, values[question.id]);\n if (error) {\n newErrors[question.id] = error;\n isValid = false;\n }\n });\n\n setErrors(newErrors);\n return isValid;\n }, [visibleQuestions, values, validateField]);\n\n // Clear errors for hidden questions\n useEffect(() => {\n const visibleIds = new Set(visibleQuestions.map((q) => q.id));\n\n setErrors((prev) => {\n let changed = false;\n const next = { ...prev };\n\n Object.keys(next).forEach((idStr) => {\n const id = Number(idStr);\n if (!visibleIds.has(id)) {\n delete next[id];\n changed = true;\n }\n });\n\n return changed ? next : prev;\n });\n }, [visibleQuestions]);\n\n const handleChange = (questionId: number, value: string | number | boolean) => {\n setValues((prev) => ({ ...prev, [questionId]: value }));\n setTouched((prev) => ({ ...prev, [questionId]: true }));\n\n // Clear error on change if touched\n if (touched[questionId]) {\n const question = questions.find((q) => q.id === questionId);\n if (question) {\n const error = validateField(question, value);\n setErrors((prev) => {\n if (error) {\n return { ...prev, [questionId]: error };\n }\n const { [questionId]: _, ...rest } = prev;\n return rest;\n });\n }\n }\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n\n // Mark all fields as touched\n const allTouched: Record<number, boolean> = {};\n visibleQuestions.forEach((q) => {\n allTouched[q.id] = true;\n });\n setTouched(allTouched);\n\n if (validateAll()) {\n // Only include visible question values\n const visibleValues: FormValues = {};\n visibleQuestions.forEach((q) => {\n if (q.detail_type !== 'heading' && values[q.id] !== undefined) {\n visibleValues[q.id] = values[q.id];\n }\n });\n onSubmit(visibleValues);\n }\n };\n\n const defaultButton = 'w-full mt-4 px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors';\n\n return (\n <form onSubmit={handleSubmit} className={className} noValidate>\n {visibleQuestions.map((question) => (\n <FormField\n key={question.id}\n question={question}\n value={values[question.id]}\n error={touched[question.id] ? errors[question.id] : undefined}\n onChange={(value) => handleChange(question.id, value)}\n classNames={classNames}\n />\n ))}\n <button\n type=\"submit\"\n className={classNames?.button ?? defaultButton}\n >\n {submitLabel}\n </button>\n </form>\n );\n}\n\nexport default BookingForm;\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface QuestionOption {
|
|
4
|
+
id: number;
|
|
5
|
+
name: string;
|
|
6
|
+
price?: number;
|
|
7
|
+
is_default?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface QuestionSettings {
|
|
10
|
+
conditional_question?: string;
|
|
11
|
+
conditional_answers?: Record<string, string>;
|
|
12
|
+
min?: number;
|
|
13
|
+
max?: number;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
}
|
|
16
|
+
interface Question {
|
|
17
|
+
id: number;
|
|
18
|
+
name: string;
|
|
19
|
+
detail_type: 'heading' | 'text_field' | 'text_area' | 'select' | 'date' | 'number' | 'check';
|
|
20
|
+
required?: boolean;
|
|
21
|
+
important?: boolean;
|
|
22
|
+
admin_only?: boolean;
|
|
23
|
+
applies_to?: number;
|
|
24
|
+
ask_member?: boolean;
|
|
25
|
+
options?: QuestionOption[];
|
|
26
|
+
settings?: QuestionSettings;
|
|
27
|
+
help_text?: string;
|
|
28
|
+
price?: number;
|
|
29
|
+
price_per_booking?: boolean;
|
|
30
|
+
outcome?: boolean;
|
|
31
|
+
hide_on_customer_journey?: boolean;
|
|
32
|
+
}
|
|
33
|
+
type FormValues = Record<number, string | number | boolean>;
|
|
34
|
+
type FormErrors = Record<number, string>;
|
|
35
|
+
/**
|
|
36
|
+
* Custom class names for styling form elements
|
|
37
|
+
*/
|
|
38
|
+
interface BookingFormClassNames {
|
|
39
|
+
/** Wrapper for each form field */
|
|
40
|
+
fieldWrapper?: string;
|
|
41
|
+
/** All labels */
|
|
42
|
+
label?: string;
|
|
43
|
+
/** Heading elements (detail_type: 'heading') */
|
|
44
|
+
heading?: string;
|
|
45
|
+
/** All input elements (text, textarea, select, date, number) */
|
|
46
|
+
input?: string;
|
|
47
|
+
/** Input elements in error state */
|
|
48
|
+
inputError?: string;
|
|
49
|
+
/** Checkbox input specifically */
|
|
50
|
+
checkbox?: string;
|
|
51
|
+
/** Help text below fields */
|
|
52
|
+
helpText?: string;
|
|
53
|
+
/** Error message text */
|
|
54
|
+
errorText?: string;
|
|
55
|
+
/** Submit button */
|
|
56
|
+
button?: string;
|
|
57
|
+
}
|
|
58
|
+
interface BookingFormProps {
|
|
59
|
+
questions: Question[];
|
|
60
|
+
onSubmit: (values: FormValues) => void;
|
|
61
|
+
submitLabel?: string;
|
|
62
|
+
/** Class name for the form element */
|
|
63
|
+
className?: string;
|
|
64
|
+
/** @deprecated Use classNames.label instead */
|
|
65
|
+
labelClassName?: string;
|
|
66
|
+
/** Custom class names for styling individual elements */
|
|
67
|
+
classNames?: BookingFormClassNames;
|
|
68
|
+
}
|
|
69
|
+
interface BookingUIConfig {
|
|
70
|
+
theme?: 'light' | 'dark' | 'system';
|
|
71
|
+
locale?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* BookingForm - A dynamic form component for booking questions
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```tsx
|
|
79
|
+
* const questions: Question[] = [
|
|
80
|
+
* { id: 1, name: 'Personal Details', detail_type: 'heading' },
|
|
81
|
+
* { id: 2, name: 'Full Name', detail_type: 'text_field', required: true },
|
|
82
|
+
* { id: 3, name: 'Email', detail_type: 'text_field', required: true },
|
|
83
|
+
* ];
|
|
84
|
+
*
|
|
85
|
+
* <BookingForm
|
|
86
|
+
* questions={questions}
|
|
87
|
+
* onSubmit={(values) => console.log(values)}
|
|
88
|
+
* />
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function BookingForm({ questions, onSubmit, submitLabel, className, labelClassName, classNames: classNamesProp, }: BookingFormProps): react_jsx_runtime.JSX.Element;
|
|
92
|
+
|
|
93
|
+
export { BookingForm, type BookingFormClassNames, type BookingFormProps, type BookingUIConfig, type FormErrors, type FormValues, type Question, type QuestionOption, type QuestionSettings };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface QuestionOption {
|
|
4
|
+
id: number;
|
|
5
|
+
name: string;
|
|
6
|
+
price?: number;
|
|
7
|
+
is_default?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface QuestionSettings {
|
|
10
|
+
conditional_question?: string;
|
|
11
|
+
conditional_answers?: Record<string, string>;
|
|
12
|
+
min?: number;
|
|
13
|
+
max?: number;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
}
|
|
16
|
+
interface Question {
|
|
17
|
+
id: number;
|
|
18
|
+
name: string;
|
|
19
|
+
detail_type: 'heading' | 'text_field' | 'text_area' | 'select' | 'date' | 'number' | 'check';
|
|
20
|
+
required?: boolean;
|
|
21
|
+
important?: boolean;
|
|
22
|
+
admin_only?: boolean;
|
|
23
|
+
applies_to?: number;
|
|
24
|
+
ask_member?: boolean;
|
|
25
|
+
options?: QuestionOption[];
|
|
26
|
+
settings?: QuestionSettings;
|
|
27
|
+
help_text?: string;
|
|
28
|
+
price?: number;
|
|
29
|
+
price_per_booking?: boolean;
|
|
30
|
+
outcome?: boolean;
|
|
31
|
+
hide_on_customer_journey?: boolean;
|
|
32
|
+
}
|
|
33
|
+
type FormValues = Record<number, string | number | boolean>;
|
|
34
|
+
type FormErrors = Record<number, string>;
|
|
35
|
+
/**
|
|
36
|
+
* Custom class names for styling form elements
|
|
37
|
+
*/
|
|
38
|
+
interface BookingFormClassNames {
|
|
39
|
+
/** Wrapper for each form field */
|
|
40
|
+
fieldWrapper?: string;
|
|
41
|
+
/** All labels */
|
|
42
|
+
label?: string;
|
|
43
|
+
/** Heading elements (detail_type: 'heading') */
|
|
44
|
+
heading?: string;
|
|
45
|
+
/** All input elements (text, textarea, select, date, number) */
|
|
46
|
+
input?: string;
|
|
47
|
+
/** Input elements in error state */
|
|
48
|
+
inputError?: string;
|
|
49
|
+
/** Checkbox input specifically */
|
|
50
|
+
checkbox?: string;
|
|
51
|
+
/** Help text below fields */
|
|
52
|
+
helpText?: string;
|
|
53
|
+
/** Error message text */
|
|
54
|
+
errorText?: string;
|
|
55
|
+
/** Submit button */
|
|
56
|
+
button?: string;
|
|
57
|
+
}
|
|
58
|
+
interface BookingFormProps {
|
|
59
|
+
questions: Question[];
|
|
60
|
+
onSubmit: (values: FormValues) => void;
|
|
61
|
+
submitLabel?: string;
|
|
62
|
+
/** Class name for the form element */
|
|
63
|
+
className?: string;
|
|
64
|
+
/** @deprecated Use classNames.label instead */
|
|
65
|
+
labelClassName?: string;
|
|
66
|
+
/** Custom class names for styling individual elements */
|
|
67
|
+
classNames?: BookingFormClassNames;
|
|
68
|
+
}
|
|
69
|
+
interface BookingUIConfig {
|
|
70
|
+
theme?: 'light' | 'dark' | 'system';
|
|
71
|
+
locale?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* BookingForm - A dynamic form component for booking questions
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```tsx
|
|
79
|
+
* const questions: Question[] = [
|
|
80
|
+
* { id: 1, name: 'Personal Details', detail_type: 'heading' },
|
|
81
|
+
* { id: 2, name: 'Full Name', detail_type: 'text_field', required: true },
|
|
82
|
+
* { id: 3, name: 'Email', detail_type: 'text_field', required: true },
|
|
83
|
+
* ];
|
|
84
|
+
*
|
|
85
|
+
* <BookingForm
|
|
86
|
+
* questions={questions}
|
|
87
|
+
* onSubmit={(values) => console.log(values)}
|
|
88
|
+
* />
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function BookingForm({ questions, onSubmit, submitLabel, className, labelClassName, classNames: classNamesProp, }: BookingFormProps): react_jsx_runtime.JSX.Element;
|
|
92
|
+
|
|
93
|
+
export { BookingForm, type BookingFormClassNames, type BookingFormProps, type BookingUIConfig, type FormErrors, type FormValues, type Question, type QuestionOption, type QuestionSettings };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/components/BookingForm.tsx
|
|
5
|
+
var cx = (...classes) => classes.filter(Boolean).join(" ");
|
|
6
|
+
function FormField({
|
|
7
|
+
question,
|
|
8
|
+
value,
|
|
9
|
+
error,
|
|
10
|
+
onChange,
|
|
11
|
+
classNames
|
|
12
|
+
}) {
|
|
13
|
+
const inputId = `question-${question.id}`;
|
|
14
|
+
const errorId = `${inputId}-error`;
|
|
15
|
+
const hasError = !!error;
|
|
16
|
+
const defaultFieldWrapper = "mb-4";
|
|
17
|
+
const defaultLabel = "block text-sm font-medium mb-1 text-gray-700";
|
|
18
|
+
const defaultHeading = "text-lg font-semibold text-gray-900 mt-4 mb-2 first:mt-0";
|
|
19
|
+
const defaultInput = "w-full px-3 py-2 border rounded-md text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 border-gray-300 focus:ring-blue-500 focus:border-blue-500";
|
|
20
|
+
const defaultInputError = "border-red-500 focus:ring-red-500";
|
|
21
|
+
const defaultCheckbox = "mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500";
|
|
22
|
+
const defaultHelpText = "mt-1 text-xs text-gray-500";
|
|
23
|
+
const defaultErrorText = "mt-1 text-xs text-red-600";
|
|
24
|
+
const inputClasses = hasError ? cx(classNames?.input ?? defaultInput, classNames?.inputError ?? defaultInputError) : cx(classNames?.input ?? defaultInput);
|
|
25
|
+
const labelClasses = classNames?.label ?? defaultLabel;
|
|
26
|
+
const checkboxLabelClasses = cx("text-sm", classNames?.label ?? "text-gray-700");
|
|
27
|
+
const renderLabel = () => {
|
|
28
|
+
if (question.detail_type === "heading") return null;
|
|
29
|
+
return /* @__PURE__ */ jsxs("label", { htmlFor: inputId, className: labelClasses, children: [
|
|
30
|
+
question.name,
|
|
31
|
+
question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
|
|
32
|
+
] });
|
|
33
|
+
};
|
|
34
|
+
const renderHelpText = () => {
|
|
35
|
+
if (!question.help_text) return null;
|
|
36
|
+
return /* @__PURE__ */ jsx("p", { className: classNames?.helpText ?? defaultHelpText, children: question.help_text });
|
|
37
|
+
};
|
|
38
|
+
const renderError = () => {
|
|
39
|
+
if (!error) return null;
|
|
40
|
+
return /* @__PURE__ */ jsx("p", { id: errorId, className: classNames?.errorText ?? defaultErrorText, role: "alert", children: error });
|
|
41
|
+
};
|
|
42
|
+
const ariaProps = {
|
|
43
|
+
"aria-invalid": hasError ? true : void 0,
|
|
44
|
+
"aria-describedby": hasError ? errorId : void 0,
|
|
45
|
+
"aria-required": question.required || void 0
|
|
46
|
+
};
|
|
47
|
+
switch (question.detail_type) {
|
|
48
|
+
case "heading":
|
|
49
|
+
return /* @__PURE__ */ jsx("h3", { className: classNames?.heading ?? defaultHeading, children: question.name });
|
|
50
|
+
case "text_field":
|
|
51
|
+
return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
52
|
+
renderLabel(),
|
|
53
|
+
/* @__PURE__ */ jsx(
|
|
54
|
+
"input",
|
|
55
|
+
{
|
|
56
|
+
id: inputId,
|
|
57
|
+
type: "text",
|
|
58
|
+
value: value || "",
|
|
59
|
+
onChange: (e) => onChange(e.target.value),
|
|
60
|
+
placeholder: question.settings?.placeholder,
|
|
61
|
+
className: inputClasses,
|
|
62
|
+
...ariaProps
|
|
63
|
+
}
|
|
64
|
+
),
|
|
65
|
+
renderHelpText(),
|
|
66
|
+
renderError()
|
|
67
|
+
] });
|
|
68
|
+
case "text_area":
|
|
69
|
+
return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
70
|
+
renderLabel(),
|
|
71
|
+
/* @__PURE__ */ jsx(
|
|
72
|
+
"textarea",
|
|
73
|
+
{
|
|
74
|
+
id: inputId,
|
|
75
|
+
value: value || "",
|
|
76
|
+
onChange: (e) => onChange(e.target.value),
|
|
77
|
+
placeholder: question.settings?.placeholder,
|
|
78
|
+
rows: 4,
|
|
79
|
+
className: inputClasses,
|
|
80
|
+
...ariaProps
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
renderHelpText(),
|
|
84
|
+
renderError()
|
|
85
|
+
] });
|
|
86
|
+
case "select":
|
|
87
|
+
return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
88
|
+
renderLabel(),
|
|
89
|
+
/* @__PURE__ */ jsxs(
|
|
90
|
+
"select",
|
|
91
|
+
{
|
|
92
|
+
id: inputId,
|
|
93
|
+
value: value ?? "",
|
|
94
|
+
onChange: (e) => onChange(e.target.value),
|
|
95
|
+
className: inputClasses,
|
|
96
|
+
...ariaProps,
|
|
97
|
+
children: [
|
|
98
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "Select an option" }),
|
|
99
|
+
question.options?.map((option) => /* @__PURE__ */ jsx("option", { value: option.id, children: option.name }, option.id))
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
),
|
|
103
|
+
renderHelpText(),
|
|
104
|
+
renderError()
|
|
105
|
+
] });
|
|
106
|
+
case "date":
|
|
107
|
+
return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
108
|
+
renderLabel(),
|
|
109
|
+
/* @__PURE__ */ jsx(
|
|
110
|
+
"input",
|
|
111
|
+
{
|
|
112
|
+
id: inputId,
|
|
113
|
+
type: "date",
|
|
114
|
+
value: value || "",
|
|
115
|
+
onChange: (e) => onChange(e.target.value),
|
|
116
|
+
min: question.settings?.min?.toString(),
|
|
117
|
+
max: question.settings?.max?.toString(),
|
|
118
|
+
className: inputClasses,
|
|
119
|
+
...ariaProps
|
|
120
|
+
}
|
|
121
|
+
),
|
|
122
|
+
renderHelpText(),
|
|
123
|
+
renderError()
|
|
124
|
+
] });
|
|
125
|
+
case "number":
|
|
126
|
+
return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
127
|
+
renderLabel(),
|
|
128
|
+
/* @__PURE__ */ jsx(
|
|
129
|
+
"input",
|
|
130
|
+
{
|
|
131
|
+
id: inputId,
|
|
132
|
+
type: "number",
|
|
133
|
+
value: value ?? "",
|
|
134
|
+
onChange: (e) => onChange(e.target.value ? Number(e.target.value) : ""),
|
|
135
|
+
min: question.settings?.min,
|
|
136
|
+
max: question.settings?.max,
|
|
137
|
+
placeholder: question.settings?.placeholder,
|
|
138
|
+
className: inputClasses,
|
|
139
|
+
...ariaProps
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
renderHelpText(),
|
|
143
|
+
renderError()
|
|
144
|
+
] });
|
|
145
|
+
case "check":
|
|
146
|
+
return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
|
|
147
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
|
|
148
|
+
/* @__PURE__ */ jsx(
|
|
149
|
+
"input",
|
|
150
|
+
{
|
|
151
|
+
id: inputId,
|
|
152
|
+
type: "checkbox",
|
|
153
|
+
checked: !!value,
|
|
154
|
+
onChange: (e) => onChange(e.target.checked),
|
|
155
|
+
className: classNames?.checkbox ?? defaultCheckbox,
|
|
156
|
+
...ariaProps
|
|
157
|
+
}
|
|
158
|
+
),
|
|
159
|
+
/* @__PURE__ */ jsxs("label", { htmlFor: inputId, className: checkboxLabelClasses, children: [
|
|
160
|
+
question.name,
|
|
161
|
+
question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
|
|
162
|
+
] })
|
|
163
|
+
] }),
|
|
164
|
+
renderHelpText(),
|
|
165
|
+
renderError()
|
|
166
|
+
] });
|
|
167
|
+
default:
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function BookingForm({
|
|
172
|
+
questions,
|
|
173
|
+
onSubmit,
|
|
174
|
+
submitLabel = "Submit",
|
|
175
|
+
className = "",
|
|
176
|
+
labelClassName,
|
|
177
|
+
classNames: classNamesProp
|
|
178
|
+
}) {
|
|
179
|
+
const classNames = {
|
|
180
|
+
...classNamesProp,
|
|
181
|
+
label: classNamesProp?.label ?? labelClassName
|
|
182
|
+
};
|
|
183
|
+
const [values, setValues] = useState({});
|
|
184
|
+
const [errors, setErrors] = useState({});
|
|
185
|
+
const [touched, setTouched] = useState({});
|
|
186
|
+
const isQuestionVisible = useCallback(
|
|
187
|
+
(question) => {
|
|
188
|
+
const { settings } = question;
|
|
189
|
+
if (!settings?.conditional_answers) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
const conditionEntries = Object.entries(settings.conditional_answers);
|
|
193
|
+
if (conditionEntries.length === 0) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
return conditionEntries.some(([questionIdStr, expectedAnswer]) => {
|
|
197
|
+
const questionId = Number(questionIdStr);
|
|
198
|
+
const currentValue = values[String(questionId)];
|
|
199
|
+
if (currentValue === void 0 || currentValue === "" || currentValue === null) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
const normalizedCurrent = typeof currentValue === "object" && currentValue !== null && "id" in currentValue ? currentValue : currentValue;
|
|
203
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
204
|
+
const addCandidate = (v) => {
|
|
205
|
+
if (v === void 0 || v === null) return;
|
|
206
|
+
const s = String(v).trim();
|
|
207
|
+
if (s) candidates.add(s);
|
|
208
|
+
};
|
|
209
|
+
const sourceQuestion = questions.find((q) => q.id === questionId);
|
|
210
|
+
if (typeof normalizedCurrent === "object" && normalizedCurrent !== null) {
|
|
211
|
+
addCandidate(normalizedCurrent.id);
|
|
212
|
+
addCandidate(normalizedCurrent.name);
|
|
213
|
+
} else {
|
|
214
|
+
addCandidate(normalizedCurrent);
|
|
215
|
+
}
|
|
216
|
+
if (sourceQuestion?.detail_type === "select" && sourceQuestion.options?.length) {
|
|
217
|
+
const currentStr = [...candidates][0] ?? "";
|
|
218
|
+
const optionById = sourceQuestion.options.find((o) => String(o.id) === currentStr);
|
|
219
|
+
const optionByName = sourceQuestion.options.find((o) => String(o.name).trim() === currentStr);
|
|
220
|
+
const opt = optionById ?? optionByName;
|
|
221
|
+
if (opt) {
|
|
222
|
+
addCandidate(opt.id);
|
|
223
|
+
addCandidate(opt.name);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const matchesScalar = (expected) => {
|
|
227
|
+
const expectedStr = String(expected).trim();
|
|
228
|
+
return candidates.has(expectedStr);
|
|
229
|
+
};
|
|
230
|
+
if (Array.isArray(expectedAnswer)) {
|
|
231
|
+
return expectedAnswer.some((v) => matchesScalar(v));
|
|
232
|
+
}
|
|
233
|
+
if (expectedAnswer && typeof expectedAnswer === "object") {
|
|
234
|
+
const expectedMap = expectedAnswer;
|
|
235
|
+
for (const c of candidates) {
|
|
236
|
+
if (Object.prototype.hasOwnProperty.call(expectedMap, c)) {
|
|
237
|
+
const flag = expectedMap[c];
|
|
238
|
+
return flag === void 0 ? true : !!flag;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
return matchesScalar(expectedAnswer);
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
[values, questions]
|
|
247
|
+
);
|
|
248
|
+
const visibleQuestions = questions.filter(isQuestionVisible);
|
|
249
|
+
const validateField = useCallback((question, value) => {
|
|
250
|
+
if (question.detail_type === "heading") return null;
|
|
251
|
+
if (question.required) {
|
|
252
|
+
if (value === void 0 || value === "" || value === null) {
|
|
253
|
+
return `${question.name} is required`;
|
|
254
|
+
}
|
|
255
|
+
if (question.detail_type === "check" && !value) {
|
|
256
|
+
return `${question.name} must be checked`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (question.detail_type === "number" && value !== "" && value !== void 0) {
|
|
260
|
+
const numValue = Number(value);
|
|
261
|
+
if (question.settings?.min !== void 0 && numValue < question.settings.min) {
|
|
262
|
+
return `Minimum value is ${question.settings.min}`;
|
|
263
|
+
}
|
|
264
|
+
if (question.settings?.max !== void 0 && numValue > question.settings.max) {
|
|
265
|
+
return `Maximum value is ${question.settings.max}`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}, []);
|
|
270
|
+
const validateAll = useCallback(() => {
|
|
271
|
+
const newErrors = {};
|
|
272
|
+
let isValid = true;
|
|
273
|
+
visibleQuestions.forEach((question) => {
|
|
274
|
+
const error = validateField(question, values[question.id]);
|
|
275
|
+
if (error) {
|
|
276
|
+
newErrors[question.id] = error;
|
|
277
|
+
isValid = false;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
setErrors(newErrors);
|
|
281
|
+
return isValid;
|
|
282
|
+
}, [visibleQuestions, values, validateField]);
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
const visibleIds = new Set(visibleQuestions.map((q) => q.id));
|
|
285
|
+
setErrors((prev) => {
|
|
286
|
+
let changed = false;
|
|
287
|
+
const next = { ...prev };
|
|
288
|
+
Object.keys(next).forEach((idStr) => {
|
|
289
|
+
const id = Number(idStr);
|
|
290
|
+
if (!visibleIds.has(id)) {
|
|
291
|
+
delete next[id];
|
|
292
|
+
changed = true;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
return changed ? next : prev;
|
|
296
|
+
});
|
|
297
|
+
}, [visibleQuestions]);
|
|
298
|
+
const handleChange = (questionId, value) => {
|
|
299
|
+
setValues((prev) => ({ ...prev, [questionId]: value }));
|
|
300
|
+
setTouched((prev) => ({ ...prev, [questionId]: true }));
|
|
301
|
+
if (touched[questionId]) {
|
|
302
|
+
const question = questions.find((q) => q.id === questionId);
|
|
303
|
+
if (question) {
|
|
304
|
+
const error = validateField(question, value);
|
|
305
|
+
setErrors((prev) => {
|
|
306
|
+
if (error) {
|
|
307
|
+
return { ...prev, [questionId]: error };
|
|
308
|
+
}
|
|
309
|
+
const { [questionId]: _, ...rest } = prev;
|
|
310
|
+
return rest;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const handleSubmit = (e) => {
|
|
316
|
+
e.preventDefault();
|
|
317
|
+
const allTouched = {};
|
|
318
|
+
visibleQuestions.forEach((q) => {
|
|
319
|
+
allTouched[q.id] = true;
|
|
320
|
+
});
|
|
321
|
+
setTouched(allTouched);
|
|
322
|
+
if (validateAll()) {
|
|
323
|
+
const visibleValues = {};
|
|
324
|
+
visibleQuestions.forEach((q) => {
|
|
325
|
+
if (q.detail_type !== "heading" && values[q.id] !== void 0) {
|
|
326
|
+
visibleValues[q.id] = values[q.id];
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
onSubmit(visibleValues);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
const defaultButton = "w-full mt-4 px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors";
|
|
333
|
+
return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className, noValidate: true, children: [
|
|
334
|
+
visibleQuestions.map((question) => /* @__PURE__ */ jsx(
|
|
335
|
+
FormField,
|
|
336
|
+
{
|
|
337
|
+
question,
|
|
338
|
+
value: values[question.id],
|
|
339
|
+
error: touched[question.id] ? errors[question.id] : void 0,
|
|
340
|
+
onChange: (value) => handleChange(question.id, value),
|
|
341
|
+
classNames
|
|
342
|
+
},
|
|
343
|
+
question.id
|
|
344
|
+
)),
|
|
345
|
+
/* @__PURE__ */ jsx(
|
|
346
|
+
"button",
|
|
347
|
+
{
|
|
348
|
+
type: "submit",
|
|
349
|
+
className: classNames?.button ?? defaultButton,
|
|
350
|
+
children: submitLabel
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
] });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export { BookingForm };
|
|
357
|
+
//# sourceMappingURL=index.js.map
|
|
358
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/BookingForm.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import React, { useState, useEffect, useCallback } from 'react';\nimport type { Question, FormValues, FormErrors, BookingFormProps, BookingFormClassNames } from '../types';\n\n/** Merge base classes with optional custom classes */\nconst cx = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ');\n\n/**\n * Renders a single form field based on question type\n */\nfunction FormField({\n question,\n value,\n error,\n onChange,\n classNames,\n}: {\n question: Question;\n value: string | number | boolean | undefined;\n error?: string;\n onChange: (value: string | number | boolean) => void;\n classNames?: BookingFormClassNames;\n}) {\n const inputId = `question-${question.id}`;\n const errorId = `${inputId}-error`;\n const hasError = !!error;\n\n // Default classes (can be overridden via classNames prop)\n const defaultFieldWrapper = 'mb-4';\n const defaultLabel = 'block text-sm font-medium mb-1 text-gray-700';\n const defaultHeading = 'text-lg font-semibold text-gray-900 mt-4 mb-2 first:mt-0';\n const defaultInput = 'w-full px-3 py-2 border rounded-md text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 border-gray-300 focus:ring-blue-500 focus:border-blue-500';\n const defaultInputError = 'border-red-500 focus:ring-red-500';\n const defaultCheckbox = 'mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500';\n const defaultHelpText = 'mt-1 text-xs text-gray-500';\n const defaultErrorText = 'mt-1 text-xs text-red-600';\n\n const inputClasses = hasError\n ? cx(classNames?.input ?? defaultInput, classNames?.inputError ?? defaultInputError)\n : cx(classNames?.input ?? defaultInput);\n\n const labelClasses = classNames?.label ?? defaultLabel;\n const checkboxLabelClasses = cx('text-sm', classNames?.label ?? 'text-gray-700');\n\n const renderLabel = () => {\n if (question.detail_type === 'heading') return null;\n return (\n <label htmlFor={inputId} className={labelClasses}>\n {question.name}\n {question.required && <span className=\"text-red-500 ml-1\" aria-hidden=\"true\">*</span>}\n </label>\n );\n };\n\n const renderHelpText = () => {\n if (!question.help_text) return null;\n return <p className={classNames?.helpText ?? defaultHelpText}>{question.help_text}</p>;\n };\n\n const renderError = () => {\n if (!error) return null;\n return (\n <p id={errorId} className={classNames?.errorText ?? defaultErrorText} role=\"alert\">\n {error}\n </p>\n );\n };\n\n const ariaProps = {\n 'aria-invalid': hasError ? true : undefined,\n 'aria-describedby': hasError ? errorId : undefined,\n 'aria-required': question.required || undefined,\n };\n\n switch (question.detail_type) {\n case 'heading':\n return (\n <h3 className={classNames?.heading ?? defaultHeading}>\n {question.name}\n </h3>\n );\n\n case 'text_field':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"text\"\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n placeholder={question.settings?.placeholder}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'text_area':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <textarea\n id={inputId}\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n placeholder={question.settings?.placeholder}\n rows={4}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'select':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <select\n id={inputId}\n value={(value as string | number) ?? ''}\n onChange={(e) => onChange(e.target.value)}\n className={inputClasses}\n {...ariaProps}\n >\n <option value=\"\">Select an option</option>\n {question.options?.map((option) => (\n <option key={option.id} value={option.id}>\n {option.name}\n </option>\n ))}\n </select>\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'date':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"date\"\n value={(value as string) || ''}\n onChange={(e) => onChange(e.target.value)}\n min={question.settings?.min?.toString()}\n max={question.settings?.max?.toString()}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'number':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n {renderLabel()}\n <input\n id={inputId}\n type=\"number\"\n value={(value as number) ?? ''}\n onChange={(e) => onChange(e.target.value ? Number(e.target.value) : '')}\n min={question.settings?.min}\n max={question.settings?.max}\n placeholder={question.settings?.placeholder}\n className={inputClasses}\n {...ariaProps}\n />\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n case 'check':\n return (\n <div className={classNames?.fieldWrapper ?? defaultFieldWrapper}>\n <div className=\"flex items-start gap-2\">\n <input\n id={inputId}\n type=\"checkbox\"\n checked={!!value}\n onChange={(e) => onChange(e.target.checked)}\n className={classNames?.checkbox ?? defaultCheckbox}\n {...ariaProps}\n />\n <label htmlFor={inputId} className={checkboxLabelClasses}>\n {question.name}\n {question.required && <span className=\"text-red-500 ml-1\" aria-hidden=\"true\">*</span>}\n </label>\n </div>\n {renderHelpText()}\n {renderError()}\n </div>\n );\n\n default:\n return null;\n }\n}\n\n/**\n * BookingForm - A dynamic form component for booking questions\n *\n * @example\n * ```tsx\n * const questions: Question[] = [\n * { id: 1, name: 'Personal Details', detail_type: 'heading' },\n * { id: 2, name: 'Full Name', detail_type: 'text_field', required: true },\n * { id: 3, name: 'Email', detail_type: 'text_field', required: true },\n * ];\n *\n * <BookingForm\n * questions={questions}\n * onSubmit={(values) => console.log(values)}\n * />\n * ```\n */\nexport function BookingForm({\n questions,\n onSubmit,\n submitLabel = 'Submit',\n className = '',\n labelClassName,\n classNames: classNamesProp,\n}: BookingFormProps) {\n // Merge deprecated labelClassName into classNames for backwards compatibility\n const classNames: BookingFormClassNames = {\n ...classNamesProp,\n label: classNamesProp?.label ?? labelClassName,\n };\n const [values, setValues] = useState<FormValues>({});\n const [errors, setErrors] = useState<FormErrors>({});\n const [touched, setTouched] = useState<Record<number, boolean>>({});\n\n\n // Check if a question should be visible based on conditional settings\n const isQuestionVisible = useCallback(\n (question: Question): boolean => {\n const { settings } = question;\n if (!settings?.conditional_answers) {\n return true;\n }\n\n // Check all conditions in conditional_answers\n // Each entry is { questionId: expectedAnswerValue }\n const conditionEntries = Object.entries(settings.conditional_answers as Record<string, unknown>);\n if (conditionEntries.length === 0) {\n return true;\n }\n\n // Question is visible if ANY condition is met\n return conditionEntries.some(([questionIdStr, expectedAnswer]) => {\n const questionId = Number(questionIdStr);\n const currentValue = (values as Record<string, unknown>)[String(questionId)];\n\n if (currentValue === undefined || currentValue === '' || currentValue === null) {\n return false;\n }\n\n // Selects typically store the option id (as string), but allow `{ id, name }` objects too\n const normalizedCurrent =\n typeof currentValue === 'object' && currentValue !== null && 'id' in (currentValue as any)\n ? (currentValue as any)\n : currentValue;\n\n // Build a set of candidate strings for comparison.\n // For select questions we support matching by BOTH option id and option name.\n const candidates = new Set<string>();\n\n const addCandidate = (v: unknown) => {\n if (v === undefined || v === null) return;\n const s = String(v).trim();\n if (s) candidates.add(s);\n };\n\n const sourceQuestion = questions.find((q) => q.id === questionId);\n\n if (typeof normalizedCurrent === 'object' && normalizedCurrent !== null) {\n addCandidate((normalizedCurrent as any).id);\n addCandidate((normalizedCurrent as any).name);\n } else {\n addCandidate(normalizedCurrent);\n }\n\n if (sourceQuestion?.detail_type === 'select' && sourceQuestion.options?.length) {\n // Try to resolve the selected option via id or name.\n const currentStr = [...candidates][0] ?? '';\n const optionById = sourceQuestion.options.find((o) => String(o.id) === currentStr);\n const optionByName = sourceQuestion.options.find((o) => String(o.name).trim() === currentStr);\n const opt = optionById ?? optionByName;\n if (opt) {\n addCandidate(opt.id);\n addCandidate(opt.name);\n }\n }\n\n const matchesScalar = (expected: unknown) => {\n const expectedStr = String(expected).trim();\n return candidates.has(expectedStr);\n };\n\n // Support multiple expected formats:\n // - scalar: \"Consultation\" / \"1\" / 1 / true\n // - array: [\"Consultation\", \"Follow-up\"]\n // - map/object: { \"Consultation\": true }\n if (Array.isArray(expectedAnswer)) {\n return expectedAnswer.some((v) => matchesScalar(v));\n }\n\n if (expectedAnswer && typeof expectedAnswer === 'object') {\n const expectedMap = expectedAnswer as Record<string, unknown>;\n for (const c of candidates) {\n if (Object.prototype.hasOwnProperty.call(expectedMap, c)) {\n const flag = expectedMap[c];\n return flag === undefined ? true : !!flag;\n }\n }\n return false;\n }\n\n return matchesScalar(expectedAnswer);\n });\n },\n [values, questions]\n );\n\n // Get visible questions\n const visibleQuestions = questions.filter(isQuestionVisible);\n\n\n // Validate a single field\n const validateField = useCallback((question: Question, value: FormValues[number]): string | null => {\n if (question.detail_type === 'heading') return null;\n\n if (question.required) {\n if (value === undefined || value === '' || value === null) {\n return `${question.name} is required`;\n }\n if (question.detail_type === 'check' && !value) {\n return `${question.name} must be checked`;\n }\n }\n\n if (question.detail_type === 'number' && value !== '' && value !== undefined) {\n const numValue = Number(value);\n if (question.settings?.min !== undefined && numValue < question.settings.min) {\n return `Minimum value is ${question.settings.min}`;\n }\n if (question.settings?.max !== undefined && numValue > question.settings.max) {\n return `Maximum value is ${question.settings.max}`;\n }\n }\n\n return null;\n }, []);\n\n // Validate all visible fields\n const validateAll = useCallback((): boolean => {\n const newErrors: FormErrors = {};\n let isValid = true;\n\n visibleQuestions.forEach((question) => {\n const error = validateField(question, values[question.id]);\n if (error) {\n newErrors[question.id] = error;\n isValid = false;\n }\n });\n\n setErrors(newErrors);\n return isValid;\n }, [visibleQuestions, values, validateField]);\n\n // Clear errors for hidden questions\n useEffect(() => {\n const visibleIds = new Set(visibleQuestions.map((q) => q.id));\n\n setErrors((prev) => {\n let changed = false;\n const next = { ...prev };\n\n Object.keys(next).forEach((idStr) => {\n const id = Number(idStr);\n if (!visibleIds.has(id)) {\n delete next[id];\n changed = true;\n }\n });\n\n return changed ? next : prev;\n });\n }, [visibleQuestions]);\n\n const handleChange = (questionId: number, value: string | number | boolean) => {\n setValues((prev) => ({ ...prev, [questionId]: value }));\n setTouched((prev) => ({ ...prev, [questionId]: true }));\n\n // Clear error on change if touched\n if (touched[questionId]) {\n const question = questions.find((q) => q.id === questionId);\n if (question) {\n const error = validateField(question, value);\n setErrors((prev) => {\n if (error) {\n return { ...prev, [questionId]: error };\n }\n const { [questionId]: _, ...rest } = prev;\n return rest;\n });\n }\n }\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n\n // Mark all fields as touched\n const allTouched: Record<number, boolean> = {};\n visibleQuestions.forEach((q) => {\n allTouched[q.id] = true;\n });\n setTouched(allTouched);\n\n if (validateAll()) {\n // Only include visible question values\n const visibleValues: FormValues = {};\n visibleQuestions.forEach((q) => {\n if (q.detail_type !== 'heading' && values[q.id] !== undefined) {\n visibleValues[q.id] = values[q.id];\n }\n });\n onSubmit(visibleValues);\n }\n };\n\n const defaultButton = 'w-full mt-4 px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors';\n\n return (\n <form onSubmit={handleSubmit} className={className} noValidate>\n {visibleQuestions.map((question) => (\n <FormField\n key={question.id}\n question={question}\n value={values[question.id]}\n error={touched[question.id] ? errors[question.id] : undefined}\n onChange={(value) => handleChange(question.id, value)}\n classNames={classNames}\n />\n ))}\n <button\n type=\"submit\"\n className={classNames?.button ?? defaultButton}\n >\n {submitLabel}\n </button>\n </form>\n );\n}\n\nexport default BookingForm;\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bookinglab/booking-ui-react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "React UI components for BookingLab booking journeys",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"react",
|
|
24
|
+
"ui",
|
|
25
|
+
"components",
|
|
26
|
+
"booking",
|
|
27
|
+
"bookinglab",
|
|
28
|
+
"scheduling",
|
|
29
|
+
"appointments"
|
|
30
|
+
],
|
|
31
|
+
"author": "BookingLab",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/bookinglab/booking-ui-react"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/bookinglab/booking-ui-react/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/bookinglab/booking-ui-react#readme",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"dev": "tsup --watch",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"prepublishOnly": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": ">=17.0.0",
|
|
49
|
+
"react-dom": ">=17.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/react": "^18.3.0",
|
|
53
|
+
"@types/react-dom": "^18.3.0",
|
|
54
|
+
"tsup": "^8.0.0",
|
|
55
|
+
"typescript": "^5.8.0"
|
|
56
|
+
}
|
|
57
|
+
}
|