@escapenavigator/hooks 2.1.23 → 2.1.25

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.
@@ -1,5 +1,38 @@
1
1
  "use strict";
2
2
  /* eslint-disable import/no-extraneous-dependencies */
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
3
36
  var __importDefault = (this && this.__importDefault) || function (mod) {
4
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
38
  };
@@ -10,7 +43,7 @@ const PlusM_1 = __importDefault(require("@alphakits/icons/dist/PlusM"));
10
43
  const ui_1 = require("@alphakits/ui");
11
44
  const lead_field_type_enum_1 = require("@escapenavigator/types/dist/lead/enums/lead-field-type.enum");
12
45
  const lead_time_constants_1 = require("@escapenavigator/types/dist/lead/lead-time.constants");
13
- const react_1 = __importDefault(require("react"));
46
+ const react_1 = __importStar(require("react"));
14
47
  const input_tel_1 = require("../input-tel");
15
48
  const index_module_css_1 = __importDefault(require("./index.module.css"));
16
49
  const lib_1 = require("./lib");
@@ -43,6 +76,11 @@ const ChoiceGroup = ({ label, options, isSelected, onToggle, hint, error, }) =>
43
76
  const LeadField = ({ field, value, error, language, profileCountry, questrooms, locations, onChange, onPhoneValidChange, }) => {
44
77
  var _a, _b;
45
78
  const t = (0, use_lead_t_1.useLeadT)();
79
+ const [textareaExpanded, setTextareaExpanded] = (0, react_1.useState)(Boolean(value || error));
80
+ (0, react_1.useEffect)(() => {
81
+ if (value || error)
82
+ setTextareaExpanded(true);
83
+ }, [error, value]);
46
84
  const title = (0, lib_1.fieldTitle)(field, language);
47
85
  const label = field.required ? `${title} *` : title;
48
86
  const placeholder = (0, lib_1.fieldText)(field, language, 'placeholder');
@@ -68,7 +106,14 @@ const LeadField = ({ field, value, error, language, profileCountry, questrooms,
68
106
  onPhoneValidChange(payload.isValid);
69
107
  } }));
70
108
  case lead_field_type_enum_1.LeadFieldTypeEnum.TEXTAREA:
71
- return withHint(react_1.default.createElement(ui_1.Textarea, { block: true, maxLength: 1000, label: label, placeholder: placeholder, value: String(value || ''), error: error, onChange: (_e, payload) => onChange(payload.value) }));
109
+ if (!textareaExpanded) {
110
+ return (react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.commentToggle, onClick: () => setTextareaExpanded(true) },
111
+ react_1.default.createElement("span", { "aria-hidden": 'true' }, "+"),
112
+ title));
113
+ }
114
+ return withHint(react_1.default.createElement("div", { className: index_module_css_1.default.commentEditor },
115
+ react_1.default.createElement(ui_1.Textarea, { block: true, maxLength: 1000, label: label, placeholder: placeholder, value: String(value || ''), error: error, onChange: (_e, payload) => onChange(payload.value) }),
116
+ !value && !error && (react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.commentCollapse, onClick: () => setTextareaExpanded(false) }, t('lead.collapse')))));
72
117
  case lead_field_type_enum_1.LeadFieldTypeEnum.DATE:
73
118
  return withHint(react_1.default.createElement(ui_1.CalendarInput, { block: true, label: label, lang: language, value: String(value || ''), error: error, onChange: (_e, payload) => onChange(payload.value) }));
74
119
  case lead_field_type_enum_1.LeadFieldTypeEnum.TIME:
@@ -93,14 +138,14 @@ const LeadField = ({ field, value, error, language, profileCountry, questrooms,
93
138
  case lead_field_type_enum_1.LeadFieldTypeEnum.GUESTS: {
94
139
  const min = (_a = field.min) !== null && _a !== void 0 ? _a : 1;
95
140
  const max = (_b = field.max) !== null && _b !== void 0 ? _b : 30;
96
- const current = typeof value === 'number' ? value : min;
141
+ const current = typeof value === 'number' ? value : undefined;
97
142
  return (react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 8 },
98
143
  react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', weight: 'medium', color: 'primary' }, label),
99
144
  react_1.default.createElement("div", { className: index_module_css_1.default.stepper },
100
- react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.stepperButton, "aria-label": t('lead.guestsMinus'), disabled: current <= min, onClick: () => onChange(Math.max(min, current - 1)) },
145
+ react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.stepperButton, "aria-label": t('lead.guestsMinus'), disabled: current === undefined || current <= min, onClick: () => onChange(Math.max(min, (current !== null && current !== void 0 ? current : min + 1) - 1)) },
101
146
  react_1.default.createElement(MinusM_1.default, null)),
102
- react_1.default.createElement("span", { className: index_module_css_1.default.stepperValue }, current),
103
- react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.stepperButton, "aria-label": t('lead.guestsPlus'), disabled: current >= max, onClick: () => onChange(Math.min(max, current + 1)) },
147
+ react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.stepperValue, onClick: () => onChange(undefined) }, current !== null && current !== void 0 ? current : t('lead.guestsUnknown')),
148
+ react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.stepperButton, "aria-label": t('lead.guestsPlus'), disabled: current !== undefined && current >= max, onClick: () => onChange(Math.min(max, (current !== null && current !== void 0 ? current : min - 1) + 1)) },
104
149
  react_1.default.createElement(PlusM_1.default, null))),
105
150
  hint));
106
151
  }
@@ -46,6 +46,10 @@ type Props = {
46
46
  policyUrl?: string;
47
47
  /** Документ согласия на обработку ПДн (галочка есть только в РФ). */
48
48
  pdConsentUrl?: string;
49
+ /** Модалка управляет шагом, чтобы использовать свою кнопку «Назад». */
50
+ currentStep?: 1 | 2;
51
+ onStepChange?: (step: 1 | 2) => void;
52
+ showBackButton?: boolean;
49
53
  };
50
54
  export declare const LeadForm: React.FC<Props>;
51
55
  export {};
@@ -48,6 +48,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
48
48
  Object.defineProperty(exports, "__esModule", { value: true });
49
49
  exports.LeadForm = void 0;
50
50
  const ui_1 = require("@alphakits/ui");
51
+ const lead_field_type_enum_1 = require("@escapenavigator/types/dist/lead/enums/lead-field-type.enum");
51
52
  const react_1 = __importStar(require("react"));
52
53
  const personal_data_consent_1 = require("../personal-data-consent");
53
54
  const index_module_css_1 = __importDefault(require("./index.module.css"));
@@ -56,11 +57,37 @@ const lib_1 = require("./lib");
56
57
  const use_lead_t_1 = require("./use-lead-t");
57
58
  const ArrowRight = () => (react_1.default.createElement("svg", { width: '18', height: '18', viewBox: '0 0 24 24', fill: 'none', "aria-hidden": 'true' },
58
59
  react_1.default.createElement("path", { d: 'M5 12h14m-6-6 6 6-6 6', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' })));
59
- const LeadForm = ({ form, source, language, profileCountry, submit, prefill, contextNote, withHeader = true, onDone, doneLabel, onSuccess, policyUrl, pdConsentUrl, }) => {
60
+ const ArrowLeft = () => (react_1.default.createElement("svg", { width: '16', height: '16', viewBox: '0 0 24 24', fill: 'none', "aria-hidden": 'true' },
61
+ react_1.default.createElement("path", { d: 'M19 12H5m6 6-6-6 6-6', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' })));
62
+ const PLANNING_FIELD_TYPES = new Set([
63
+ lead_field_type_enum_1.LeadFieldTypeEnum.SELECT,
64
+ lead_field_type_enum_1.LeadFieldTypeEnum.RADIO,
65
+ lead_field_type_enum_1.LeadFieldTypeEnum.CHECKBOX,
66
+ lead_field_type_enum_1.LeadFieldTypeEnum.QUESTROOM,
67
+ lead_field_type_enum_1.LeadFieldTypeEnum.LOCATION,
68
+ ]);
69
+ const PLANNING_FIELD_IDS = new Set(['f_children_age']);
70
+ const CONTACT_FIELD_ORDER = {
71
+ [lead_field_type_enum_1.LeadFieldTypeEnum.PHONE]: 0,
72
+ [lead_field_type_enum_1.LeadFieldTypeEnum.NAME]: 1,
73
+ [lead_field_type_enum_1.LeadFieldTypeEnum.DATE]: 2,
74
+ [lead_field_type_enum_1.LeadFieldTypeEnum.TIME]: 3,
75
+ [lead_field_type_enum_1.LeadFieldTypeEnum.GUESTS]: 4,
76
+ [lead_field_type_enum_1.LeadFieldTypeEnum.TEXTAREA]: 5,
77
+ [lead_field_type_enum_1.LeadFieldTypeEnum.EMAIL]: 6,
78
+ [lead_field_type_enum_1.LeadFieldTypeEnum.CONSENT]: 7,
79
+ };
80
+ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, contextNote, withHeader = true, onDone, doneLabel, onSuccess, policyUrl, pdConsentUrl, currentStep, onStepChange, showBackButton = true, }) => {
60
81
  const t = (0, use_lead_t_1.useLeadT)();
61
82
  const consentRequired = (0, personal_data_consent_1.requiresPersonalDataConsent)(profileCountry);
62
83
  const [consent, setConsent] = (0, react_1.useState)(false);
63
84
  const [showConsentError, setShowConsentError] = (0, react_1.useState)(false);
85
+ const [internalStep, setInternalStep] = (0, react_1.useState)(1);
86
+ const step = currentStep !== null && currentStep !== void 0 ? currentStep : internalStep;
87
+ const setStep = (nextStep) => {
88
+ setInternalStep(nextStep);
89
+ onStepChange === null || onStepChange === void 0 ? void 0 : onStepChange(nextStep);
90
+ };
64
91
  const [values, setValues] = (0, react_1.useState)(() => (0, lib_1.buildInitialValues)(form, prefill));
65
92
  const [errors, setErrors] = (0, react_1.useState)({});
66
93
  const [formError, setFormError] = (0, react_1.useState)();
@@ -74,6 +101,67 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
74
101
  setErrors((prev) => (prev[fieldId] ? Object.assign(Object.assign({}, prev), { [fieldId]: '' }) : prev));
75
102
  setFormError(undefined);
76
103
  };
104
+ const visibleFields = form.fields.filter((field) => (0, lib_1.isVisible)(field, values));
105
+ const planningFields = visibleFields.filter((field) => PLANNING_FIELD_IDS.has(field.id) || PLANNING_FIELD_TYPES.has(field.type));
106
+ const contactFields = visibleFields
107
+ .filter((field) => !PLANNING_FIELD_IDS.has(field.id) && !PLANNING_FIELD_TYPES.has(field.type))
108
+ .sort((left, right) => {
109
+ var _a, _b;
110
+ return ((_a = CONTACT_FIELD_ORDER[left.type]) !== null && _a !== void 0 ? _a : 10) - ((_b = CONTACT_FIELD_ORDER[right.type]) !== null && _b !== void 0 ? _b : 10) ||
111
+ left.order - right.order;
112
+ });
113
+ const hasTwoSteps = planningFields.length > 0 && contactFields.length > 0;
114
+ const activeFields = !hasTwoSteps ? visibleFields : step === 1 ? planningFields : contactFields;
115
+ const planningFieldIds = new Set(planningFields.map((field) => field.id));
116
+ const planningSummary = planningFields
117
+ .map((field) => {
118
+ var _a, _b, _c;
119
+ const value = values[field.id];
120
+ if (value === undefined || value === '' || value === false)
121
+ return '';
122
+ if (Array.isArray(value)) {
123
+ return value
124
+ .map((id) => {
125
+ var _a;
126
+ const selected = (_a = field.options) === null || _a === void 0 ? void 0 : _a.find((item) => item.id === id);
127
+ return selected ? (0, lib_1.optionLabel)(selected, language, id) : id;
128
+ })
129
+ .join(', ');
130
+ }
131
+ if (typeof value === 'string' && ((_a = field.options) === null || _a === void 0 ? void 0 : _a.length)) {
132
+ const selected = field.options.find((item) => item.id === value);
133
+ return selected ? (0, lib_1.optionLabel)(selected, language, value) : value;
134
+ }
135
+ if (field.type === lead_field_type_enum_1.LeadFieldTypeEnum.QUESTROOM) {
136
+ return (((_b = form.questrooms.find((item) => String(item.id) === String(value))) === null || _b === void 0 ? void 0 : _b.title) || '');
137
+ }
138
+ if (field.type === lead_field_type_enum_1.LeadFieldTypeEnum.LOCATION) {
139
+ return (((_c = form.locations.find((item) => String(item.id) === String(value))) === null || _c === void 0 ? void 0 : _c.title) || '');
140
+ }
141
+ if (field.type === lead_field_type_enum_1.LeadFieldTypeEnum.GUESTS ||
142
+ field.type === lead_field_type_enum_1.LeadFieldTypeEnum.NUMBER) {
143
+ return `${(0, lib_1.fieldTitle)(field, language)}: ${String(value)}`;
144
+ }
145
+ return String(value);
146
+ })
147
+ .filter(Boolean);
148
+ const handleNext = () => {
149
+ const result = (0, lib_1.validate)({
150
+ form,
151
+ values,
152
+ phoneValid,
153
+ t,
154
+ fieldIds: planningFieldIds,
155
+ requireContact: false,
156
+ });
157
+ if (Object.keys(result.errors).length) {
158
+ setErrors(result.errors);
159
+ return;
160
+ }
161
+ setErrors({});
162
+ setFormError(undefined);
163
+ setStep(2);
164
+ };
77
165
  const handleSubmit = () => __awaiter(void 0, void 0, void 0, function* () {
78
166
  var _a;
79
167
  if (sending)
@@ -84,6 +172,9 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
84
172
  if (Object.keys(result.errors).length || result.formError || consentMissing) {
85
173
  setErrors(result.errors);
86
174
  setFormError(result.formError);
175
+ if (Object.keys(result.errors).some((fieldId) => planningFieldIds.has(fieldId))) {
176
+ setStep(1);
177
+ }
87
178
  return;
88
179
  }
89
180
  setSending(true);
@@ -118,18 +209,34 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
118
209
  }
119
210
  const title = (0, lib_1.localized)(form.clientTitle, language) || t('lead.title');
120
211
  const description = (0, lib_1.localized)(form.description, language);
212
+ const stepLabel = step === 1 ? t('lead.stepPlanning') : t('lead.stepContacts');
213
+ const headerTitle = hasTwoSteps && step === 2 ? t('lead.stepQuestMatchTitle') : title;
214
+ const headerDescription = hasTwoSteps && step === 2 ? t('lead.stepContactsHint') : description;
121
215
  return (react_1.default.createElement("div", { className: index_module_css_1.default.form },
122
216
  withHeader && (react_1.default.createElement("div", { className: index_module_css_1.default.header },
123
- react_1.default.createElement("span", { className: index_module_css_1.default.headerAccent, "aria-hidden": 'true' }),
124
- react_1.default.createElement(ui_1.Typography.Title, { tag: 'h1', view: 'small', weight: 'bold' }, title),
125
- !!description && (react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', color: 'secondary' }, description)))),
217
+ hasTwoSteps ? (react_1.default.createElement(react_1.default.Fragment, null,
218
+ react_1.default.createElement("div", { className: index_module_css_1.default.stepMeta },
219
+ step === 2 && showBackButton ? (react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.backButton, onClick: () => setStep(1) },
220
+ react_1.default.createElement(ArrowLeft, null),
221
+ t('lead.back'))) : (react_1.default.createElement("span", null)),
222
+ react_1.default.createElement("span", null, t('lead.stepCounter', {
223
+ current: step,
224
+ label: stepLabel,
225
+ }))),
226
+ react_1.default.createElement("div", { className: index_module_css_1.default.progress, "aria-hidden": 'true' },
227
+ react_1.default.createElement("span", { "data-step": step })))) : (react_1.default.createElement("span", { className: index_module_css_1.default.headerAccent, "aria-hidden": 'true' })),
228
+ react_1.default.createElement(ui_1.Typography.Title, { tag: 'h1', view: 'small', weight: 'bold' }, headerTitle),
229
+ !!headerDescription && (react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', color: 'secondary' }, headerDescription)))),
126
230
  !!formError && react_1.default.createElement(ui_1.ToastPlate, { view: 'negative' }, formError),
127
- react_1.default.createElement("div", { className: index_module_css_1.default.fields }, form.fields
128
- .filter((field) => (0, lib_1.isVisible)(field, values))
129
- .map((field) => (react_1.default.createElement("div", { key: field.id, className: index_module_css_1.default.fieldCard, "data-field-type": field.type },
231
+ hasTwoSteps && step === 2 && !!planningSummary.length && (react_1.default.createElement("div", { className: index_module_css_1.default.selectionSummary },
232
+ react_1.default.createElement("div", null,
233
+ react_1.default.createElement("span", null, t('lead.selection')),
234
+ react_1.default.createElement("strong", null, planningSummary.join(' · '))),
235
+ react_1.default.createElement("button", { type: 'button', onClick: () => setStep(1) }, t('lead.edit')))),
236
+ react_1.default.createElement("div", { key: hasTwoSteps ? step : 'single', className: index_module_css_1.default.fields, "data-step": hasTwoSteps ? step : 1 }, activeFields.map((field) => (react_1.default.createElement("div", { key: field.id, className: index_module_css_1.default.fieldCard, "data-field-type": field.type },
130
237
  react_1.default.createElement(lead_field_1.LeadField, { field: field, value: values[field.id], error: errors[field.id], language: language, profileCountry: profileCountry, questrooms: form.questrooms, locations: form.locations, onChange: (value) => setValue(field.id, value), onPhoneValidChange: setPhoneValid }))))),
131
238
  react_1.default.createElement("input", { className: index_module_css_1.default.trap, tabIndex: -1, autoComplete: 'off', "aria-hidden": 'true', value: trap, onChange: (event) => setTrap(event.target.value) }),
132
- react_1.default.createElement("div", { className: index_module_css_1.default.footer },
239
+ react_1.default.createElement("div", { className: index_module_css_1.default.footer }, hasTwoSteps && step === 1 ? (react_1.default.createElement(ui_1.Button, { view: 'primary', size: 's', block: true, rightAddons: react_1.default.createElement(ArrowRight, null), onClick: handleNext }, t('lead.next'))) : (react_1.default.createElement(react_1.default.Fragment, null,
133
240
  (consentRequired || !!policyUrl) && (react_1.default.createElement(personal_data_consent_1.PersonalDataConsent, { required: consentRequired, consentUrl: pdConsentUrl || '', policyUrl: policyUrl || '', checked: consent, showError: showConsentError, onChange: (checked) => {
134
241
  setConsent(checked);
135
242
  if (checked)
@@ -138,6 +245,6 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
138
245
  react_1.default.createElement(ui_1.Button, { view: 'primary', size: 's', block: true, loading: sending, rightAddons: sending ? undefined : react_1.default.createElement(ArrowRight, null), onClick: handleSubmit }, sending
139
246
  ? t('lead.sending')
140
247
  : (0, lib_1.localized)(form.submitLabel, language) || t('lead.submit')),
141
- react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: 'secondary', className: index_module_css_1.default.footnote, tag: 'div' }, t('lead.footnote')))));
248
+ react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: 'secondary', className: index_module_css_1.default.footnote, tag: 'div' }, t('lead.footnote')))))));
142
249
  };
143
250
  exports.LeadForm = LeadForm;
@@ -37,13 +37,17 @@ type ValidateContext = {
37
37
  /** `InputTel` сам знает, валиден ли номер для выбранной страны. */
38
38
  phoneValid: boolean;
39
39
  t: (key: string) => string;
40
+ /** Для перехода между шагами проверяем только поля текущего шага. */
41
+ fieldIds?: Set<string>;
42
+ /** На первом шаге контакт ещё не спрашивали — общая проверка не нужна. */
43
+ requireContact?: boolean;
40
44
  };
41
45
  /**
42
46
  * Валидация на клиенте — это про удобство: сервер всё равно перепроверит
43
47
  * обязательность и наличие канала связи. Возвращаем ошибки по id поля плюс
44
48
  * общую ошибку формы.
45
49
  */
46
- export declare const validate: ({ form, values, phoneValid, t, }: ValidateContext) => {
50
+ export declare const validate: ({ form, values, phoneValid, t, fieldIds, requireContact, }: ValidateContext) => {
47
51
  errors: Record<string, string>;
48
52
  formError?: string;
49
53
  };
@@ -46,20 +46,26 @@ exports.isMultiple = isMultiple;
46
46
  * «уточните какой квест» блокировала бы отправку у всех остальных.
47
47
  */
48
48
  const isVisible = (field, values) => {
49
+ var _a;
49
50
  const condition = field.conditional;
50
51
  if (!(condition === null || condition === void 0 ? void 0 : condition.fieldId))
51
52
  return true;
52
53
  const parentValue = values[condition.fieldId];
53
- if (!condition.optionId) {
54
+ const optionIds = ((_a = condition.optionIds) === null || _a === void 0 ? void 0 : _a.length)
55
+ ? condition.optionIds
56
+ : condition.optionId
57
+ ? [condition.optionId]
58
+ : [];
59
+ if (!optionIds.length) {
54
60
  return Array.isArray(parentValue) ? parentValue.length > 0 : !!parentValue;
55
61
  }
56
- if (Array.isArray(parentValue))
57
- return parentValue.includes(condition.optionId);
58
- return parentValue === condition.optionId;
62
+ if (Array.isArray(parentValue)) {
63
+ return parentValue.some((value) => optionIds.includes(value));
64
+ }
65
+ return typeof parentValue === 'string' && optionIds.includes(parentValue);
59
66
  };
60
67
  exports.isVisible = isVisible;
61
68
  const buildInitialValues = (form, prefill) => {
62
- var _a;
63
69
  const values = {};
64
70
  for (const field of form.fields) {
65
71
  const prefilled = field.mapsTo ? prefill === null || prefill === void 0 ? void 0 : prefill[field.mapsTo] : undefined;
@@ -75,10 +81,10 @@ const buildInitialValues = (form, prefill) => {
75
81
  values[field.id] = [];
76
82
  continue;
77
83
  }
78
- // У степпера гостей всегда есть значение: пустое поле «сколько вас»
79
- // клиент чаще пропускает, чем заполняет.
84
+ // Пустое значение у степпера означает «пока не знаю». Первый клик
85
+ // по плюсу начнёт счёт с минимального значения.
80
86
  if (field.type === lead_field_type_enum_1.LeadFieldTypeEnum.GUESTS) {
81
- values[field.id] = (_a = field.min) !== null && _a !== void 0 ? _a : 2;
87
+ values[field.id] = '';
82
88
  continue;
83
89
  }
84
90
  values[field.id] = '';
@@ -101,11 +107,13 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
101
107
  * обязательность и наличие канала связи. Возвращаем ошибки по id поля плюс
102
108
  * общую ошибку формы.
103
109
  */
104
- const validate = ({ form, values, phoneValid, t, }) => {
110
+ const validate = ({ form, values, phoneValid, t, fieldIds, requireContact = true, }) => {
105
111
  const errors = {};
106
112
  let hasPhone = false;
107
113
  let hasEmail = false;
108
114
  for (const field of form.fields) {
115
+ if (fieldIds && !fieldIds.has(field.id))
116
+ continue;
109
117
  if (!(0, exports.isVisible)(field, values))
110
118
  continue;
111
119
  const value = values[field.id];
@@ -133,7 +141,9 @@ const validate = ({ form, values, phoneValid, t, }) => {
133
141
  }
134
142
  // Заявку без телефона и почты обработать нельзя — бэкенд её отклонит,
135
143
  // так что честнее сказать это до отправки.
136
- const formError = !hasPhone && !hasEmail && !Object.keys(errors).length ? t('lead.needContact') : undefined;
144
+ const formError = requireContact && !hasPhone && !hasEmail && !Object.keys(errors).length
145
+ ? t('lead.needContact')
146
+ : undefined;
137
147
  return { errors, formError };
138
148
  };
139
149
  exports.validate = validate;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const lead_field_type_enum_1 = require("@escapenavigator/types/dist/lead/enums/lead-field-type.enum");
4
+ const lib_1 = require("./lib");
5
+ const form = {
6
+ fields: [
7
+ {
8
+ id: 'occasion',
9
+ type: lead_field_type_enum_1.LeadFieldTypeEnum.RADIO,
10
+ required: true,
11
+ enabled: true,
12
+ order: 0,
13
+ options: [],
14
+ i18n: {},
15
+ },
16
+ {
17
+ id: 'phone',
18
+ type: lead_field_type_enum_1.LeadFieldTypeEnum.PHONE,
19
+ required: true,
20
+ enabled: true,
21
+ order: 1,
22
+ i18n: {},
23
+ },
24
+ ],
25
+ };
26
+ describe('lead form step validation', () => {
27
+ it('validates only planning fields before opening the contacts step', () => {
28
+ const result = (0, lib_1.validate)({
29
+ form,
30
+ values: { occasion: '', phone: '' },
31
+ phoneValid: false,
32
+ t: (key) => key,
33
+ fieldIds: new Set(['occasion']),
34
+ requireContact: false,
35
+ });
36
+ expect(result).toEqual({
37
+ errors: { occasion: 'lead.required' },
38
+ formError: undefined,
39
+ });
40
+ });
41
+ it('shows children age for children and mixed groups', () => {
42
+ const ageField = {
43
+ id: 'age',
44
+ conditional: {
45
+ fieldId: 'audience',
46
+ optionIds: ['children', 'mixed'],
47
+ },
48
+ };
49
+ expect((0, lib_1.isVisible)(ageField, { audience: 'children' })).toBe(true);
50
+ expect((0, lib_1.isVisible)(ageField, { audience: 'mixed' })).toBe(true);
51
+ expect((0, lib_1.isVisible)(ageField, { audience: 'adults' })).toBe(false);
52
+ });
53
+ it('starts an optional guest stepper in the unknown state', () => {
54
+ const guestsForm = {
55
+ fields: [
56
+ {
57
+ id: 'guests',
58
+ type: lead_field_type_enum_1.LeadFieldTypeEnum.GUESTS,
59
+ required: false,
60
+ enabled: true,
61
+ order: 0,
62
+ min: 1,
63
+ i18n: {},
64
+ },
65
+ ],
66
+ };
67
+ expect((0, lib_1.buildInitialValues)(guestsForm)).toEqual({ guests: '' });
68
+ });
69
+ });
@@ -25,7 +25,19 @@ const DEFAULTS = {
25
25
  'lead.unavailable': 'The request form is not available right now',
26
26
  'lead.guestsMinus': 'One guest less',
27
27
  'lead.guestsPlus': 'One guest more',
28
+ 'lead.guestsUnknown': 'Not sure yet',
28
29
  'lead.timeUndecided': 'Not sure about the time yet',
30
+ 'lead.stepPlanning': 'Your preferences',
31
+ 'lead.stepContacts': 'Contacts',
32
+ 'lead.stepCounter': 'Step {{current}} of 2 · {{label}}',
33
+ 'lead.stepContactsTitle': 'We will help you choose a quest',
34
+ 'lead.stepQuestMatchTitle': 'We will help you choose a quest',
35
+ 'lead.stepContactsHint': 'Leave your contacts and preferred date — we will confirm the details.',
36
+ 'lead.next': 'Next — contacts',
37
+ 'lead.back': 'Back',
38
+ 'lead.selection': 'Your choices',
39
+ 'lead.edit': 'Edit',
40
+ 'lead.collapse': 'Collapse',
29
41
  'lead.close': 'Close',
30
42
  'lead.another': 'Send another request',
31
43
  'lead.footnote': 'We will contact you and suggest options. A request is not a booking yet.',
@@ -41,6 +53,18 @@ const DEFAULTS = {
41
53
  const RU_DEFAULTS = {
42
54
  'lead.footnote': 'Менеджер свяжется с вами и предложит варианты. Заявка пока не бронирует игру.',
43
55
  'lead.timeUndecided': 'Пока не определился со временем',
56
+ 'lead.guestsUnknown': 'Не знаю',
57
+ 'lead.stepPlanning': 'Ваши пожелания',
58
+ 'lead.stepContacts': 'Контакты',
59
+ 'lead.stepCounter': 'Шаг {{current}} из 2 · {{label}}',
60
+ 'lead.stepContactsTitle': 'Поможем выбрать ваш квест',
61
+ 'lead.stepQuestMatchTitle': 'Поможем выбрать ваш квест',
62
+ 'lead.stepContactsHint': 'Оставьте контакты, желаемую дату и время — мы уточним детали.',
63
+ 'lead.next': 'Далее — контакты',
64
+ 'lead.back': 'Назад',
65
+ 'lead.selection': 'Вы выбрали',
66
+ 'lead.edit': 'Изменить',
67
+ 'lead.collapse': 'Свернуть',
44
68
  };
45
69
  const useLeadT = () => {
46
70
  var _a;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/hooks",
3
- "version": "2.1.23",
3
+ "version": "2.1.25",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -14,9 +14,9 @@
14
14
  "test": "jest"
15
15
  },
16
16
  "dependencies": {
17
- "@escapenavigator/services": "^2.1.23",
18
- "@escapenavigator/types": "^2.1.23",
19
- "@escapenavigator/utils": "^2.1.23",
17
+ "@escapenavigator/services": "^2.1.25",
18
+ "@escapenavigator/types": "^2.1.25",
19
+ "@escapenavigator/utils": "^2.1.25",
20
20
  "libphonenumber-js": "^1.12.24",
21
21
  "react": "19.2.6",
22
22
  "react-phone-input-2": "^2.15.1"
@@ -32,5 +32,5 @@
32
32
  "ts-jest": "^29.1.1",
33
33
  "typescript": "^5.6"
34
34
  },
35
- "gitHead": "533e344e432372b89b80d81c0ce35bc27463b677"
35
+ "gitHead": "561bdff886f82b428de45a54fc63ce6f71500015"
36
36
  }