{"version":3,"file":"index-C8X53FvG.js","sources":["../../../src/utils/error.ts","../../../src/stepsContext.tsx","../../../src/api.tsx","../../../src/components/OutletComponent.tsx","../../../src/components/InnerPage/InnerPage.tsx","../../../src/utils/isMobileDevice.ts","../../../src/components/Card/Card.tsx","../../../src/components/LayoutCard/LayoutCard.tsx","../../../src/icons/IconLeftArrow.tsx","../../../src/components/ui/PageLoader/PageLoader.tsx","../../../src/components/Layout/Layout.tsx","../../../src/icons/IconPhone.tsx","../../../src/icons/IconSMS.tsx","../../../src/icons/IconTelegram.tsx","../../../src/icons/IconWhatsapp.tsx","../../../src/components/ui/DotLoader/DotLoader.tsx","../../../src/components/ui/Button/Button.tsx","../../../src/icons/IconApply.tsx","../../../src/icons/IconApplyLarge.tsx","../../../src/icons/IconX.tsx","../../../src/icons/IconGiftSmall.tsx","../../../src/components/ui/GiftBall/GiftBall.tsx","../../../src/components/ui/Checkbox/Checkbox.tsx","../../../src/components/CommunicationTypes/CommunicationTypes.tsx","../../../src/components/AuthForm/SendCode/Timer.tsx","../../../src/components/ClientForm/useValidateClientForm.tsx","../../../src/components/ui/Input/Input.tsx","../../../src/components/AuthForm/SendCode/SendCode.tsx","../../../src/components/AuthForm/TryByMessengers.tsx","../../../src/components/ui/InputMask/InputMask.tsx","../../../src/components/AuthForm/AuthForm.tsx","../../../src/icons/IconArrowDropdown.tsx","../../../src/utils/validateEmail.ts","../../../src/components/ui/Autocomplete/Autocomplete.tsx","../../../src/dadata.ts","../../../src/utils/validateDate.ts","../../../src/components/ClientForm/ChildrenInput/ChildrenInput.tsx","../../../src/components/ui/InputBarcode/useBarcodeScanner.tsx","../../../src/icons/Barcode.tsx","../../../src/components/ui/InputBarcode/InputBarcode.tsx","../../../src/components/ClientForm/FieldComponent.tsx","../../../src/components/ClientForm/ClientForm.tsx","../../../src/components/FormRouter/FormRouter.tsx","../../../src/icons/IconError500.tsx","../../../src/pages/ErrorPage/ErrorPage.tsx","../../../src/pages/ThankyouPage/ThankyouPage.tsx","../../../src/pages/QRCodePage/QRCodePage.tsx","../../../src/pages/MainPage/MainPage.tsx","../../../src/App.tsx","../../../src/main.tsx"],"sourcesContent":["export const errorMap: Record = {\n // start\n ErrFormNotFound: 'Страница не найдена',\n 1: 'Страница не найдена',\n\n // send-code\n // submit-phone\n // submit-client\n // pass\n //(это когда ты пытаешься сделать запрос, не сделав /start, или сессия протухла)\"\n ErrSessionNotStarted: 'Сессия истекла, перезагрузите страницу',\n 2: 'Сессия истекла, перезагрузите страницу',\n\n // send-code\n ErrNotAgreedWithTerms: 'Вы не согласились с условиями',\n 3: 'Вы не согласились с условиями',\n\n // send-code\n // submit-phone\n ErrIncorrectPhone: 'Неверный номер телефона',\n 4: 'Неверный номер телефона',\n\n // send-code\n ErrTooManyCodeRequests: 'Слишком частая отправка кода',\n 5: 'Слишком частая отправка кода',\n\n // submit-phone\n ErrPhonesMismatch: 'Телефоны не совпадают',\n ErrCodesMismatch: 'Неверный код',\n ErrCodeExpired: 'Срок действия кода истёк',\n\n 6: 'Телефоны не совпадают',\n 7: 'Неверный код',\n 8: 'Срок действия кода истёк',\n\n // submit-client\n ErrPhoneNotVerified: 'Телефон не подтвержден',\n ErrValidationFailed: 'Форма заполнена неверно',\n ErrServerError: 'Не удалось отправить запрос, повторите попытку позднее',\n\n 9: 'Телефон не подтвержден',\n 10: 'Форма заполнена неверно',\n 11: 'Не удалось отправить запрос, повторите попытку позднее',\n 12: 'Ошибка проверки браузера',\n\n // field-level errors\n ErrFieldRequired: 'Поле обязательно к заполнению',\n ErrFieldInvalid: 'Поле заполнено неверно',\n ErrFieldDuplicating: 'Клиент с таким номером уже существует',\n\n 100: 'Поле обязательно к заполнению',\n 101: 'Поле заполнено неверно',\n 102: 'Клиент с таким номером уже существует',\n}\n","import React, { createContext, useCallback, useContext, useState } from 'react'\nimport {\n Field,\n Steps,\n Brand,\n StepsContextType,\n RequestError,\n Client,\n} from './types'\n\nconst StepsContext = createContext({\n isChangePhoneNumber: false,\n isInitRequestLoading: false,\n agreedWithTerms: true,\n phoneMask: '',\n fields: [],\n commonClientData: undefined,\n userPhoneNumber: '',\n requestError: {\n code: '',\n is400: false,\n is500: false,\n },\n brand: {\n phoneMask: '',\n name: null,\n logo: '',\n cardColor: '',\n brandName: '',\n termsLink: '',\n codeAdapter: '',\n verifyMethods: [],\n phoneNumberVerified: false,\n dadataApiKey: '',\n },\n walletsLink: '',\n verifyByTelegram: false,\n verifyByWhatsapp: false,\n currentStep: 'start',\n handleChangeStep: () => {},\n setAgreedWithTerms: () => {},\n setWalletsLink: () => {},\n setCurrentStep: () => {},\n setBrand: () => {},\n setCommonClientData: () => {},\n setFields: () => {},\n setUserPhoneNumber: () => {},\n setRequestError: () => {},\n setIsChangePhoneNumber: () => {},\n setIsInitRequestLoading: () => {},\n})\n\nexport const StepsContextProvider = ({\n children,\n}: {\n children: React.ReactNode\n}) => {\n const [isInitRequestLoading, setIsInitRequestLoading] = useState(false)\n const [isChangePhoneNumber, setIsChangePhoneNumber] = useState(false)\n const [brand, setBrand] = useState(null)\n const [commonClientData, setCommonClientData] = useState(\n undefined,\n )\n const [userPhoneNumber, setUserPhoneNumber] = useState('')\n\n const [walletsLink, setWalletsLink] = useState(null)\n const [agreedWithTerms, setAgreedWithTerms] = useState(true)\n const [fields, setFields] = useState([])\n const [requestError, setRequestError] = useState({\n code: '',\n is400: false,\n is500: false,\n })\n\n const [currentStep, setCurrentStep] = useState('start')\n\n const handleChangeStep = useCallback((step: Steps) => {\n setCurrentStep(step)\n }, [])\n\n const verifyByTelegram = brand?.verifyMethods?.includes('telegram') || false\n const verifyByWhatsapp = brand?.verifyMethods?.includes('whatsapp') || false\n\n return (\n \n {children}\n \n )\n}\n\nexport const useStepsContext = (): StepsContextType => {\n return useContext(StepsContext)\n}\n","import { useCallback, useState } from 'react'\nimport { errorMap } from './utils/error'\n\nimport axios, { AxiosError } from 'axios'\nimport { useStepsContext } from './stepsContext'\nimport { Error } from './types'\n\n// Для работы с локальным беком в режиме разработки поставить - /api\n// Для работы с беком на тестовом стенде - https://form-test.maxma.com/\nconst baseURL =\n process.env.NODE_ENV === 'production' ? '/' : 'https://form-test.maxma.com/'\n\nconst app = axios.create({\n baseURL,\n withCredentials: true,\n})\n\ntype HandleFetch = {\n only500?: boolean\n isErrorPageRedirect?: boolean\n domain: string\n handleSuccess?: (data: T) => void\n handleError?: (error: Error) => void\n handleFinally?: () => void\n}\n\nexport function useApi>({\n isErrorPageRedirect = false,\n only500 = false,\n domain,\n handleError,\n handleFinally,\n handleSuccess,\n}: HandleFetch) {\n const [error, setError] = useState(false)\n const [isLoading, setIsLoading] = useState(false)\n const [data, setData] = useState(null)\n\n const { setRequestError, setIsChangePhoneNumber, setCurrentStep } =\n useStepsContext()\n\n const resetError = () => {\n setError(false)\n }\n\n const handleFetch = useCallback(\n (body?: Record, token?: string) => {\n setError(false)\n setIsLoading(true)\n setData(null)\n\n const isProd = process.env.NODE_ENV === 'production'\n\n const headers: Record = {\n 'Content-Type': 'application/json',\n }\n\n if (token) {\n headers['X-ReCaptcha-Response'] = isProd ? token : 'dev-token'\n }\n\n app\n .post(domain, body, {\n headers,\n })\n\n .catch((error: AxiosError) => {\n const errorCode =\n errorMap[error?.response?.data.error?.code as keyof typeof errorMap]\n\n if (\n (isErrorPageRedirect || only500) &&\n error.status &&\n (only500 ? error.status >= 500 : error.status >= 400)\n ) {\n setRequestError({\n code: errorCode,\n is400: error.status >= 400 && error.status < 500,\n is500: error.status >= 500,\n })\n setIsChangePhoneNumber(true)\n setCurrentStep('error')\n\n return\n }\n\n if (error.response && error.response.data.error) {\n setError(errorCode)\n }\n\n handleError?.(error.response!.data)\n })\n .then((response) => {\n if (response?.data) {\n handleSuccess?.(response?.data)\n setData(response?.data)\n }\n })\n\n .finally(() => {\n setIsLoading(false)\n handleFinally?.()\n })\n },\n [],\n )\n\n return {\n isLoading,\n error,\n data,\n handleFetch,\n resetError,\n }\n}\n","import { useParams, Outlet } from 'react-router-dom'\nimport { useEffect } from 'react'\nimport { useApi } from '../api'\nimport { StartResponse } from '../types'\nimport { useStepsContext } from '../stepsContext'\n\nconst DEFAULT_CODE_ADAPTER = 'sms'\n\nconst DEFAULT_VERIFY_METHODS = ['telegram', 'whatsapp', 'sms']\n\nconst DEFAULT_MASK = '+7 (###) ###-##-##'\n\nexport const OutletComponent = () => {\n const {\n setBrand,\n setFields,\n setWalletsLink,\n setCurrentStep,\n setUserPhoneNumber,\n setCommonClientData,\n setIsInitRequestLoading,\n } = useStepsContext()\n\n const { query } = useParams()\n\n const { handleFetch } = useApi({\n isErrorPageRedirect: true,\n domain: 'sess',\n handleSuccess: (data) => {\n setBrand({\n name: '',\n logo: data?.appearance.logo || '',\n cardColor: data?.appearance.cardColor || '',\n brandName: data?.appearance.brandName || '',\n termsLink: data?.appearance.termsLink || '',\n codeAdapter: data?.appearance.codeAdapter || DEFAULT_CODE_ADAPTER,\n verifyMethods: data?.appearance.verifyMethods || DEFAULT_VERIFY_METHODS,\n phoneNumberVerified: data?.phoneNumberVerified || false,\n dadataApiKey: data?.dadataApiKey || '',\n phoneMask: data?.appearance.phoneMask || DEFAULT_MASK,\n })\n setCurrentStep(data?.step)\n\n setUserPhoneNumber(data?.phoneNumber)\n\n if (data?.client) {\n setCommonClientData(data?.client)\n\n setWalletsLink(data.client.walletsLink)\n }\n\n if (data?.fields.length) setFields(data?.fields)\n },\n\n handleFinally: () => {\n setIsInitRequestLoading(false)\n },\n })\n\n const search = window.location.search\n const params = new URLSearchParams(search)\n const queryParams = Object.fromEntries(params)\n\n useEffect(() => {\n setIsInitRequestLoading(true)\n handleFetch({\n formCode: query,\n query: queryParams,\n })\n }, [])\n\n return \n}\n","import styles from './InnerPage.module.scss'\nimport classNames from 'classnames'\n\ntype InnerPageProps = {\n title?: string\n containerClassName?: string\n subTitle?: string\n mobileSubTitle?: string\n center?: boolean\n children: React.ReactNode\n}\n\nexport const InnerPage: React.FC = ({\n center = false,\n mobileSubTitle,\n subTitle,\n title,\n containerClassName,\n children,\n}) => {\n return (\n \n \n {title}\n \n\n {subTitle && (\n \n {subTitle}\n \n )}\n\n {mobileSubTitle && (\n \n {mobileSubTitle}\n \n )}\n\n
{children}
\n \n )\n}\n","export function isMobileDevice() {\n return (\n typeof window.orientation !== 'undefined' ||\n navigator.userAgent.indexOf('Mobile') !== -1\n )\n}\n","import styles from './Card.module.scss'\nimport classNames from 'classnames'\nimport React, { useEffect, useState } from 'react'\nimport { isMobileDevice } from '~/src/utils/isMobileDevice'\n\ntype CardProps = {\n img?: string | null\n bgColor: string\n}\n\nexport const Card: React.FC = ({ bgColor = '#fff', img }) => {\n const cardRef = React.useRef(null)\n\n const [listening, setListening] = useState(false)\n const [calculated, setCalculated] = useState(0)\n const [transition, setTransition] = useState(true)\n\n const isMobile = isMobileDevice()\n\n const handleScroll = () => {\n const scrollTop = Math.max(\n window.scrollY,\n document.documentElement.scrollTop,\n )\n\n const maxDeg = 40\n const deg = (maxDeg * scrollTop) / 200\n\n cardRef.current!.style.transform = `rotate3d(-1, 0, 0, ${1 + deg}deg)`\n }\n\n useEffect(() => {\n if (isMobile) {\n setCalculated(1)\n setTransition(false)\n window.addEventListener('scroll', handleScroll)\n }\n }, [])\n\n const handleMouseleave = () => {\n if (isMobile) return\n\n setListening(false)\n setTransition(true)\n setCalculated(0)\n cardRef.current!.style.transform = ''\n }\n\n const handleMouseenter = () => {\n if (isMobile) return\n\n setListening(true)\n setTransition(false)\n setCalculated(0)\n }\n\n const compute = (event: React.MouseEvent) => {\n const { clientX, clientY } = event\n const { top, left, width, height } =\n cardRef.current!.getBoundingClientRect()\n\n if (calculated < 1) {\n setCalculated(calculated + 0.02)\n }\n\n return {\n x: (((clientX - left) / width) * 2 - 1) * calculated,\n y: (((clientY - top) / height) * 2 - 1) * calculated,\n }\n }\n\n const handleMousemove: React.MouseEventHandler = (evt) => {\n if (!listening || isMobile) return\n const { x, y } = compute(evt)\n\n cardRef.current!.style.transform = `rotate3d(${-y}, ${x}, 0, ${\n 4 * calculated\n }deg)`\n }\n\n return (\n
\n \n {!img ? (\n \n ) : (\n \n \n
\n )}\n\n
123456
\n \n \n )\n}\n","import { useStepsContext } from '~/src/stepsContext'\nimport { Card } from '../Card/Card'\nimport styles from './LayoutCard.module.scss'\n\nexport const LayoutCard: React.FC = () => {\n const { brand } = useStepsContext()\n\n return (\n
\n
\n \n
\n
\n
\n )\n}\n","export const IconLeftArrow = () => (\n \n \n \n)\n","import { InnerPage } from '../../InnerPage/InnerPage'\nimport styles from './PageLoader.module.scss'\n\nexport const PageLoader = () => {\n return (\n \n
\n
\n
\n \n )\n}\n","import styles from './Layout.module.scss'\nimport classNames from 'classnames'\nimport React from 'react'\nimport { LayoutCard } from '../LayoutCard/LayoutCard'\nimport { IconLeftArrow } from '../../icons/IconLeftArrow'\nimport { useStepsContext } from '~/src/stepsContext'\nimport { PageLoader } from '../ui/PageLoader/PageLoader'\n\ntype LayoutProps = {\n back?: boolean\n withCard?: boolean\n fullHeight?: boolean\n children?: React.ReactNode\n}\n\nexport const Layout: React.FC = ({\n fullHeight = false,\n back,\n withCard,\n children,\n}) => {\n const { isInitRequestLoading, brand, setCurrentStep, setUserPhoneNumber } =\n useStepsContext()\n\n const handleClickBack = () => {\n setUserPhoneNumber('')\n setCurrentStep('start')\n }\n\n return (\n \n
\n
\n \n
\n
\n\n
\n
\n
\n {back && (\n
\n \n
\n )}\n\n {withCard && (\n
\n \n
\n )}\n\n {isInitRequestLoading ? : children}\n
\n
\n
\n\n
\n
Сделано на технологии MAXMA.com
\n
\n
\n )\n}\n","export const IconPhone = () => (\n \n \n \n \n \n)\n","export const IconSMS = () => (\n \n \n \n)\n","export const IconTelegram = () => (\n \n \n \n)\n","export const IconWhatsapp = () => (\n \n \n \n)\n","import styles from './DotLoader.module.scss'\n\nexport const DotsLoader = () => {\n return (\n
\n
\n
\n
\n
\n )\n}\n","import { IconPhone } from '../../../icons/IconPhone'\nimport { IconSMS } from '../../../icons/IconSMS'\nimport { IconTelegram } from '../../../icons/IconTelegram'\nimport { IconWhatsapp } from '../../../icons/IconWhatsapp'\nimport { DotsLoader } from '../DotLoader/DotLoader'\nimport styles from './Button.module.scss'\nimport classNames from 'classnames'\n\ntype ButtonProps = {\n title: string\n href?: string\n className?: string\n type?: 'submit' | 'button'\n variant?: 'telegram' | 'whatsapp' | 'phone' | 'sms' | 'simple'\n disabled?: boolean\n isLoading?: boolean\n onClick?: () => void\n}\n\nconst ICONS = {\n telegram: IconTelegram,\n whatsapp: IconWhatsapp,\n phone: IconPhone,\n sms: IconSMS,\n simple: null,\n}\n\nexport const Button: React.FC = ({\n disabled = false,\n isLoading = false,\n href,\n title,\n className,\n variant = 'simple',\n onClick,\n type = 'button',\n}) => {\n const ButtonComponent = href ? 'a' : 'button'\n\n const Icon = ICONS[variant]\n\n return (\n \n
\n {isLoading ? (\n \n ) : (\n <>\n {title}\n {Icon && }\n \n )}\n
\n \n )\n}\n","export const IconApply = () => (\n \n \n \n)\n","export const IconApplyLarge = () => (\n \n \n \n)\n","export const IconX = () => (\n \n \n \n \n)\n","export const IconGiftSmall = () => (\n \n \n \n)\n","import { useState } from 'react'\nimport { IconGiftSmall } from '../../../icons/IconGiftSmall'\nimport styles from './GiftBall.module.scss'\n\ntype GiftBallProps = {\n className?: string\n value: number\n hasInlineHint?: boolean\n}\n\nconst TEXT_HINT = 'Получите бонус за заполнение поля'\n\nexport const GiftBall: React.FC = ({\n className,\n hasInlineHint,\n value,\n}) => {\n const [isActive, setIsActive] = useState(false)\n\n return (\n
\n setIsActive(true)}\n onMouseLeave={() => setIsActive(false)}\n className={styles.giftball}\n >\n +{value}{' '}\n
\n \n
\n {!hasInlineHint && isActive && (\n
{TEXT_HINT}
\n )}\n \n\n {hasInlineHint && (\n {TEXT_HINT}\n )}\n
\n )\n}\n","import { useState } from 'react'\nimport { IconApply } from '../../../icons/IconApply'\nimport { IconApplyLarge } from '../../../icons/IconApplyLarge'\nimport { IconX } from '../../../icons/IconX'\nimport { GiftBall } from '../GiftBall/GiftBall'\nimport styles from './Checkbox.module.scss'\nimport classNames from 'classnames'\n\ntype CheckboxProps = {\n checked: boolean\n disabled?: boolean\n asFormControl?: boolean\n error?: boolean\n notice?: string\n giftBall?: number\n className?: string\n name?: string\n children?: React.ReactNode\n iconType?: 'checkMark' | 'checkMarkLarge' | 'cross'\n onChange?: (checked: boolean) => void\n}\n\nconst ICONS = {\n checkMark: IconApply,\n checkMarkLarge: IconApplyLarge,\n cross: IconX,\n}\n\nexport const Checkbox: React.FC = ({\n iconType = 'checkMark',\n asFormControl = false,\n checked = false,\n disabled = false,\n error,\n giftBall,\n notice,\n className,\n children,\n onChange,\n name,\n}) => {\n const [isChecked, setIsChecked] = useState(checked)\n\n const handleChange = () => {\n setIsChecked(!isChecked)\n onChange?.(!isChecked)\n }\n\n const Icon = ICONS[iconType]\n\n return (\n \n \n
\n \n \n
\n\n
\n {children}\n\n {notice &&
{notice}
}\n
\n\n {!!giftBall && !disabled && (\n \n )}\n
\n \n )\n}\n","import { useStepsContext } from '~/src/stepsContext'\nimport { Button } from '../ui/Button/Button'\nimport { Checkbox } from '../ui/Checkbox/Checkbox'\nimport styles from './CommunicationTypes.module.scss'\n\nexport const CommunicationTypes: React.FC = () => {\n const { agreedWithTerms, brand, setAgreedWithTerms, handleChangeStep } =\n useStepsContext()\n\n return (\n
\n {brand?.verifyMethods?.map((method) => {\n if (method === 'telegram') {\n return (\n \n )\n }\n\n if (method === 'whatsapp') {\n return (\n \n )\n }\n\n if (method === 'sms') {\n return (\n handleChangeStep('otp')}\n variant='sms'\n title='ЧЕРЕЗ SMS'\n disabled={!agreedWithTerms}\n />\n )\n }\n })}\n\n setAgreedWithTerms(!agreedWithTerms)}\n >\n {brand?.termsLink ? (\n \n ) : (\n \n Текст требует согласования с партнером\n \n )}\n \n
\n )\n}\n","import React, { useState, useEffect } from 'react'\nimport styles from './SendCode.module.scss'\nimport classNames from 'classnames'\n\ntype TimerProps = {\n initialSeconds: number\n refetchCode: () => void\n}\n\nconst Timer: React.FC = ({ initialSeconds, refetchCode }) => {\n const [seconds, setSeconds] = useState(initialSeconds)\n\n useEffect(() => {\n const interval = setInterval(() => {\n setSeconds((prevSeconds) => {\n if (prevSeconds > 0) {\n return prevSeconds - 1\n } else {\n clearInterval(interval)\n return 0\n }\n })\n }, 1000)\n\n return () => clearInterval(interval)\n }, [seconds])\n\n const formatTime = (time: number) => {\n const minutes = Math.floor(time / 60)\n .toString()\n .padStart(2, '0')\n const seconds = (time % 60).toString().padStart(2, '0')\n\n return `${minutes}:${seconds}`\n }\n\n return (\n
\n {seconds ? (\n `Запросить новый код можно через ${formatTime(seconds)}`\n ) : (\n {\n refetchCode()\n setSeconds(initialSeconds)\n }}\n >\n Запросить новый код\n
\n )}\n
\n )\n}\n\nexport default Timer\n","import { useEffect } from 'react'\n\nexport const useValidateClientForm = (handleError: () => void) => {\n useEffect(() => {\n const clientForm = document?.getElementById('clientForm')\n\n clientForm?.addEventListener('submit', handleError)\n\n return () => {\n clientForm?.removeEventListener('submit', handleError)\n }\n }, [handleError])\n}\n","import { useCallback, useState } from 'react'\nimport classNames from 'classnames'\n\nimport styles from './Input.module.scss'\nimport { type AriaTextFieldProps } from '@react-aria/textfield'\nimport { GiftBall } from '../GiftBall/GiftBall'\n\nimport { Label, TextField, Input as AriaInput } from 'react-aria-components'\nimport { useValidateClientForm } from '../../ClientForm/useValidateClientForm'\nexport type InputProps = AriaTextFieldProps & {\n isDate?: boolean\n withChange?: boolean\n placeholder?: string\n className?: string\n inputClassName?: string\n label?: string\n notice?: string\n giftBall?: number\n mask?: string\n inputRef?: React.RefObject\n children?: React.ReactNode\n rigthElement?: React.ReactNode\n onChange?: (value: string) => void\n handleClickChange?: () => void\n onValid?: (value: string) => string\n}\n\nexport const Input: React.FC = ({\n withChange = false,\n label,\n notice,\n className,\n inputClassName,\n handleClickChange,\n giftBall,\n onValid,\n onChange,\n inputRef,\n errorMessage,\n rigthElement,\n ...props\n}) => {\n const [query, setQuery] = useState(props.defaultValue || '')\n\n const [focus, setFocus] = useState(false)\n\n const inputLabel = label && props.isRequired ? `${label}*` : label\n\n const [error, setError] = useState('')\n\n const isError = !!error\n\n const handleError = useCallback(() => {\n if (!query && props.isRequired) {\n setError('Обязательно')\n return\n }\n\n if (query && onValid) {\n setError(onValid?.(query))\n }\n }, [onValid, props, query])\n\n useValidateClientForm(handleError)\n\n return (\n \n {label && (\n \n {label && !!isError ? label + ' . ' : inputLabel}\n\n {error}\n \n )}\n\n \n
\n {\n if (onChange) onChange?.(e.target.value)\n else setQuery(e.target.value)\n setError('')\n }}\n onFocus={() => {\n setFocus(true)\n }}\n onBlur={() => {\n setFocus(false)\n handleError()\n }}\n />\n
\n\n {rigthElement && (\n
{rigthElement}
\n )}\n\n {!!giftBall && !props.isDisabled && (\n \n )}\n\n {withChange && (\n \n Изменить\n
\n )}\n
\n\n {notice &&
{notice}
}\n \n )\n}\n","import React, { useEffect, useRef, useState } from 'react'\nimport Timer from './Timer'\nimport { Input } from '../../ui/Input/Input'\nimport styles from './SendCode.module.scss'\nimport { useApi } from '~/src/api'\nimport { SubmitPhoneResponse } from '~/src/types'\nimport { useStepsContext } from '~/src/stepsContext'\nimport classNames from 'classnames'\n\ntype SendCodeProps = {\n refetchCode: () => void\n}\n\nexport const SendCode: React.FC = ({ refetchCode }) => {\n const [smsCode, setSmsCode] = useState([])\n\n const inputOne = useRef(null)\n const inputTwo = useRef(null)\n const inputThree = useRef(null)\n\n const inputRefs = [inputOne, inputTwo, inputThree]\n\n const { userPhoneNumber, setCommonClientData, setCurrentStep } =\n useStepsContext()\n\n const handleInputChange = (index: number, value: string) => {\n resetError()\n if (value && !/^\\d+$/.test(value)) {\n return\n }\n\n const newSmsCode = [...smsCode]\n newSmsCode[index] = value\n setSmsCode(newSmsCode)\n\n if (value) {\n const currentInput = inputRefs[index]\n const nextInput = inputRefs[index + 1]\n\n if (nextInput) {\n nextInput.current?.focus()\n } else {\n currentInput.current?.blur()\n }\n }\n }\n\n const { handleFetch, resetError, error } = useApi({\n domain: 'submit-phone',\n\n handleSuccess: (data) => {\n setCommonClientData(data.client)\n setCurrentStep('form')\n },\n\n handleError: () => {\n setSmsCode([])\n inputOne.current?.focus()\n },\n })\n\n useEffect(() => {\n if (smsCode.length === 3) {\n handleFetch({\n code: smsCode.join(''),\n phoneNumber: userPhoneNumber,\n })\n }\n }, [smsCode])\n\n return (\n <>\n \n {error || 'SMS-код'}\n \n
\n {Array(3)\n .fill(null)\n .map((_, index) => (\n
\n handleInputChange(index, value)}\n inputRef={inputRefs[index]}\n errorMessage={error}\n inputClassName={styles.codeInput}\n />\n
\n ))}\n
\n\n {\n refetchCode()\n resetError()\n }}\n />\n \n )\n}\n","import classNames from 'classnames'\nimport { IconTelegram } from '../../icons/IconTelegram'\nimport { IconWhatsapp } from '../../icons/IconWhatsapp'\n\nimport styles from './AuthForm.module.scss'\n\ntype TryByMessengersProps = {\n verifyByTelegram: boolean\n verifyByWhatsapp: boolean\n}\n\nexport const TryByMessengers: React.FC = ({\n verifyByTelegram,\n verifyByWhatsapp,\n}) => {\n if (verifyByTelegram || verifyByWhatsapp)\n return (\n <>\n {verifyByTelegram && (\n \n Telegram\n \n )}\n\n {verifyByWhatsapp && (\n <>\n {verifyByTelegram && или}\n \n WhatsApp\n \n \n )}\n \n )\n}\n","import { useCallback, useEffect, useState } from 'react'\nimport classNames from 'classnames'\n\nimport styles from './InputMask.module.scss'\nimport ReactInputMask from 'react-input-mask'\nimport { type AriaTextFieldProps } from '@react-aria/textfield'\nimport { GiftBall } from '../GiftBall/GiftBall'\n\nimport { Label, TextField } from 'react-aria-components'\nimport { useValidateClientForm } from '../../ClientForm/useValidateClientForm'\nexport type InputProps = AriaTextFieldProps & {\n withChange?: boolean\n isValidNumber?: boolean\n placeholder?: string\n label?: string\n notice?: string\n giftBall?: number\n mask: string\n inputRef?: React.RefObject\n children?: React.ReactNode\n onChange?: (value: string) => void\n onValid?: (value: string) => string\n handleClickChange?: () => void\n}\n\nexport const InputMask: React.FC = ({\n withChange = false,\n mask,\n label,\n notice,\n handleClickChange,\n giftBall,\n onChange,\n onValid,\n isDisabled,\n ...props\n}) => {\n const [query, setQuery] = useState(props.value || props.defaultValue)\n\n const [focus, setFocus] = useState(false)\n const [error, setError] = useState('')\n\n const inputLabel = label && props.isRequired ? `${label}*` : label\n\n const placeholder = props.placeholder || ''\n\n const maskPlaceholder =\n query + placeholder.slice(((query as string) || '').length)\n\n useEffect(() => {\n if (!props.value && !props.defaultValue) setQuery('')\n }, [])\n\n const isError = !!error\n\n const handleError = useCallback(() => {\n if (!query && props.isRequired) {\n setError('Обязательно')\n return\n }\n\n if (query && onValid) {\n setError(onValid?.(query))\n }\n }, [onValid, props, query])\n\n useValidateClientForm(handleError)\n\n useEffect(() => {\n if (!props.isRequired) {\n setError('')\n }\n }, [props.isRequired])\n\n return (\n \n {label && (\n \n {label && !!isError ? label + '. ' : inputLabel}\n\n {error}\n \n )}\n\n \n
\n {!isDisabled && (\n \n )}\n\n {/* @ts-ignore */}\n {\n setQuery(e.target.value)\n onChange?.(e.target.value)\n setError('')\n }}\n onFocus={() => {\n setFocus(true)\n }}\n onBlur={() => {\n setFocus(false)\n handleError()\n }}\n />\n
\n\n {!!giftBall && !isDisabled && (\n \n )}\n\n {withChange && (\n {\n handleClickChange?.()\n setQuery('')\n }}\n >\n Изменить\n \n )}\n \n\n {notice &&
{notice}
}\n \n )\n}\n","import styles from './AuthForm.module.scss'\nimport classNames from 'classnames'\nimport { Button } from '../ui/Button/Button'\nimport React, { useCallback, useState } from 'react'\nimport { SendCode } from './SendCode/SendCode'\nimport { TryByMessengers } from './TryByMessengers'\nimport { useApi } from '../../api'\nimport { InvisibleSmartCaptcha } from '@yandex/smart-captcha'\n\nimport { Client } from '~/src/types'\nimport { useStepsContext } from '~/src/stepsContext'\nimport { InputMask } from '../ui/InputMask/InputMask'\nimport {\n GoogleReCaptchaProvider,\n useGoogleReCaptcha,\n} from 'react-google-recaptcha-v3'\n\nconst AuthFormComponent: React.FC = () => {\n const {\n brand,\n userPhoneNumber,\n agreedWithTerms,\n verifyByTelegram,\n verifyByWhatsapp,\n isChangePhoneNumber,\n setUserPhoneNumber,\n setIsChangePhoneNumber,\n } = useStepsContext()\n\n const [phoneNumber, setPhoneNumber] = useState(userPhoneNumber || '')\n const [resetCaptcha, setResetCaptcha] = useState(0)\n const [sendCodeView, setSendCodeView] = useState(\n !!userPhoneNumber && !isChangePhoneNumber,\n )\n\n const handleReset = () => setResetCaptcha((prev) => prev + 1)\n\n const { error, handleFetch, isLoading } = useApi<{ client: Client }>({\n only500: true,\n domain: 'send-code',\n handleSuccess: () => setSendCodeView(true),\n })\n\n const [visible, setVisible] = useState(false)\n\n const { executeRecaptcha } = useGoogleReCaptcha()\n\n const handleReCaptchaVerify = useCallback(async () => {\n if (!executeRecaptcha) {\n console.log('recaptcha is not working')\n return\n }\n\n const token = await executeRecaptcha('send_code')\n\n handleFetch(\n {\n agreedWithTerms,\n phoneNumber,\n },\n token,\n )\n }, [executeRecaptcha, phoneNumber])\n\n const handleSendCode = () => {\n setUserPhoneNumber(phoneNumber)\n setIsChangePhoneNumber(false)\n\n if (import.meta.env?.VITE_CAPTCHA_TOOL === 'google') {\n handleReCaptchaVerify()\n } else {\n handleReset()\n setVisible(true)\n }\n }\n\n const handleKeyUp = (event: React.KeyboardEvent) => {\n if (event.key === 'Enter') {\n handleSendCode()\n }\n }\n\n const handleClickChange = () => {\n setPhoneNumber('')\n setIsChangePhoneNumber(true)\n setSendCodeView(false)\n }\n\n const phoneMask =\n sendCodeView || !brand ? '' : brand?.phoneMask.replace(/[#]/g, '9')\n\n const placeholder = brand?.phoneMask.replace(/[#]/g, '0')\n\n const handleChallengeHidden = useCallback(() => setVisible(false), [])\n\n return (\n <>\n
\n
\n
\n
\n setPhoneNumber(event)}\n withChange={sendCodeView}\n handleClickChange={handleClickChange}\n onKeyUp={handleKeyUp}\n />\n
\n\n
\n {sendCodeView ? (\n <>\n \n \n ) : (\n handleSendCode()}\n />\n )}\n
\n
\n\n {error &&
{error}
}\n\n
\n \n {sendCodeView ? (\n <>\n Не приходит код?\n Попробуйте через\n \n ) : (\n Через\n )}\n
\n\n \n
\n
\n \n\n {import.meta.env?.VITE_CAPTCHA_TOOL === 'yandex' && (\n {\n handleFetch(\n {\n agreedWithTerms,\n phoneNumber,\n },\n token,\n )\n }}\n onChallengeHidden={handleChallengeHidden}\n visible={visible}\n />\n )}\n \n )\n}\n\nexport const AuthForm = () => {\n if (import.meta.env?.VITE_CAPTCHA_TOOL === 'google') {\n return (\n \n \n \n )\n }\n\n return \n}\n","export const IconArrowDropdown = () => (\n \n \n \n)\n","const emailPattern =\n /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/\n\nexport const validateEmail = (email: string) => {\n return emailPattern.test(email)\n}\n","import {\n Button,\n ComboBox,\n type ComboBoxProps,\n Input,\n Key,\n Label,\n ListBox,\n ListBoxItem,\n Popover,\n PopoverRenderProps,\n} from 'react-aria-components'\nimport { debounce } from 'lodash'\n\nimport styles from '../Input/Input.module.scss'\nimport stylesAutocomplete from './Autocomplete.module.scss'\nimport classNames from 'classnames'\nimport React, { useCallback, useEffect, useState } from 'react'\nimport { ListBoxItemType, SuggestionsResponse } from '../../../types'\nimport { IconArrowDropdown } from '../../../icons/IconArrowDropdown'\nimport { useValidateClientForm } from '../../ClientForm/useValidateClientForm'\nimport { GiftBall } from '../GiftBall/GiftBall'\nimport { isMobileDevice } from '~/src/utils/isMobileDevice'\nimport { validateEmail } from '~/src/utils/validateEmail'\n\ntype AutocompleteProps = Omit<\n ComboBoxProps,\n 'children' | 'items'\n> & {\n asSelect?: boolean\n giftBall?: number\n width?: number\n errorMessage?: string\n notice?: string\n label?: string\n customLabel?: string\n defaultValue?: string\n items?: string[] | null\n fetchOptions?: (query: string) => Promise\n onChange?: (value: string) => void\n}\n\nconst NOTHING_FOUND = 'Ничего не найдено'\nconst START_SEARCH = 'Начните ввод для поиска'\n\nexport function Autocomplete({\n asSelect,\n label,\n notice,\n customLabel,\n errorMessage,\n items,\n defaultValue,\n giftBall,\n fetchOptions,\n onInputChange,\n ...props\n}: AutocompleteProps) {\n const [query, setQuery] = useState(props.defaultInputValue || '')\n\n const [focus, setFocus] = useState(false)\n const [isOpen, setIsOpen] = useState(false)\n const [popoverPlacement, setPopoverPlacement] = useState<\n PopoverRenderProps['placement'] | undefined\n >(undefined)\n\n const selectItems = items?.map((item, index) => ({\n id: index + 1,\n value: item.trim(),\n }))\n\n const defaultSelectedKey = selectItems?.find(\n (item) => item.value === defaultValue,\n )\n\n const [options, setOptions] = useState([])\n\n const [selectedKey, setSelectedKey] = React.useState(\n defaultSelectedKey ? +defaultSelectedKey.id : null,\n )\n\n const onSelectionChange = (id: Key | null) => {\n setSelectedKey(id)\n setError('')\n }\n\n const [error, setError] = useState(errorMessage || '')\n\n const handleFetchOptions = useCallback(\n debounce((inputValue: string) => {\n fetchOptions?.(inputValue).then((data) => {\n setOptions(\n data.suggestions.map((item, index) => ({\n id: index + item.value,\n value: item.value,\n })),\n )\n })\n }, 300),\n [],\n )\n\n useEffect(() => {\n if (!props.isRequired && !errorMessage) {\n setError('')\n }\n\n if (errorMessage) {\n setError(errorMessage)\n }\n }, [props.isRequired, errorMessage])\n\n const isError = !!error\n const isSelect = !!selectItems?.length\n\n const handleError = useCallback(() => {\n if (props.name === 'email' && query) {\n setError(validateEmail(query) ? '' : 'Некорректный email')\n return\n }\n\n if (\n (isSelect ? !selectedKey && !props.defaultInputValue : !query) &&\n props.isRequired\n ) {\n setError('Обязательно')\n }\n }, [props, query, isSelect, selectedKey])\n\n useValidateClientForm(handleError)\n\n const inputLabel = label && props.isRequired ? `${label}*` : label\n\n const emptyPlaceholderHint = asSelect\n ? options.length\n ? START_SEARCH\n : NOTHING_FOUND\n : NOTHING_FOUND\n\n return (\n setIsOpen(open)}\n defaultItems={selectItems}\n allowsCustomValue={!isSelect}\n menuTrigger={isSelect || asSelect ? 'focus' : undefined}\n className={classNames(stylesAutocomplete.autocomplete, {\n [styles.disabled]: props.isDisabled,\n })}\n selectedKey={selectedKey}\n onSelectionChange={onSelectionChange}\n onInputChange={setQuery}\n items={options.length ? options : undefined}\n allowsEmptyCollection\n {...props}\n >\n {(label || customLabel) && (\n \n {label && !!isError ? label + '. ' : inputLabel}\n {customLabel && !!isError ? customLabel + '. ' : customLabel}\n\n {error}\n \n )}\n\n \n {\n onInputChange?.(e.target.value)\n setQuery(e.target.value)\n\n if (fetchOptions) {\n handleFetchOptions(e.target.value)\n }\n setError('')\n }}\n onFocus={() => {\n setFocus(true)\n }}\n onBlur={() => {\n if (isSelect) setQuery(props.defaultInputValue || '')\n setFocus(false)\n handleError()\n }}\n />\n\n {(isSelect || asSelect) && (\n \n \n \n )}\n\n {!!giftBall && !props.isDisabled && (\n \n \n \n )}\n \n\n {notice &&
{notice}
}\n\n {\n setPopoverPlacement(placement)\n\n return classNames(stylesAutocomplete.optionsPopover, {\n [stylesAutocomplete.error]: isError,\n })\n }}\n offset={-1}\n maxHeight={isMobileDevice() ? 150 : 200}\n >\n (\n
\n {emptyPlaceholderHint}\n
\n )}\n >\n {(item: ListBoxItemType) => (\n \n classNames(stylesAutocomplete.optionsItem, {\n [stylesAutocomplete.focused]: isFocused,\n })\n }\n >\n {item.value}\n \n )}\n \n \n \n )\n}\n","const ORIGIN = 'https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest'\nconst MOKK_API_KEY = '4975418d48482494fd143286b6302ef4a5a5bacd'\n\nconst makeRequest = (\n url: string,\n body: Record,\n apiKey: string,\n) =>\n fetch(`${ORIGIN}/${url}`, {\n method: 'POST',\n mode: 'cors',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n Authorization: `Token ${apiKey}`,\n },\n body: JSON.stringify(body),\n }).then((response) => response.json())\n\nexport const fetchSuggestions = (\n apiKey: string = MOKK_API_KEY,\n type: string,\n query: string,\n) => {\n switch (type) {\n case 'fullName': {\n return makeRequest('fio', { query }, apiKey)\n }\n\n case 'name': {\n return makeRequest('fio', { query, parts: ['NAME'] }, apiKey)\n }\n\n case 'surname': {\n return makeRequest('fio', { query, parts: ['SURNAME'] }, apiKey)\n }\n\n case 'patronymic': {\n return makeRequest('fio', { query, parts: ['PATRONYMIC'] }, apiKey)\n }\n\n case 'email': {\n return makeRequest('email', { query }, apiKey)\n }\n }\n\n return Promise.resolve({ suggestions: [] })\n}\n","import moment from 'moment'\n\nexport const validateDate = (date: string) => {\n if (!date || date.length !== 10) {\n return false\n }\n\n const mDate = moment(date, 'DD.MM.YYYY')\n\n return mDate.isValid()\n}\n","import { useState } from 'react'\n\nimport styles from './ChildrenInput.module.scss'\nimport { Autocomplete } from '../../ui/Autocomplete/Autocomplete'\nimport { fetchSuggestions } from '../../../dadata'\nimport { InputMask } from '../../ui/InputMask/InputMask'\nimport { validateDate } from '../../../utils/validateDate'\n\ntype ChildrenInputProps = {\n notice?: string\n isRequired?: boolean\n dadataApiKey?: string\n defaultValue?: Array<{ name: string; birthdate: string }>\n}\n\nexport const ChildrenInput: React.FC = ({\n isRequired,\n defaultValue,\n dadataApiKey,\n notice,\n}) => {\n const initialDates = defaultValue?.length\n ? [...defaultValue.map((child) => child.birthdate), '']\n : ['']\n\n const initialNames = defaultValue?.length\n ? [...defaultValue.map((child) => child.name), '']\n : ['']\n\n const [names, setNames] = useState(initialNames)\n const [dates, setDates] = useState(initialDates)\n\n const handleInputNameChange = (index: number, value: string) => {\n const newInputs = [...names]\n newInputs[index] = value\n\n if (value === '') {\n const emptyIndex = newInputs.findIndex(\n (input, i) => input === '' && i !== index,\n )\n if (emptyIndex !== -1) {\n newInputs.splice(emptyIndex, 1)\n }\n } else if (\n newInputs.filter((input) => input !== '').length === newInputs.length\n ) {\n newInputs.push('')\n }\n\n setNames(newInputs)\n }\n\n return (\n <>\n
\n {names.map((_, index) => (\n
\n index : false}\n defaultInputValue={names[index] || ''}\n name={`children-name${index}`}\n customLabel={\n index === 0 ? (isRequired ? 'Дети*' : 'Дети') : undefined\n }\n isRequired={(isRequired && names.length === 1) || !!dates[index]}\n fetchOptions={(query) =>\n fetchSuggestions(dadataApiKey, 'name', query)\n }\n onInputChange={(data) => {\n handleInputNameChange(index, data)\n }}\n />\n\n index : false}\n name={`children-birthdate${index}`}\n value={dates[index]}\n isRequired={index !== names.length - 1}\n mask='99.99.9999'\n placeholder='01.01.1900'\n onChange={(data) => {\n const newDates = [...dates]\n newDates[index] = data\n setDates(newDates)\n }}\n inputMode={'numeric'}\n onValid={(value) => (validateDate(value) ? '' : 'Неверная дата')}\n />\n
\n ))}\n
\n\n {notice &&
{notice}
}\n \n )\n}\n","import { useRef, useState } from 'react'\nimport {\n BarcodeDetectorPolyfill,\n DetectedBarcode,\n} from '@undecaf/barcode-detector-polyfill'\nimport { Button } from '../Button/Button'\nimport styles from './InputBarcode.module.scss'\n\nexport const useBarcodeScanner = ({\n onChange,\n}: {\n onChange: (value: string) => void\n}) => {\n const repeat = useRef(true)\n const detector = new BarcodeDetectorPolyfill()\n\n const videoRef = useRef(null)\n const canvas = document.createElement('canvas')\n\n const [requestId, setRequestId] = useState(0)\n const [isScanning, setIsScanning] = useState(false)\n\n // Обрабатываем кадры\n const detect = async (source: HTMLVideoElement) => {\n if (!detector) return\n\n const symbols: DetectedBarcode[] = await detector.detect(source)\n const ctx = canvas.getContext('2d')\n\n canvas.width = source.videoWidth || source.width\n canvas.height = source.videoHeight || source.height\n ctx?.clearRect(0, 0, canvas.width, canvas.height)\n\n symbols.forEach((symbol) => {\n const lastCornerPoint =\n symbol.cornerPoints[symbol.cornerPoints.length - 1]\n ctx?.moveTo(lastCornerPoint.x, lastCornerPoint.y)\n symbol.cornerPoints.forEach((point) => ctx?.lineTo(point.x, point.y))\n\n if (ctx) {\n ctx.lineWidth = 3\n ctx.strokeStyle = '#00e000ff'\n }\n\n ctx?.stroke()\n })\n\n if (symbols.length && symbols[0].rawValue) {\n onChange(symbols[0].rawValue)\n stopCameraScan()\n }\n }\n\n // Функция для обработки видео\n const detectVideo = () => {\n if (repeat.current) {\n const ctx = canvas.getContext('2d')\n ctx?.clearRect(0, 0, canvas.width, canvas.height)\n setRequestId(0)\n cancelAnimationFrame(requestId)\n }\n\n if (repeat.current && videoRef.current) {\n // на каждый кадр запускаем сканирование\n detect(videoRef.current).then(() => {\n const id = requestAnimationFrame(() => detectVideo())\n setRequestId(id)\n })\n } else {\n cancelAnimationFrame(requestId)\n setRequestId(0)\n }\n }\n\n const startCameraScan = async () => {\n if (!requestId) {\n setIsScanning(true)\n document.getElementsByTagName('footer')[0].style.display = 'none'\n repeat.current = true\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: false,\n video: { facingMode: 'environment' },\n })\n if (videoRef.current) videoRef.current.srcObject = stream\n detectVideo()\n } catch (error) {\n console.error('Start camera scan error:', error)\n }\n } else {\n detectVideo()\n }\n }\n\n function stopCameraScan() {\n if (videoRef.current && videoRef.current.srcObject) {\n // @ts-ignore\n videoRef.current.srcObject.getTracks().forEach((track) => track.stop())\n videoRef.current.srcObject = null\n }\n repeat.current = false\n cancelAnimationFrame(requestId)\n setRequestId(0)\n setIsScanning(false)\n document.getElementsByTagName('footer')[0].style.display = 'block'\n }\n\n const layout = (\n \n \n\n {isScanning && (\n <>\n
\n Наведите камеру на штрих-код карты\n
\n\n \n \n )}\n \n )\n\n const toggleScanner = () =>\n isScanning ? stopCameraScan() : startCameraScan()\n\n return {\n layout,\n toggleScanner,\n }\n}\n","export const Barcode = ({ onClick }: { onClick?: () => void }) => {\n return (\n \n \n \n \n )\n}\n","import { useState } from 'react'\nimport { Input, InputProps } from '../Input/Input'\n\nimport { useBarcodeScanner } from './useBarcodeScanner'\nimport { isMobileDevice } from '~/src/utils/isMobileDevice'\nimport { Barcode as BarcodeIcon } from '../../../icons/Barcode.tsx'\n\ntype InputBarcodeProps = InputProps\n\nexport const InputBarcode: React.FC = ({\n name,\n label,\n inputMode,\n type,\n pattern,\n defaultValue,\n giftBall,\n isRequired,\n isDisabled,\n notice,\n}) => {\n const [value, setValue] = useState('')\n\n const onChange = (value: string) => {\n setValue(value)\n }\n\n const { layout, toggleScanner } = useBarcodeScanner({ onChange })\n\n if (\n !isMobileDevice() ||\n !navigator.mediaDevices ||\n !navigator.mediaDevices.getUserMedia ||\n !window.WebAssembly\n )\n return (\n \n )\n\n return (\n <>\n \n }\n />\n\n {layout}\n \n )\n}\n","import React, { useMemo } from 'react'\nimport styles from './ClientForm.module.scss'\nimport { Autocomplete } from '../ui/Autocomplete/Autocomplete'\nimport { fetchSuggestions } from '../../dadata'\nimport { Children, Field } from '../../types'\nimport { Checkbox } from '../ui/Checkbox/Checkbox'\nimport { GiftBall } from '../ui/GiftBall/GiftBall'\nimport { ChildrenInput } from './ChildrenInput/ChildrenInput'\nimport { Input } from '../ui/Input/Input'\nimport { InputMask } from '../ui/InputMask/InputMask'\nimport { validateDate } from '../../utils/validateDate'\nimport { InputBarcode } from '../ui/InputBarcode/InputBarcode'\n\ntype FieldComponentProps = {\n field: Field\n defaultValues?: Record\n defaultChildren?: Children[]\n errorMessage?: string\n dadataApiKey?: string\n}\n\nconst GENDERS = ['Мужской', 'Женский']\n\nconst SELECT_FIELDS = ['gender', 'static-set', 'level']\nconst DADATA_AUTOCOMPLETE_FIELDS = [\n 'fullName',\n 'name',\n 'surname',\n 'patronymic',\n 'email',\n]\n\nexport const FieldComponent: React.FC = ({\n field,\n defaultValues,\n defaultChildren,\n errorMessage,\n dadataApiKey,\n}) => {\n const isLargeList = !!(field.options?.length && field.options?.length >= 300)\n\n const largeListAsAutocompleteOptions = useMemo(\n () =>\n field.options?.map((title) => ({\n value: title,\n })),\n [field],\n )\n\n if (field.type === 'children') {\n return (\n <>\n \n\n {!!field.bonus?.amount && (\n \n )}\n \n )\n }\n\n if (SELECT_FIELDS.includes(field.type) && !isLargeList) {\n const defaultValue =\n field.type === 'gender'\n ? defaultValues?.[field.code]\n ? GENDERS[+defaultValues?.[field.code] - 1]\n : GENDERS[+field.defaultValue]\n : defaultValues?.[field.code] || field.defaultValue\n\n return (\n \n )\n }\n\n if (DADATA_AUTOCOMPLETE_FIELDS.includes(field.code) || isLargeList) {\n const requestSimulation = (query: string) => {\n return new Promise((resolve) => {\n if (!query) {\n resolve({ suggestions: [] })\n }\n\n const lowerCaseQuery = query.toLowerCase()\n resolve({\n suggestions: largeListAsAutocompleteOptions?.filter(({ value }) =>\n value.toLowerCase().includes(lowerCaseQuery),\n ),\n })\n })\n }\n return (\n \n isLargeList\n ? requestSimulation(query)\n : fetchSuggestions(dadataApiKey, field.code, query)\n }\n name={field.code}\n label={field.title}\n asSelect={isLargeList}\n giftBall={field.bonus?.amount}\n notice={field.notice}\n />\n )\n }\n\n if (field.type === 'checkbox') {\n const checked =\n defaultValues?.[field.code] === 'Yes' || field.defaultValue === 'Yes'\n\n return (\n \n
{field.title}
\n \n )\n }\n\n if (field.type === 'subscribed') {\n const checked = !!defaultValues?.[field.code]\n\n return (\n \n
{field.title}
\n \n )\n }\n\n if (field.code === 'birthdate') {\n return (\n (validateDate(value) ? '' : 'Неверная дата')}\n notice={field.notice}\n />\n )\n }\n\n if (field.code === 'cardText') {\n return (\n \n )\n }\n\n return (\n \n )\n}\n","import React, { useEffect } from 'react'\nimport styles from './ClientForm.module.scss'\nimport { Form } from 'react-aria-components'\nimport { FieldComponent } from './FieldComponent'\nimport { Button } from '../ui/Button/Button'\nimport { isMobileDevice } from '~/src/utils/isMobileDevice'\nimport { useApi } from '~/src/api'\nimport { Error, SubmitClientResponse } from '~/src/types'\nimport { useStepsContext } from '~/src/stepsContext'\nimport { errorMap } from '~/src/utils/error'\nimport { InputMask } from '../ui/InputMask/InputMask'\n\nconst scrollToInvalidField = () => {\n const invalidInputs = document.querySelectorAll('input[aria-invalid=\"true\"]')\n\n if (invalidInputs.length) {\n invalidInputs[0]?.scrollIntoView?.({\n block: 'center',\n })\n }\n\n return !!invalidInputs.length\n}\n\nexport const ClientForm: React.FC = () => {\n const {\n brand,\n commonClientData,\n fields,\n userPhoneNumber,\n setUserPhoneNumber,\n setWalletsLink,\n setCurrentStep,\n setIsChangePhoneNumber,\n setCommonClientData,\n } = useStepsContext()\n\n const defaultValues = commonClientData?.fields\n const defaultChildren = commonClientData?.children\n const dadataApiKey = brand?.dadataApiKey\n\n const [validationErrors, setValidationErrors] = React.useState<\n Error['validationErrors']\n >([])\n\n const validationErrorsMap = validationErrors.reduce(\n (acc, item) => {\n acc[item.code] = item.errorCode\n\n return acc\n },\n {} as Record,\n )\n\n const { handleFetch, isLoading, error } = useApi({\n domain: 'submit-client',\n handleSuccess: ({ client }) => {\n if (client) {\n setCommonClientData(client)\n setWalletsLink(client.walletsLink)\n }\n\n if (!client.walletsLink) {\n setCurrentStep('thankyou')\n\n return\n }\n\n if (isMobileDevice()) {\n setCurrentStep('thankyou')\n\n setTimeout(() => {\n document.location.href = client.walletsLink as string\n }, 1000)\n } else {\n setCurrentStep('qr')\n }\n },\n\n handleError: (error) => {\n if (error?.validationErrors) {\n setValidationErrors(error?.validationErrors)\n setTimeout(() => {\n scrollToInvalidField()\n }, 100)\n }\n },\n })\n\n const handleSubmit = (event: React.FormEvent) => {\n event.preventDefault()\n\n const hasScroll = scrollToInvalidField()\n\n if (hasScroll) return\n\n const formData = Object.fromEntries(\n new FormData(event.currentTarget),\n ) as Record\n\n const children = []\n\n for (const key in formData) {\n if (formData[key] === 'true') {\n formData[key] = true\n }\n\n if (formData[key] === 'false') {\n formData[key] = true\n }\n\n if (key.startsWith('children-name')) {\n const index = key.replace('children-name', '')\n const birthdateKey = `children-birthdate${index}`\n if (formData[birthdateKey]) {\n children.push({\n name: formData[key],\n birthdate: formData[birthdateKey],\n })\n }\n\n delete formData[`children-name${index}`]\n delete formData[birthdateKey]\n }\n\n if (formData[key] === '') delete formData[key]\n }\n\n const payload = {\n ...defaultValues,\n ...formData,\n children: defaultChildren ? [...defaultChildren, ...children] : children,\n }\n\n handleFetch({ form: payload })\n }\n\n const handleClickChange = () => {\n setIsChangePhoneNumber(true)\n setCurrentStep('otp')\n setUserPhoneNumber('')\n }\n\n useEffect(() => {\n const inputs = document.querySelectorAll(\n 'input:not([disabled])',\n )\n\n if (inputs[0]) inputs[0].focus()\n }, [])\n\n return (\n \n
\n
\n \n
\n\n {fields?.map((field) => (\n
\n \n
\n ))}\n
\n\n