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