@escapenavigator/hooks 2.1.23 → 2.1.24
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.
|
@@ -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,23 @@ 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' })));
|
|
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
|
+
lead_field_type_enum_1.LeadFieldTypeEnum.GUESTS,
|
|
69
|
+
lead_field_type_enum_1.LeadFieldTypeEnum.NUMBER,
|
|
70
|
+
]);
|
|
59
71
|
const LeadForm = ({ form, source, language, profileCountry, submit, prefill, contextNote, withHeader = true, onDone, doneLabel, onSuccess, policyUrl, pdConsentUrl, }) => {
|
|
60
72
|
const t = (0, use_lead_t_1.useLeadT)();
|
|
61
73
|
const consentRequired = (0, personal_data_consent_1.requiresPersonalDataConsent)(profileCountry);
|
|
62
74
|
const [consent, setConsent] = (0, react_1.useState)(false);
|
|
63
75
|
const [showConsentError, setShowConsentError] = (0, react_1.useState)(false);
|
|
76
|
+
const [step, setStep] = (0, react_1.useState)(1);
|
|
64
77
|
const [values, setValues] = (0, react_1.useState)(() => (0, lib_1.buildInitialValues)(form, prefill));
|
|
65
78
|
const [errors, setErrors] = (0, react_1.useState)({});
|
|
66
79
|
const [formError, setFormError] = (0, react_1.useState)();
|
|
@@ -74,6 +87,61 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
74
87
|
setErrors((prev) => (prev[fieldId] ? Object.assign(Object.assign({}, prev), { [fieldId]: '' }) : prev));
|
|
75
88
|
setFormError(undefined);
|
|
76
89
|
};
|
|
90
|
+
const visibleFields = form.fields.filter((field) => (0, lib_1.isVisible)(field, values));
|
|
91
|
+
const planningFields = visibleFields.filter((field) => PLANNING_FIELD_TYPES.has(field.type));
|
|
92
|
+
const contactFields = visibleFields.filter((field) => !PLANNING_FIELD_TYPES.has(field.type));
|
|
93
|
+
const hasTwoSteps = planningFields.length > 0 && contactFields.length > 0;
|
|
94
|
+
const activeFields = !hasTwoSteps ? visibleFields : step === 1 ? planningFields : contactFields;
|
|
95
|
+
const planningFieldIds = new Set(planningFields.map((field) => field.id));
|
|
96
|
+
const planningSummary = planningFields
|
|
97
|
+
.map((field) => {
|
|
98
|
+
var _a, _b, _c;
|
|
99
|
+
const value = values[field.id];
|
|
100
|
+
if (value === undefined || value === '' || value === false)
|
|
101
|
+
return '';
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
return value
|
|
104
|
+
.map((id) => {
|
|
105
|
+
var _a;
|
|
106
|
+
const selected = (_a = field.options) === null || _a === void 0 ? void 0 : _a.find((item) => item.id === id);
|
|
107
|
+
return selected ? (0, lib_1.optionLabel)(selected, language, id) : id;
|
|
108
|
+
})
|
|
109
|
+
.join(', ');
|
|
110
|
+
}
|
|
111
|
+
if (typeof value === 'string' && ((_a = field.options) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
112
|
+
const selected = field.options.find((item) => item.id === value);
|
|
113
|
+
return selected ? (0, lib_1.optionLabel)(selected, language, value) : value;
|
|
114
|
+
}
|
|
115
|
+
if (field.type === lead_field_type_enum_1.LeadFieldTypeEnum.QUESTROOM) {
|
|
116
|
+
return (((_b = form.questrooms.find((item) => String(item.id) === String(value))) === null || _b === void 0 ? void 0 : _b.title) || '');
|
|
117
|
+
}
|
|
118
|
+
if (field.type === lead_field_type_enum_1.LeadFieldTypeEnum.LOCATION) {
|
|
119
|
+
return (((_c = form.locations.find((item) => String(item.id) === String(value))) === null || _c === void 0 ? void 0 : _c.title) || '');
|
|
120
|
+
}
|
|
121
|
+
if (field.type === lead_field_type_enum_1.LeadFieldTypeEnum.GUESTS ||
|
|
122
|
+
field.type === lead_field_type_enum_1.LeadFieldTypeEnum.NUMBER) {
|
|
123
|
+
return `${(0, lib_1.fieldTitle)(field, language)}: ${String(value)}`;
|
|
124
|
+
}
|
|
125
|
+
return String(value);
|
|
126
|
+
})
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
const handleNext = () => {
|
|
129
|
+
const result = (0, lib_1.validate)({
|
|
130
|
+
form,
|
|
131
|
+
values,
|
|
132
|
+
phoneValid,
|
|
133
|
+
t,
|
|
134
|
+
fieldIds: planningFieldIds,
|
|
135
|
+
requireContact: false,
|
|
136
|
+
});
|
|
137
|
+
if (Object.keys(result.errors).length) {
|
|
138
|
+
setErrors(result.errors);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
setErrors({});
|
|
142
|
+
setFormError(undefined);
|
|
143
|
+
setStep(2);
|
|
144
|
+
};
|
|
77
145
|
const handleSubmit = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
146
|
var _a;
|
|
79
147
|
if (sending)
|
|
@@ -84,6 +152,9 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
84
152
|
if (Object.keys(result.errors).length || result.formError || consentMissing) {
|
|
85
153
|
setErrors(result.errors);
|
|
86
154
|
setFormError(result.formError);
|
|
155
|
+
if (Object.keys(result.errors).some((fieldId) => planningFieldIds.has(fieldId))) {
|
|
156
|
+
setStep(1);
|
|
157
|
+
}
|
|
87
158
|
return;
|
|
88
159
|
}
|
|
89
160
|
setSending(true);
|
|
@@ -118,18 +189,34 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
118
189
|
}
|
|
119
190
|
const title = (0, lib_1.localized)(form.clientTitle, language) || t('lead.title');
|
|
120
191
|
const description = (0, lib_1.localized)(form.description, language);
|
|
192
|
+
const stepLabel = step === 1 ? t('lead.stepPlanning') : t('lead.stepContacts');
|
|
193
|
+
const headerTitle = hasTwoSteps && step === 2 ? t('lead.stepContactsTitle') : title;
|
|
194
|
+
const headerDescription = hasTwoSteps && step === 2 ? t('lead.stepContactsHint') : description;
|
|
121
195
|
return (react_1.default.createElement("div", { className: index_module_css_1.default.form },
|
|
122
196
|
withHeader && (react_1.default.createElement("div", { className: index_module_css_1.default.header },
|
|
123
|
-
react_1.default.createElement(
|
|
124
|
-
|
|
125
|
-
|
|
197
|
+
hasTwoSteps ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
198
|
+
react_1.default.createElement("div", { className: index_module_css_1.default.stepMeta },
|
|
199
|
+
step === 2 ? (react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.backButton, onClick: () => setStep(1) },
|
|
200
|
+
react_1.default.createElement(ArrowLeft, null),
|
|
201
|
+
t('lead.back'))) : (react_1.default.createElement("span", null)),
|
|
202
|
+
react_1.default.createElement("span", null, t('lead.stepCounter', {
|
|
203
|
+
current: step,
|
|
204
|
+
label: stepLabel,
|
|
205
|
+
}))),
|
|
206
|
+
react_1.default.createElement("div", { className: index_module_css_1.default.progress, "aria-hidden": 'true' },
|
|
207
|
+
react_1.default.createElement("span", { "data-step": step })))) : (react_1.default.createElement("span", { className: index_module_css_1.default.headerAccent, "aria-hidden": 'true' })),
|
|
208
|
+
react_1.default.createElement(ui_1.Typography.Title, { tag: 'h1', view: 'small', weight: 'bold' }, headerTitle),
|
|
209
|
+
!!headerDescription && (react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', color: 'secondary' }, headerDescription)))),
|
|
126
210
|
!!formError && react_1.default.createElement(ui_1.ToastPlate, { view: 'negative' }, formError),
|
|
127
|
-
react_1.default.createElement("div", { className: index_module_css_1.default.
|
|
128
|
-
.
|
|
129
|
-
|
|
211
|
+
hasTwoSteps && step === 2 && !!planningSummary.length && (react_1.default.createElement("div", { className: index_module_css_1.default.selectionSummary },
|
|
212
|
+
react_1.default.createElement("div", null,
|
|
213
|
+
react_1.default.createElement("span", null, t('lead.selection')),
|
|
214
|
+
react_1.default.createElement("strong", null, planningSummary.join(' · '))),
|
|
215
|
+
react_1.default.createElement("button", { type: 'button', onClick: () => setStep(1) }, t('lead.edit')))),
|
|
216
|
+
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
217
|
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
218
|
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 },
|
|
219
|
+
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
220
|
(consentRequired || !!policyUrl) && (react_1.default.createElement(personal_data_consent_1.PersonalDataConsent, { required: consentRequired, consentUrl: pdConsentUrl || '', policyUrl: policyUrl || '', checked: consent, showError: showConsentError, onChange: (checked) => {
|
|
134
221
|
setConsent(checked);
|
|
135
222
|
if (checked)
|
|
@@ -138,6 +225,6 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
|
|
|
138
225
|
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
226
|
? t('lead.sending')
|
|
140
227
|
: (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')))));
|
|
228
|
+
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
229
|
};
|
|
143
230
|
exports.LeadForm = LeadForm;
|
package/dist/lead-form/lib.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/lead-form/lib.js
CHANGED
|
@@ -101,11 +101,13 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
|
|
101
101
|
* обязательность и наличие канала связи. Возвращаем ошибки по id поля плюс
|
|
102
102
|
* общую ошибку формы.
|
|
103
103
|
*/
|
|
104
|
-
const validate = ({ form, values, phoneValid, t, }) => {
|
|
104
|
+
const validate = ({ form, values, phoneValid, t, fieldIds, requireContact = true, }) => {
|
|
105
105
|
const errors = {};
|
|
106
106
|
let hasPhone = false;
|
|
107
107
|
let hasEmail = false;
|
|
108
108
|
for (const field of form.fields) {
|
|
109
|
+
if (fieldIds && !fieldIds.has(field.id))
|
|
110
|
+
continue;
|
|
109
111
|
if (!(0, exports.isVisible)(field, values))
|
|
110
112
|
continue;
|
|
111
113
|
const value = values[field.id];
|
|
@@ -133,7 +135,9 @@ const validate = ({ form, values, phoneValid, t, }) => {
|
|
|
133
135
|
}
|
|
134
136
|
// Заявку без телефона и почты обработать нельзя — бэкенд её отклонит,
|
|
135
137
|
// так что честнее сказать это до отправки.
|
|
136
|
-
const formError = !hasPhone && !hasEmail && !Object.keys(errors).length
|
|
138
|
+
const formError = requireContact && !hasPhone && !hasEmail && !Object.keys(errors).length
|
|
139
|
+
? t('lead.needContact')
|
|
140
|
+
: undefined;
|
|
137
141
|
return { errors, formError };
|
|
138
142
|
};
|
|
139
143
|
exports.validate = validate;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
});
|
|
@@ -26,6 +26,15 @@ const DEFAULTS = {
|
|
|
26
26
|
'lead.guestsMinus': 'One guest less',
|
|
27
27
|
'lead.guestsPlus': 'One guest more',
|
|
28
28
|
'lead.timeUndecided': 'Not sure about the time yet',
|
|
29
|
+
'lead.stepPlanning': 'Your preferences',
|
|
30
|
+
'lead.stepContacts': 'Contacts',
|
|
31
|
+
'lead.stepCounter': 'Step {{current}} of 2 · {{label}}',
|
|
32
|
+
'lead.stepContactsTitle': 'How can we contact you?',
|
|
33
|
+
'lead.stepContactsHint': 'Leave your contacts and preferred date — we will confirm the details.',
|
|
34
|
+
'lead.next': 'Next — contacts',
|
|
35
|
+
'lead.back': 'Back',
|
|
36
|
+
'lead.selection': 'Your choices',
|
|
37
|
+
'lead.edit': 'Edit',
|
|
29
38
|
'lead.close': 'Close',
|
|
30
39
|
'lead.another': 'Send another request',
|
|
31
40
|
'lead.footnote': 'We will contact you and suggest options. A request is not a booking yet.',
|
|
@@ -41,6 +50,15 @@ const DEFAULTS = {
|
|
|
41
50
|
const RU_DEFAULTS = {
|
|
42
51
|
'lead.footnote': 'Менеджер свяжется с вами и предложит варианты. Заявка пока не бронирует игру.',
|
|
43
52
|
'lead.timeUndecided': 'Пока не определился со временем',
|
|
53
|
+
'lead.stepPlanning': 'Ваши пожелания',
|
|
54
|
+
'lead.stepContacts': 'Контакты',
|
|
55
|
+
'lead.stepCounter': 'Шаг {{current}} из 2 · {{label}}',
|
|
56
|
+
'lead.stepContactsTitle': 'Как с вами связаться?',
|
|
57
|
+
'lead.stepContactsHint': 'Оставьте контакты, желаемую дату и время — мы уточним детали.',
|
|
58
|
+
'lead.next': 'Далее — контакты',
|
|
59
|
+
'lead.back': 'Назад',
|
|
60
|
+
'lead.selection': 'Вы выбрали',
|
|
61
|
+
'lead.edit': 'Изменить',
|
|
44
62
|
};
|
|
45
63
|
const useLeadT = () => {
|
|
46
64
|
var _a;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/hooks",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.24",
|
|
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.
|
|
18
|
-
"@escapenavigator/types": "^2.1.
|
|
19
|
-
"@escapenavigator/utils": "^2.1.
|
|
17
|
+
"@escapenavigator/services": "^2.1.24",
|
|
18
|
+
"@escapenavigator/types": "^2.1.24",
|
|
19
|
+
"@escapenavigator/utils": "^2.1.24",
|
|
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": "
|
|
35
|
+
"gitHead": "153992d51e958b46acefa16ab49c9a0c1f727150"
|
|
36
36
|
}
|