@bookinglab/booking-ui-react 1.2.0 → 1.4.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.mjs CHANGED
@@ -425,6 +425,12 @@ var DEFAULT_FIELDS = [
425
425
  required: true,
426
426
  validate: compose(required, email)
427
427
  },
428
+ {
429
+ name: "password",
430
+ label: "Password",
431
+ type: "password",
432
+ required: false
433
+ },
428
434
  {
429
435
  name: "phone",
430
436
  label: "Contact number",
@@ -660,7 +666,411 @@ var RegistrationForm = forwardRef(
660
666
  }
661
667
  );
662
668
  RegistrationForm.displayName = "RegistrationForm";
669
+ var cx2 = (...classes) => classes.filter(Boolean).join(" ");
670
+ var ContactDetailsForm = forwardRef(
671
+ ({
672
+ questions = [],
673
+ onSubmit,
674
+ onChange: _onChange,
675
+ validateOnBlur = true,
676
+ submitLabel = "Submit",
677
+ className = "",
678
+ classNames = {},
679
+ fieldSettings = {}
680
+ }, ref) => {
681
+ const formId = useId();
682
+ const [firstName, setFirstName] = useState("");
683
+ const [lastName, setLastName] = useState("");
684
+ const [emailValue, setEmailValue] = useState("");
685
+ const [contactErrors, setContactErrors] = useState({});
686
+ const [contactTouched, setContactTouched] = useState({});
687
+ const [questionValues, setQuestionValues] = useState({});
688
+ const [questionErrors, setQuestionErrors] = useState({});
689
+ const [questionTouched, setQuestionTouched] = useState({});
690
+ const getFieldRequired = (name) => {
691
+ const s = fieldSettings[name];
692
+ return s?.required !== void 0 ? s.required : true;
693
+ };
694
+ const getFieldDisabled = (name) => {
695
+ const s = fieldSettings[name];
696
+ return !!s?.disabled;
697
+ };
698
+ const contactFields = [
699
+ { name: "firstName", label: "First name", value: firstName, setter: setFirstName },
700
+ { name: "lastName", label: "Last name", value: lastName, setter: setLastName },
701
+ { name: "email", label: "Email", value: emailValue, setter: setEmailValue }
702
+ ];
703
+ const validateContactField = useCallback((name, value) => {
704
+ const isRequired = getFieldRequired(name);
705
+ if (name === "email") {
706
+ const validators = isRequired ? compose(required, email) : email;
707
+ return value ? validators(value) : isRequired ? required(value) : null;
708
+ }
709
+ return isRequired ? required(value) : null;
710
+ }, [fieldSettings]);
711
+ const validateQuestionField = useCallback((question, value) => {
712
+ if (question.detail_type === "heading") return null;
713
+ if (question.required) {
714
+ if (value === void 0 || value === "" || value === null) {
715
+ return `${question.name} is required`;
716
+ }
717
+ if (question.detail_type === "check" && !value) {
718
+ return `${question.name} must be checked`;
719
+ }
720
+ }
721
+ if (question.detail_type === "number" && value !== "" && value !== void 0) {
722
+ const numValue = Number(value);
723
+ if (question.settings?.min !== void 0 && numValue < question.settings.min) {
724
+ return `Minimum value is ${question.settings.min}`;
725
+ }
726
+ if (question.settings?.max !== void 0 && numValue > question.settings.max) {
727
+ return `Maximum value is ${question.settings.max}`;
728
+ }
729
+ }
730
+ return null;
731
+ }, []);
732
+ const validateAllContacts = useCallback(() => {
733
+ const errs = {};
734
+ let valid = true;
735
+ for (const f of contactFields) {
736
+ if (getFieldDisabled(f.name)) continue;
737
+ const err = validateContactField(f.name, f.value);
738
+ if (err) {
739
+ errs[f.name] = err;
740
+ valid = false;
741
+ }
742
+ }
743
+ setContactErrors(errs);
744
+ return valid;
745
+ }, [firstName, lastName, emailValue, validateContactField, fieldSettings]);
746
+ const validateAllQuestions = useCallback(() => {
747
+ const errs = {};
748
+ let valid = true;
749
+ for (const q of questions) {
750
+ const err = validateQuestionField(q, questionValues[q.id]);
751
+ if (err) {
752
+ errs[q.id] = err;
753
+ valid = false;
754
+ }
755
+ }
756
+ setQuestionErrors(errs);
757
+ return valid;
758
+ }, [questions, questionValues, validateQuestionField]);
759
+ const buildOutput = useCallback(() => {
760
+ const q = {};
761
+ const answers = [];
762
+ for (const question of questions) {
763
+ if (question.detail_type === "heading") continue;
764
+ const rawValue = questionValues[question.id];
765
+ if (rawValue === void 0 || rawValue === "") continue;
766
+ let answer = String(rawValue);
767
+ let answerId = 0;
768
+ if (question.detail_type === "select" && question.options?.length) {
769
+ const option = question.options.find(
770
+ (o) => String(o.id) === String(rawValue)
771
+ );
772
+ if (option) {
773
+ answer = option.name;
774
+ answerId = typeof option.id === "number" ? option.id : parseInt(String(option.id), 10) || 0;
775
+ }
776
+ }
777
+ const idStr = String(question.id);
778
+ q[idStr] = { answer, answer_id: answerId, name: question.name };
779
+ answers.push({ question_id: question.id, name: question.name, answer, answer_id: answerId });
780
+ }
781
+ return { firstName, lastName, email: emailValue, q, answers };
782
+ }, [firstName, lastName, emailValue, questions, questionValues]);
783
+ const handleContactChange = useCallback((name, value) => {
784
+ const setter = name === "firstName" ? setFirstName : name === "lastName" ? setLastName : setEmailValue;
785
+ setter(value);
786
+ if (contactTouched[name]) {
787
+ const err = validateContactField(name, value);
788
+ setContactErrors((prev) => {
789
+ if (err) return { ...prev, [name]: err };
790
+ const { [name]: _, ...rest } = prev;
791
+ return rest;
792
+ });
793
+ }
794
+ }, [contactTouched, validateContactField]);
795
+ const handleContactBlur = useCallback((name, value) => {
796
+ setContactTouched((prev) => ({ ...prev, [name]: true }));
797
+ if (validateOnBlur) {
798
+ const err = validateContactField(name, value);
799
+ setContactErrors((prev) => {
800
+ if (err) return { ...prev, [name]: err };
801
+ const { [name]: _, ...rest } = prev;
802
+ return rest;
803
+ });
804
+ }
805
+ }, [validateOnBlur, validateContactField]);
806
+ const handleQuestionChange = useCallback((questionId, value) => {
807
+ setQuestionValues((prev) => ({ ...prev, [questionId]: value }));
808
+ setQuestionTouched((prev) => ({ ...prev, [questionId]: true }));
809
+ if (questionTouched[questionId]) {
810
+ const question = questions.find((q) => q.id === questionId);
811
+ if (question) {
812
+ const err = validateQuestionField(question, value);
813
+ setQuestionErrors((prev) => {
814
+ if (err) return { ...prev, [questionId]: err };
815
+ const { [questionId]: _, ...rest } = prev;
816
+ return rest;
817
+ });
818
+ }
819
+ }
820
+ }, [questionTouched, questions, validateQuestionField]);
821
+ const handleQuestionBlur = useCallback((questionId) => {
822
+ setQuestionTouched((prev) => ({ ...prev, [questionId]: true }));
823
+ if (validateOnBlur) {
824
+ const question = questions.find((q) => q.id === questionId);
825
+ if (question) {
826
+ const err = validateQuestionField(question, questionValues[questionId]);
827
+ setQuestionErrors((prev) => {
828
+ if (err) return { ...prev, [questionId]: err };
829
+ const { [questionId]: _, ...rest } = prev;
830
+ return rest;
831
+ });
832
+ }
833
+ }
834
+ }, [validateOnBlur, questions, questionValues, validateQuestionField]);
835
+ const handleSubmit = useCallback((e) => {
836
+ e.preventDefault();
837
+ const allContactTouched = {};
838
+ for (const f of contactFields) allContactTouched[f.name] = true;
839
+ setContactTouched(allContactTouched);
840
+ const allQTouched = {};
841
+ for (const q of questions) allQTouched[q.id] = true;
842
+ setQuestionTouched(allQTouched);
843
+ const contactValid = validateAllContacts();
844
+ const questionsValid = validateAllQuestions();
845
+ if (contactValid && questionsValid) {
846
+ onSubmit(buildOutput());
847
+ }
848
+ }, [contactFields, questions, validateAllContacts, validateAllQuestions, onSubmit, buildOutput]);
849
+ useImperativeHandle(ref, () => ({
850
+ reset: () => {
851
+ setFirstName("");
852
+ setLastName("");
853
+ setEmailValue("");
854
+ setContactErrors({});
855
+ setContactTouched({});
856
+ setQuestionValues({});
857
+ setQuestionErrors({});
858
+ setQuestionTouched({});
859
+ },
860
+ setValues: (vals) => {
861
+ if (vals.firstName !== void 0) setFirstName(vals.firstName);
862
+ if (vals.lastName !== void 0) setLastName(vals.lastName);
863
+ if (vals.email !== void 0) setEmailValue(vals.email);
864
+ }
865
+ }), []);
866
+ const styles = {
867
+ fieldWrapper: classNames.fieldWrapper || "mb-4",
868
+ label: classNames.label || "block text-sm font-medium mb-1",
869
+ heading: classNames.heading || "text-lg font-semibold mt-4 mb-2 first:mt-0",
870
+ input: classNames.input || "w-full px-3 py-2 border rounded-md text-sm",
871
+ inputError: classNames.inputError || "border-red-500",
872
+ checkbox: classNames.checkbox || "mt-1 h-4 w-4 rounded border-gray-300",
873
+ helpText: classNames.helpText || "mt-1 text-xs text-gray-500",
874
+ errorText: classNames.errorText || "mt-1 text-xs text-red-600",
875
+ 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"
876
+ };
877
+ const inputClasses = (hasError) => hasError ? cx2(styles.input, styles.inputError) : styles.input;
878
+ const renderContactField = (name, label, value, type = "text") => {
879
+ const fieldId = `${formId}-${name}`;
880
+ const errorId = `${fieldId}-error`;
881
+ const isDisabled = getFieldDisabled(name);
882
+ const isRequired = getFieldRequired(name);
883
+ const error = contactTouched[name] ? contactErrors[name] : void 0;
884
+ const hasError = !!error;
885
+ return /* @__PURE__ */ jsxs("div", { className: styles.fieldWrapper, children: [
886
+ /* @__PURE__ */ jsxs("label", { htmlFor: fieldId, className: styles.label, children: [
887
+ label,
888
+ isRequired && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
889
+ ] }),
890
+ /* @__PURE__ */ jsx(
891
+ "input",
892
+ {
893
+ id: fieldId,
894
+ name,
895
+ type,
896
+ value,
897
+ disabled: isDisabled,
898
+ onChange: (e) => handleContactChange(name, e.target.value),
899
+ onBlur: () => handleContactBlur(name, name === "firstName" ? firstName : name === "lastName" ? lastName : emailValue),
900
+ className: cx2(inputClasses(hasError), isDisabled ? "opacity-50 cursor-not-allowed" : ""),
901
+ "aria-invalid": hasError ? "true" : "false",
902
+ "aria-describedby": hasError ? errorId : void 0,
903
+ "aria-required": isRequired ? "true" : void 0
904
+ }
905
+ ),
906
+ hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: styles.errorText, role: "alert", children: error })
907
+ ] }, name);
908
+ };
909
+ const renderQuestionField = (question) => {
910
+ const fieldId = `${formId}-q-${question.id}`;
911
+ const errorId = `${fieldId}-error`;
912
+ const value = questionValues[question.id];
913
+ const error = questionTouched[question.id] ? questionErrors[question.id] : void 0;
914
+ const hasError = !!error;
915
+ const ariaProps = {
916
+ "aria-invalid": hasError ? true : void 0,
917
+ "aria-describedby": hasError ? errorId : void 0,
918
+ "aria-required": question.required || void 0
919
+ };
920
+ switch (question.detail_type) {
921
+ case "heading":
922
+ return /* @__PURE__ */ jsx("h3", { className: styles.heading, children: question.name }, question.id);
923
+ case "text_field":
924
+ return /* @__PURE__ */ jsxs("div", { className: styles.fieldWrapper, children: [
925
+ /* @__PURE__ */ jsxs("label", { htmlFor: fieldId, className: styles.label, children: [
926
+ question.name,
927
+ question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
928
+ ] }),
929
+ /* @__PURE__ */ jsx(
930
+ "input",
931
+ {
932
+ id: fieldId,
933
+ type: "text",
934
+ value: value || "",
935
+ onChange: (e) => handleQuestionChange(question.id, e.target.value),
936
+ onBlur: () => handleQuestionBlur(question.id),
937
+ placeholder: question.settings?.placeholder,
938
+ className: inputClasses(hasError),
939
+ ...ariaProps
940
+ }
941
+ ),
942
+ question.help_text && /* @__PURE__ */ jsx("p", { className: styles.helpText, children: question.help_text }),
943
+ hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: styles.errorText, role: "alert", children: error })
944
+ ] }, question.id);
945
+ case "text_area":
946
+ return /* @__PURE__ */ jsxs("div", { className: styles.fieldWrapper, children: [
947
+ /* @__PURE__ */ jsxs("label", { htmlFor: fieldId, className: styles.label, children: [
948
+ question.name,
949
+ question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
950
+ ] }),
951
+ /* @__PURE__ */ jsx(
952
+ "textarea",
953
+ {
954
+ id: fieldId,
955
+ value: value || "",
956
+ onChange: (e) => handleQuestionChange(question.id, e.target.value),
957
+ onBlur: () => handleQuestionBlur(question.id),
958
+ placeholder: question.settings?.placeholder,
959
+ rows: 4,
960
+ className: inputClasses(hasError),
961
+ ...ariaProps
962
+ }
963
+ ),
964
+ question.help_text && /* @__PURE__ */ jsx("p", { className: styles.helpText, children: question.help_text }),
965
+ hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: styles.errorText, role: "alert", children: error })
966
+ ] }, question.id);
967
+ case "select":
968
+ return /* @__PURE__ */ jsxs("div", { className: styles.fieldWrapper, children: [
969
+ /* @__PURE__ */ jsxs("label", { htmlFor: fieldId, className: styles.label, children: [
970
+ question.name,
971
+ question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
972
+ ] }),
973
+ /* @__PURE__ */ jsxs(
974
+ "select",
975
+ {
976
+ id: fieldId,
977
+ value: value ?? "",
978
+ onChange: (e) => handleQuestionChange(question.id, e.target.value),
979
+ onBlur: () => handleQuestionBlur(question.id),
980
+ className: inputClasses(hasError),
981
+ ...ariaProps,
982
+ children: [
983
+ /* @__PURE__ */ jsx("option", { value: "", children: "Select an option" }),
984
+ question.options?.map((option) => /* @__PURE__ */ jsx("option", { value: option.id, children: option.name }, option.id))
985
+ ]
986
+ }
987
+ ),
988
+ question.help_text && /* @__PURE__ */ jsx("p", { className: styles.helpText, children: question.help_text }),
989
+ hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: styles.errorText, role: "alert", children: error })
990
+ ] }, question.id);
991
+ case "number":
992
+ return /* @__PURE__ */ jsxs("div", { className: styles.fieldWrapper, children: [
993
+ /* @__PURE__ */ jsxs("label", { htmlFor: fieldId, className: styles.label, children: [
994
+ question.name,
995
+ question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
996
+ ] }),
997
+ /* @__PURE__ */ jsx(
998
+ "input",
999
+ {
1000
+ id: fieldId,
1001
+ type: "number",
1002
+ value: value ?? "",
1003
+ onChange: (e) => handleQuestionChange(question.id, e.target.value ? Number(e.target.value) : ""),
1004
+ onBlur: () => handleQuestionBlur(question.id),
1005
+ min: question.settings?.min,
1006
+ max: question.settings?.max,
1007
+ placeholder: question.settings?.placeholder,
1008
+ className: inputClasses(hasError),
1009
+ ...ariaProps
1010
+ }
1011
+ ),
1012
+ question.help_text && /* @__PURE__ */ jsx("p", { className: styles.helpText, children: question.help_text }),
1013
+ hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: styles.errorText, role: "alert", children: error })
1014
+ ] }, question.id);
1015
+ case "date":
1016
+ return /* @__PURE__ */ jsxs("div", { className: styles.fieldWrapper, children: [
1017
+ /* @__PURE__ */ jsxs("label", { htmlFor: fieldId, className: styles.label, children: [
1018
+ question.name,
1019
+ question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
1020
+ ] }),
1021
+ /* @__PURE__ */ jsx(
1022
+ "input",
1023
+ {
1024
+ id: fieldId,
1025
+ type: "date",
1026
+ value: value || "",
1027
+ onChange: (e) => handleQuestionChange(question.id, e.target.value),
1028
+ onBlur: () => handleQuestionBlur(question.id),
1029
+ className: inputClasses(hasError),
1030
+ ...ariaProps
1031
+ }
1032
+ ),
1033
+ question.help_text && /* @__PURE__ */ jsx("p", { className: styles.helpText, children: question.help_text }),
1034
+ hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: styles.errorText, role: "alert", children: error })
1035
+ ] }, question.id);
1036
+ case "check":
1037
+ return /* @__PURE__ */ jsxs("div", { className: styles.fieldWrapper, children: [
1038
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
1039
+ /* @__PURE__ */ jsx(
1040
+ "input",
1041
+ {
1042
+ id: fieldId,
1043
+ type: "checkbox",
1044
+ checked: !!value,
1045
+ onChange: (e) => handleQuestionChange(question.id, e.target.checked),
1046
+ onBlur: () => handleQuestionBlur(question.id),
1047
+ className: styles.checkbox,
1048
+ ...ariaProps
1049
+ }
1050
+ ),
1051
+ /* @__PURE__ */ jsxs("label", { htmlFor: fieldId, className: "text-sm", children: [
1052
+ question.name,
1053
+ question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
1054
+ ] })
1055
+ ] }),
1056
+ question.help_text && /* @__PURE__ */ jsx("p", { className: styles.helpText, children: question.help_text }),
1057
+ hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: styles.errorText, role: "alert", children: error })
1058
+ ] }, question.id);
1059
+ default:
1060
+ return null;
1061
+ }
1062
+ };
1063
+ return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className, noValidate: true, children: [
1064
+ renderContactField("firstName", "First name", firstName),
1065
+ renderContactField("lastName", "Last name", lastName),
1066
+ renderContactField("email", "Email", emailValue, "email"),
1067
+ questions.map(renderQuestionField),
1068
+ /* @__PURE__ */ jsx("button", { type: "submit", className: styles.button, children: submitLabel })
1069
+ ] });
1070
+ }
1071
+ );
1072
+ ContactDetailsForm.displayName = "ContactDetailsForm";
663
1073
 
664
- export { BookingForm, RegistrationForm, compose, email, minLen, phone, required, ukPostcode };
1074
+ export { BookingForm, ContactDetailsForm, RegistrationForm, compose, email, minLen, phone, required, ukPostcode };
665
1075
  //# sourceMappingURL=index.mjs.map
666
1076
  //# sourceMappingURL=index.mjs.map