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