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