@bookinglab/booking-ui-react 1.0.2 → 1.1.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/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
- import { useState, useCallback, useEffect } from 'react';
2
- import { jsxs, jsx } from 'react/jsx-runtime';
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
3
5
 
4
6
  // src/components/BookingForm.tsx
5
7
  var cx = (...classes) => classes.filter(Boolean).join(" ");
@@ -26,18 +28,18 @@ function FormField({
26
28
  const checkboxLabelClasses = cx("text-sm", classNames?.label ?? "text-gray-700");
27
29
  const renderLabel = () => {
28
30
  if (question.detail_type === "heading") return null;
29
- return /* @__PURE__ */ jsxs("label", { htmlFor: inputId, className: labelClasses, children: [
31
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: inputId, className: labelClasses, children: [
30
32
  question.name,
31
- question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
33
+ question.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
32
34
  ] });
33
35
  };
34
36
  const renderHelpText = () => {
35
37
  if (!question.help_text) return null;
36
- return /* @__PURE__ */ jsx("p", { className: classNames?.helpText ?? defaultHelpText, children: question.help_text });
38
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: classNames?.helpText ?? defaultHelpText, children: question.help_text });
37
39
  };
38
40
  const renderError = () => {
39
41
  if (!error) return null;
40
- return /* @__PURE__ */ jsx("p", { id: errorId, className: classNames?.errorText ?? defaultErrorText, role: "alert", children: error });
42
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { id: errorId, className: classNames?.errorText ?? defaultErrorText, role: "alert", children: error });
41
43
  };
42
44
  const ariaProps = {
43
45
  "aria-invalid": hasError ? true : void 0,
@@ -46,11 +48,11 @@ function FormField({
46
48
  };
47
49
  switch (question.detail_type) {
48
50
  case "heading":
49
- return /* @__PURE__ */ jsx("h3", { className: classNames?.heading ?? defaultHeading, children: question.name });
51
+ return /* @__PURE__ */ jsxRuntime.jsx("h3", { className: classNames?.heading ?? defaultHeading, children: question.name });
50
52
  case "text_field":
51
- return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
53
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
52
54
  renderLabel(),
53
- /* @__PURE__ */ jsx(
55
+ /* @__PURE__ */ jsxRuntime.jsx(
54
56
  "input",
55
57
  {
56
58
  id: inputId,
@@ -66,9 +68,9 @@ function FormField({
66
68
  renderError()
67
69
  ] });
68
70
  case "text_area":
69
- return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
71
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
70
72
  renderLabel(),
71
- /* @__PURE__ */ jsx(
73
+ /* @__PURE__ */ jsxRuntime.jsx(
72
74
  "textarea",
73
75
  {
74
76
  id: inputId,
@@ -84,9 +86,9 @@ function FormField({
84
86
  renderError()
85
87
  ] });
86
88
  case "select":
87
- return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
89
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
88
90
  renderLabel(),
89
- /* @__PURE__ */ jsxs(
91
+ /* @__PURE__ */ jsxRuntime.jsxs(
90
92
  "select",
91
93
  {
92
94
  id: inputId,
@@ -95,8 +97,8 @@ function FormField({
95
97
  className: inputClasses,
96
98
  ...ariaProps,
97
99
  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
+ /* @__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))
100
102
  ]
101
103
  }
102
104
  ),
@@ -104,9 +106,9 @@ function FormField({
104
106
  renderError()
105
107
  ] });
106
108
  case "date":
107
- return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
109
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
108
110
  renderLabel(),
109
- /* @__PURE__ */ jsx(
111
+ /* @__PURE__ */ jsxRuntime.jsx(
110
112
  "input",
111
113
  {
112
114
  id: inputId,
@@ -123,9 +125,9 @@ function FormField({
123
125
  renderError()
124
126
  ] });
125
127
  case "number":
126
- return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
128
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
127
129
  renderLabel(),
128
- /* @__PURE__ */ jsx(
130
+ /* @__PURE__ */ jsxRuntime.jsx(
129
131
  "input",
130
132
  {
131
133
  id: inputId,
@@ -143,9 +145,9 @@ function FormField({
143
145
  renderError()
144
146
  ] });
145
147
  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(
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(
149
151
  "input",
150
152
  {
151
153
  id: inputId,
@@ -156,9 +158,9 @@ function FormField({
156
158
  ...ariaProps
157
159
  }
158
160
  ),
159
- /* @__PURE__ */ jsxs("label", { htmlFor: inputId, className: checkboxLabelClasses, children: [
161
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: inputId, className: checkboxLabelClasses, children: [
160
162
  question.name,
161
- question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
163
+ question.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
162
164
  ] })
163
165
  ] }),
164
166
  renderHelpText(),
@@ -180,10 +182,10 @@ function BookingForm({
180
182
  ...classNamesProp,
181
183
  label: classNamesProp?.label ?? labelClassName
182
184
  };
183
- const [values, setValues] = useState({});
184
- const [errors, setErrors] = useState({});
185
- const [touched, setTouched] = useState({});
186
- const isQuestionVisible = useCallback(
185
+ const [values, setValues] = react.useState({});
186
+ const [errors, setErrors] = react.useState({});
187
+ const [touched, setTouched] = react.useState({});
188
+ const isQuestionVisible = react.useCallback(
187
189
  (question) => {
188
190
  const { settings } = question;
189
191
  if (!settings?.conditional_answers) {
@@ -246,7 +248,7 @@ function BookingForm({
246
248
  [values, questions]
247
249
  );
248
250
  const visibleQuestions = questions.filter(isQuestionVisible);
249
- const validateField = useCallback((question, value) => {
251
+ const validateField = react.useCallback((question, value) => {
250
252
  if (question.detail_type === "heading") return null;
251
253
  if (question.required) {
252
254
  if (value === void 0 || value === "" || value === null) {
@@ -267,7 +269,7 @@ function BookingForm({
267
269
  }
268
270
  return null;
269
271
  }, []);
270
- const validateAll = useCallback(() => {
272
+ const validateAll = react.useCallback(() => {
271
273
  const newErrors = {};
272
274
  let isValid = true;
273
275
  visibleQuestions.forEach((question) => {
@@ -280,7 +282,7 @@ function BookingForm({
280
282
  setErrors(newErrors);
281
283
  return isValid;
282
284
  }, [visibleQuestions, values, validateField]);
283
- useEffect(() => {
285
+ react.useEffect(() => {
284
286
  const visibleIds = new Set(visibleQuestions.map((q) => q.id));
285
287
  setErrors((prev) => {
286
288
  let changed = false;
@@ -330,8 +332,8 @@ function BookingForm({
330
332
  }
331
333
  };
332
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";
333
- return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className, noValidate: true, children: [
334
- visibleQuestions.map((question) => /* @__PURE__ */ jsx(
335
+ return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className, noValidate: true, children: [
336
+ visibleQuestions.map((question) => /* @__PURE__ */ jsxRuntime.jsx(
335
337
  FormField,
336
338
  {
337
339
  question,
@@ -342,7 +344,7 @@ function BookingForm({
342
344
  },
343
345
  question.id
344
346
  )),
345
- /* @__PURE__ */ jsx(
347
+ /* @__PURE__ */ jsxRuntime.jsx(
346
348
  "button",
347
349
  {
348
350
  type: "submit",
@@ -353,6 +355,299 @@ function BookingForm({
353
355
  ] });
354
356
  }
355
357
 
356
- export { BookingForm };
358
+ // src/utils/validators.ts
359
+ function required(value) {
360
+ if (!value || value.trim() === "") {
361
+ return "This field is required";
362
+ }
363
+ return null;
364
+ }
365
+ function email(value) {
366
+ if (!value) return null;
367
+ const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
368
+ if (!emailRegex.test(value)) {
369
+ return "Please enter a valid email address";
370
+ }
371
+ return null;
372
+ }
373
+ function phone(value) {
374
+ if (!value) return null;
375
+ const cleaned = value.replace(/[\s\-\(\)]/g, "");
376
+ const phoneRegex = /^\+?[0-9]{7,15}$/;
377
+ if (!phoneRegex.test(cleaned)) {
378
+ return "Please enter a valid phone number";
379
+ }
380
+ return null;
381
+ }
382
+ function ukPostcode(value) {
383
+ if (!value) return null;
384
+ const postcodeRegex = /^([A-Z]{1,2}[0-9][A-Z0-9]? ?[0-9][A-Z]{2})$/i;
385
+ if (!postcodeRegex.test(value.trim())) {
386
+ return "Please enter a valid UK postcode";
387
+ }
388
+ return null;
389
+ }
390
+ function minLen(min) {
391
+ return (value) => {
392
+ if (!value) return null;
393
+ if (value.trim().length < min) {
394
+ return `Must be at least ${min} character${min === 1 ? "" : "s"}`;
395
+ }
396
+ return null;
397
+ };
398
+ }
399
+ function compose(...validators) {
400
+ return (value) => {
401
+ for (const validator of validators) {
402
+ const error = validator(value);
403
+ if (error) return error;
404
+ }
405
+ return null;
406
+ };
407
+ }
408
+ var DEFAULT_FIELDS = [
409
+ {
410
+ name: "firstName",
411
+ label: "First name",
412
+ type: "text",
413
+ required: true,
414
+ validate: required
415
+ },
416
+ {
417
+ name: "lastName",
418
+ label: "Last name",
419
+ type: "text",
420
+ required: true,
421
+ validate: required
422
+ },
423
+ {
424
+ name: "email",
425
+ label: "Email address",
426
+ type: "email",
427
+ required: true,
428
+ validate: compose(required, email)
429
+ },
430
+ {
431
+ name: "phone",
432
+ label: "Contact number",
433
+ type: "tel",
434
+ required: false,
435
+ validate: phone
436
+ },
437
+ {
438
+ name: "address1",
439
+ label: "Address 1",
440
+ type: "text",
441
+ required: true,
442
+ validate: required
443
+ },
444
+ {
445
+ name: "address2",
446
+ label: "Address 2",
447
+ type: "text",
448
+ required: false
449
+ },
450
+ {
451
+ name: "city",
452
+ label: "Town/City",
453
+ type: "text",
454
+ required: true,
455
+ validate: required
456
+ },
457
+ {
458
+ name: "postcode",
459
+ label: "Postcode",
460
+ type: "text",
461
+ required: true,
462
+ validate: compose(required, ukPostcode)
463
+ }
464
+ ];
465
+ var RegistrationForm = react.forwardRef(
466
+ ({
467
+ fields = DEFAULT_FIELDS,
468
+ onSubmit,
469
+ onChange,
470
+ validateOnBlur = true,
471
+ submitLabel = "Submit",
472
+ className = "",
473
+ classNames = {}
474
+ }, ref) => {
475
+ const formId = react.useId();
476
+ const [values, setValues] = react.useState({});
477
+ const [errors, setErrors] = react.useState({});
478
+ const [touched, setTouched] = react.useState({});
479
+ const validateField = react.useCallback(
480
+ (field, value) => {
481
+ if (field.required && (!value || value.trim() === "")) {
482
+ return "This field is required";
483
+ }
484
+ if (field.validate && value) {
485
+ return field.validate(value);
486
+ }
487
+ return null;
488
+ },
489
+ []
490
+ );
491
+ const validateAll = react.useCallback(() => {
492
+ const newErrors = {};
493
+ let isValid = true;
494
+ for (const field of fields) {
495
+ const value = values[field.name] || "";
496
+ const error = validateField(field, value);
497
+ if (error) {
498
+ newErrors[field.name] = error;
499
+ isValid = false;
500
+ }
501
+ }
502
+ setErrors(newErrors);
503
+ return isValid;
504
+ }, [fields, values, validateField]);
505
+ const checkIsValid = react.useCallback(
506
+ (currentValues) => {
507
+ for (const field of fields) {
508
+ const value = currentValues[field.name] || "";
509
+ const error = validateField(field, value);
510
+ if (error) return false;
511
+ }
512
+ return true;
513
+ },
514
+ [fields, validateField]
515
+ );
516
+ const handleChange = react.useCallback(
517
+ (fieldName, value) => {
518
+ const newValues = { ...values, [fieldName]: value };
519
+ setValues(newValues);
520
+ if (touched[fieldName]) {
521
+ const field = fields.find((f) => f.name === fieldName);
522
+ if (field) {
523
+ const error = validateField(field, value);
524
+ if (!error) {
525
+ setErrors((prev) => {
526
+ const next = { ...prev };
527
+ delete next[fieldName];
528
+ return next;
529
+ });
530
+ }
531
+ }
532
+ }
533
+ if (onChange) {
534
+ const isValid = checkIsValid(newValues);
535
+ onChange(newValues, isValid);
536
+ }
537
+ },
538
+ [values, touched, fields, validateField, onChange, checkIsValid]
539
+ );
540
+ const handleBlur = react.useCallback(
541
+ (fieldName) => {
542
+ setTouched((prev) => ({ ...prev, [fieldName]: true }));
543
+ if (validateOnBlur) {
544
+ const field = fields.find((f) => f.name === fieldName);
545
+ if (field) {
546
+ const value = values[fieldName] || "";
547
+ const error = validateField(field, value);
548
+ if (error) {
549
+ setErrors((prev) => ({ ...prev, [fieldName]: error }));
550
+ } else {
551
+ setErrors((prev) => {
552
+ const next = { ...prev };
553
+ delete next[fieldName];
554
+ return next;
555
+ });
556
+ }
557
+ }
558
+ }
559
+ },
560
+ [validateOnBlur, fields, values, validateField]
561
+ );
562
+ const handleSubmit = react.useCallback(
563
+ (e) => {
564
+ e.preventDefault();
565
+ const allTouched = {};
566
+ for (const field of fields) {
567
+ allTouched[field.name] = true;
568
+ }
569
+ setTouched(allTouched);
570
+ if (validateAll()) {
571
+ onSubmit(values);
572
+ }
573
+ },
574
+ [fields, validateAll, onSubmit, values]
575
+ );
576
+ react.useImperativeHandle(
577
+ ref,
578
+ () => ({
579
+ reset: () => {
580
+ setValues({});
581
+ setErrors({});
582
+ setTouched({});
583
+ },
584
+ setValues: (newValues) => {
585
+ setValues((prev) => {
586
+ const merged = { ...prev };
587
+ for (const [key, value] of Object.entries(newValues)) {
588
+ if (value !== void 0) {
589
+ merged[key] = value;
590
+ }
591
+ }
592
+ return merged;
593
+ });
594
+ }
595
+ }),
596
+ []
597
+ );
598
+ const styles = {
599
+ fieldWrapper: classNames.fieldWrapper || "mb-4",
600
+ label: classNames.label || "block text-sm font-medium mb-1",
601
+ input: classNames.input || "w-full px-3 py-2 border rounded-md",
602
+ inputError: classNames.inputError || "border-red-500",
603
+ errorText: classNames.errorText || "mt-1 text-xs text-red-600",
604
+ button: classNames.button || "w-full mt-4 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
605
+ };
606
+ return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className, noValidate: true, children: [
607
+ fields.map((field) => {
608
+ const fieldId = `${formId}-${field.name}`;
609
+ const errorId = `${fieldId}-error`;
610
+ const value = values[field.name] || "";
611
+ const error = errors[field.name];
612
+ const isTouched = touched[field.name];
613
+ const showError = isTouched && error;
614
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.fieldWrapper, children: [
615
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: fieldId, className: styles.label, children: [
616
+ field.label,
617
+ field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
618
+ ] }),
619
+ /* @__PURE__ */ jsxRuntime.jsx(
620
+ "input",
621
+ {
622
+ id: fieldId,
623
+ name: field.name,
624
+ type: field.type,
625
+ value,
626
+ onChange: (e) => handleChange(field.name, e.target.value),
627
+ onBlur: () => handleBlur(field.name),
628
+ placeholder: field.placeholder,
629
+ className: `${styles.input} ${showError ? styles.inputError : ""}`,
630
+ "aria-invalid": showError ? "true" : "false",
631
+ "aria-describedby": showError ? errorId : void 0,
632
+ "aria-required": field.required ? "true" : "false"
633
+ }
634
+ ),
635
+ showError && /* @__PURE__ */ jsxRuntime.jsx("p", { id: errorId, className: styles.errorText, role: "alert", children: error })
636
+ ] }, field.name);
637
+ }),
638
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: styles.button, children: submitLabel })
639
+ ] });
640
+ }
641
+ );
642
+ RegistrationForm.displayName = "RegistrationForm";
643
+
644
+ exports.BookingForm = BookingForm;
645
+ exports.RegistrationForm = RegistrationForm;
646
+ exports.compose = compose;
647
+ exports.email = email;
648
+ exports.minLen = minLen;
649
+ exports.phone = phone;
650
+ exports.required = required;
651
+ exports.ukPostcode = ukPostcode;
357
652
  //# sourceMappingURL=index.js.map
358
653
  //# sourceMappingURL=index.js.map