@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.js CHANGED
@@ -427,6 +427,12 @@ var DEFAULT_FIELDS = [
427
427
  required: true,
428
428
  validate: compose(required, email)
429
429
  },
430
+ {
431
+ name: "password",
432
+ label: "Password",
433
+ type: "password",
434
+ required: false
435
+ },
430
436
  {
431
437
  name: "phone",
432
438
  label: "Contact number",
@@ -662,8 +668,413 @@ var RegistrationForm = react.forwardRef(
662
668
  }
663
669
  );
664
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";
665
1075
 
666
1076
  exports.BookingForm = BookingForm;
1077
+ exports.ContactDetailsForm = ContactDetailsForm;
667
1078
  exports.RegistrationForm = RegistrationForm;
668
1079
  exports.compose = compose;
669
1080
  exports.email = email;