@@ -19,10 +19,8 @@ export const CheckboxFilter = ({ name, value, filters, item_key, description, se const [check, setCheck] = React.useState(value || false) React.useEffect(() => { - if (filters[item_key] !== undefined && filters[item_key] !== check) { - setCheck(filters[item_key]) - } - }, [filters]) + setCheck(filters[item_key] ?? value) + }, [filters, item_key, value]) return ( @@ -20,10 +20,8 @@ export const SelectFilter = ({ selects, filters, item_key, name, description, se const [value, setValue] = React.useState(selects.default) React.useEffect(() => { - if (filters[item_key] !== undefined && filters[item_key] !== value) { - setValue(filters[item_key]) - } - }, [filters]) + setValue(filters[item_key] ?? selects.default) + }, [filters, item_key, selects.default]) const onChange = (e: SelectChangeEvent) => { setNewParam({ [item_key]: e.target.value }) @@ -38,14 +36,13 @@ export const SelectFilter = ({ selects, filters, item_key, name, description, se IconComponent={KeyboardArrowDownIcon} value={value} onChange={onChange} - inputProps={{ - MenuProps: { - MenuListProps: { - sx: { - color: '#A6A5A5', - backgroundColor: '#151518', - }, - }, + MenuProps={{ + transitionDuration: 0, + PaperProps: { + sx: { backgroundColor: '#151518', borderRadius: '8px', marginTop: '8px' }, + }, + MenuListProps: { + sx: { backgroundColor: '#151518', color: '#A6A5A5' }, }, }} style={{ @@ -27,10 +27,8 @@ export const Slide = ({ title, values, filters, item_key, description, setNewPar } React.useEffect(() => { - if (filters[item_key] !== undefined && filters[item_key] !== range) { - setRange(filters[item_key]) - } - }, [filters]) + setRange(filters[item_key] ?? values.default) + }, [filters, item_key, values.default]) return ( <> @@ -28,14 +28,13 @@ export const ChatSelect: React.FC = ({ value, setValue, list, setDefaul IconComponent={KeyboardArrowDownIcon} value={value} onChange={onChange} - inputProps={{ - MenuProps: { - MenuListProps: { - sx: { - color: '#A6A5A5', - backgroundColor: '#151518', - }, - }, + MenuProps={{ + transitionDuration: 0, + PaperProps: { + sx: { backgroundColor: '#151518', borderRadius: '8px', marginTop: '8px' }, + }, + MenuListProps: { + sx: { backgroundColor: '#151518', color: '#A6A5A5' }, }, }} style={{ @@ -449,17 +449,6 @@ button { font: inherit; color: inherit; background-color: transparent; - text-align: left; - cursor: pointer; -} - -button { - padding: 0; - border: none; - font: inherit; - color: inherit; - background-color: transparent; - text-align: left; cursor: pointer; } @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { Box, TextField, Typography } from '@mui/material' import axios from 'axios' -import { Dayjs } from 'dayjs' +import dayjs, { Dayjs } from 'dayjs' import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' @@ -38,6 +38,11 @@ export const ApiKeyModal = ({ const keyName = `Ключ ${Math.floor(Math.random() * 100000) + 1}` + if (endDate && dayjs(endDate).isBefore(dayjs(), 'day')) { + showMessage('Указана неверная дата') + return + } + setIsLoading(true) const response = endDate && endDate !== '' @@ -0,0 +1,55 @@ +import { IModelParams } from '#/shared/api/models/models' + +export type ModelParamValues = Record + +export function getAllowedParamKeys(params: IModelParams[], currentVersion: string): Set { + return new Set( + params + .filter((el) => { + if (el.versions.length === 0) { + return true + } + + if (!currentVersion) { + return true + } + + return el.versions.includes(currentVersion) + }) + .map((el) => el.key) + ) +} + +export function pruneModelParams( + includeParams: ModelParamValues, + params: IModelParams[], + currentVersion: string +): ModelParamValues | null { + const allowedKeys = getAllowedParamKeys(params, currentVersion) + + if (allowedKeys.size === 0) { + return Object.keys(includeParams).length !== 0 ? {} : null + } + + const filteredEntries = Object.entries(includeParams).filter(([key]) => allowedKeys.has(key)) + + if (filteredEntries.length === Object.keys(includeParams).length) { + return null + } + + return filteredEntries.length ? Object.fromEntries(filteredEntries) : {} +} + +export function buildDefaultParamsForVersion(parameters: IModelParams[], version: string): ModelParamValues { + if (!version) { + return parameters.reduce((acc, param) => ({ ...acc, [param.key]: param.values.default }), {}) + } + + return parameters.reduce((acc, param) => { + if (param.versions.length === 0 || param.versions.includes(version)) { + return { ...acc, [param.key]: param.values.default } + } + + return acc + }, {} as ModelParamValues) +} @@ -0,0 +1,27 @@ +import React from 'react' + +import { replaceParams } from '#/app/store/model-parametres-store' +import { useAppDispatch, useAppSelector } from '#/app/store/store' +import { IModelParams } from '#/shared/api/models/models' + +import { pruneModelParams } from '../lib/prune-model-params' + +export function usePruneModelParams(params: IModelParams[] | undefined, currentVersion: string) { + const includeParams = useAppSelector((state) => state.params.params) as Record< + string, + string | number | number[] | boolean + > + const dispatch = useAppDispatch() + + React.useEffect(() => { + if (!params?.length) { + return + } + + const nextPayload = pruneModelParams(includeParams, params, currentVersion) + + if (nextPayload !== null) { + dispatch(replaceParams(nextPayload)) + } + }, [currentVersion, dispatch, includeParams, params]) +} @@ -1,126 +1,94 @@ -import React from 'react' -import { Stack } from '@mui/material' - -import { CheckboxFilter } from '#/app/components/filters/checkbox_filter' -import { InputFilter } from '#/app/components/filters/input_filter' -import { SelectFilter } from '#/app/components/filters/select_filter' -import { Slide } from '#/app/components/filters/slide_filter' -import { replaceParams, setParams } from '#/app/store/model-parametres-store' -import { useAppDispatch, useAppSelector } from '#/app/store/store' -import { IModelParams } from '#/shared/api/models/models' - -interface IProps { - params: IModelParams[] - currentVersion: string -} -export default function BotParamsMap({ params, currentVersion }: IProps) { - const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useAppDispatch() - - const setNewParam = React.useCallback( - (payload: { [key: string]: string | number | number[] | boolean }) => { - dispatch(setParams(payload)) - }, - [dispatch] - ) - - React.useEffect(() => { - if (!params) { - return - } - - const allowedKeys = new Set( - params - .filter((el) => { - if (el.versions.length === 0) { - return true - } - - // Согласовано с chat-bot filteredParams: без выбранной версии считаем все параметры допустимыми - if (!currentVersion) { - return true - } - - return el.versions.includes(currentVersion) - }) - .map((el) => el.key) - ) - - if (allowedKeys.size === 0) { - if (Object.keys(includeParams).length !== 0) { - dispatch(replaceParams({})) - } - return - } - - const filteredEntries = Object.entries(includeParams).filter(([key]) => allowedKeys.has(key)) - - if (filteredEntries.length !== Object.keys(includeParams).length) { - const nextPayload = filteredEntries.length ? Object.fromEntries(filteredEntries) : {} - dispatch(replaceParams(nextPayload)) - } - }, [currentVersion, dispatch, includeParams, params, setNewParam]) - - return ( - - {params.map((item, idx) => { - if (item.versions.length === 0 || item.versions.includes(currentVersion)) { - if (item.type == 'floatrange' || item.type == 'intrange') { - return ( - - ) - } - if (item.type == 'bool') { - return ( - - ) - } - - if (item.type == 'list') { - return ( - - ) - } - - if (item.type == 'int' || item.type == 'str') { - return ( - - ) - } - } - })} - - ) -} +import React from 'react' +import { Stack } from '@mui/material' + +import { CheckboxFilter } from '#/app/components/filters/checkbox_filter' +import { InputFilter } from '#/app/components/filters/input_filter' +import { SelectFilter } from '#/app/components/filters/select_filter' +import { Slide } from '#/app/components/filters/slide_filter' +import { setParams } from '#/app/store/model-parametres-store' +import { useAppDispatch, useAppSelector } from '#/app/store/store' +import { IModelParams } from '#/shared/api/models/models' + +import { usePruneModelParams } from './model/use-prune-model-params' + +interface IProps { + params: IModelParams[] + currentVersion: string +} +export default function BotParamsMap({ params, currentVersion }: IProps) { + const includeParams = useAppSelector((state) => state.params.params) + const dispatch = useAppDispatch() + + usePruneModelParams(params, currentVersion) + + const setNewParam = React.useCallback( + (payload: { [key: string]: string | number | number[] | boolean }) => { + dispatch(setParams(payload)) + }, + [dispatch] + ) + + return ( + + {params.map((item, idx) => { + if (item.versions.length === 0 || item.versions.includes(currentVersion)) { + if (item.type == 'floatrange' || item.type == 'intrange') { + return ( + + ) + } + if (item.type == 'bool') { + return ( + + ) + } + + if (item.type == 'list') { + return ( + + ) + } + + if (item.type == 'int' || item.type == 'str') { + return ( + + ) + } + } + })} + + ) +} + \ No newline at end of file @@ -4,8 +4,7 @@ interface SendReport { errorMessage: string email: string file?: File - company?:string -} +} export async function sendReport({errorMessage, email, file}: SendReport) { let formData = new FormData() @@ -16,15 +15,3 @@ export async function sendReport({errorMessage, email, file}: SendReport) { } return await api.post('/reports/', formData) } - - -export async function sendBusinessReport({errorMessage, email, company, file}: SendReport) { - let formData = new FormData() - - formData.append('report_text', `${email}: ${errorMessage}`) - formData.append('company_name', `${company}`) - if (file) { - formData.append('images', file) - } - return await api.post('/reports/business-support/', formData) -} @@ -5,11 +5,11 @@ import { useAppSelector } from '#/app/store/store' import { makePrivateRequest } from '#/shared/api' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' -import { sendBusinessReport, sendReport } from '../api' +import { sendReport } from '../api' const MAX_FILE_SIZE = 4.5 * 1024 * 1024 // 4.5 MB -export const useErrorReport = (reportType:'business' | 'default' = 'default') => { +export const useErrorReport = () => { const email = useAppSelector((state) => state.user.email) const [file, setFile] = React.useState() @@ -34,7 +34,7 @@ export const useErrorReport = (reportType:'business' | 'default' = 'default') => } } - const sendError = makePrivateRequest(async (company) => { + const sendError = makePrivateRequest(async () => { if (errorMessage.trim() === '') { showMessage('Поле не должно быть пустым.') setError(true) @@ -42,10 +42,7 @@ export const useErrorReport = (reportType:'business' | 'default' = 'default') => return } - const response = await (reportType === 'business' - ? sendBusinessReport({ errorMessage, email, company, file }) - : sendReport({ errorMessage, email, file }) - ) + const response = await sendReport({ errorMessage, email, file }) if (response.status === 201) { setErrorMessage('') @@ -1,120 +0,0 @@ -import * as React from 'react' -import { useForm } from 'react-hook-form' -import { Typography } from '@mui/material' - -import { useAppSelector } from '#/app/store/store' -import AttachSvg from '#/assets/svg/attach.svg?react' -import PlusSvg from '#/assets/svg/plus.svg?react' -import { BUSINESS_ERROR_REPORT, getModalById, PlateTemplate } from '#/features/modals' -import { CompanyAutocomplete } from '#/features/register-business/api/company-autocomplete' -import { companyNameOptions } from '#/features/register-business/lib/constatnts-step-legal-informative' -import { c } from '#/shared' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' -import { CommonButton } from '#/shared/ui/button' -import { CommonTextArea } from '#/shared/ui/common-textarea' - -import { useErrorReport } from '../model/use-error-report' - -import styles from './error-report.module.scss' - -export const BusinessErrorReportPlate = () => { - const modal = getModalById(BUSINESS_ERROR_REPORT) - const { error, isSend, setErrorMessage, handleFileChange, file, setFile, sendError } = useErrorReport('business') - const { showMessage } = useShowDataStore() - - const { companyName } = useAppSelector((state) => state.stepper.dataForCreate) - - const methods = useForm({ - defaultValues: { - companyName - }, - }) - - const handleSubmit = () => { - const formValues = methods.getValues() - const company = formValues.companyName?.trim() - - if (!company) { - showMessage('Пожалуйста, укажите наименование организации') - return - } - - sendError(company) - } - - return ( - - {isSend ? ( -
-

Спасибо!

-

Ваше сообщение направлено нашим специалистам. Ответ придет на почту, указанную при регистрации

-
- ) : ( -
e.stopPropagation()}> -

Запрос для компаний и партнеров

- - Опишите ваш сценарий — подберем решение и ответим за день. Работаем для корпораций, агентств и интеграторов - - - - - setErrorMessage(evt.target.value)} - buttomSlot={ - <> - - - } - /> - {file && ( -
-
- -
- {file.name} - {(file.size / 1024).toFixed(1)} KB -
-
- - -
- )} - -
- modal.setState(false)}> - Отмена - - - Отправить - -
-
- )} -
- ) -} \ No newline at end of file @@ -1,7 +1,6 @@ export const PLATE_CHANGE_PASSWORD = 'plate-change-password' export const RESEND_INVATION_PASSWORD = 'resend-invation-password' export const ERROR_REPORT = 'error-report' -export const BUSINESS_ERROR_REPORT = 'business-error-report' export const LOW_BALANCE_OFFER = 'low-balance-offer' export const SUBSCRIPTION_CHANGE_NOTIFICATION = 'subscription-change-notification' export const PLATE_ADD_VOICE = 'plate-add-voice' @@ -64,6 +64,23 @@ export const authOptions: NextAuthOptions = { return (await data) as User }, }), + CredentialsProvider({ + id: 'tokens', + type: 'credentials', + credentials: {}, + async authorize(credentials) { + const { access, refresh } = credentials as { access: string; refresh: string } + + if (!access || !refresh) { + throw new Error('invalid_credentials') + } + + return { + id: 'tokens', + token: { access, refresh }, + } as User + }, + }), ], callbacks: { async jwt({ token, user, account, trigger }) { @@ -1,6 +0,0 @@ -import BusinessReportsMobile from '#/views/reports-mobile/ui/business-error-reports-mobile' -import { getDefaultLayout } from '#/widgets/layouts' - -BusinessReportsMobile.getLayout = getDefaultLayout({ titlePage: 'Компаниям', device: 'mobile' }) - -export default BusinessReportsMobile @@ -1,3 +1,26 @@ +export const getApiDetail = (error: unknown): string => { + const data = (error as { response?: { data?: { detail?: unknown; message?: string } } })?.response?.data + if (!data) return '' + + if (typeof data.message === 'string') return data.message + + const detail = data.detail + if (typeof detail === 'string') return detail + if (Array.isArray(detail)) { + return detail + .map((item) => + typeof item === 'string' + ? item + : typeof item === 'object' && item !== null && 'msg' in item + ? String((item as { msg: unknown }).msg) + : String(item) + ) + .join(', ') + } + + return '' +} + export const decodeError = (err: any, defaultErr: string) => { if (err.response?.data.detail?.includes('Token balance')) { return 'Не хватает ' + err.response.data.detail.split(' ').filter(Boolean).at(-2) + ' токена' @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers' +import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import dayjs, { Dayjs } from 'dayjs' @@ -28,7 +28,8 @@ export const DateInput = ({ return ( - - + value === '' || /^[1-9]\d*$/.test(value) + export type KeyItemBaseProps = { keyValue: string name: string @@ -32,6 +35,7 @@ export const KeyItem: React.FC = ({ children, limit: initialLimit, const [isCopy, setIsCopy] = useState(false) const [limit, setLimit] = useState(initialLimit) const { data: session } = useSession() + const { showMessage } = useShowDataStore() useEffect(() => { setLimit(initialLimit) @@ -72,6 +76,10 @@ export const KeyItem: React.FC = ({ children, limit: initialLimit, }, [initialLimit, limit, name, session?.access]) const handleLimitChange = (value: string) => { + if (!isValidLimit(value)) { + showMessage('Укажите целое число больше нуля') + return + } setLimit(value) } @@ -1,5 +1,4 @@ import React, { useCallback, useMemo } from 'react' -import { useDispatch } from 'react-redux' import { Box, Collapse, Stack, Typography } from '@mui/material' import Head from 'next/head' import { useRouter } from 'next/router' @@ -7,17 +6,19 @@ import { useSession } from 'next-auth/react' import { ChatSelect } from '#/app/components/chat_select' import { ResetFilters } from '#/app/components/filters/reset_filters' -import { setParams as setParametres } from '#/app/store/model-parametres-store' -import { useAppSelector } from '#/app/store/store' +import { replaceParams } from '#/app/store/model-parametres-store' +import { useAppDispatch, useAppSelector } from '#/app/store/store' import { IModel } from '#/entities/model-entity' import BotParamsMap from '#/features/bot-params/bot-params-map' +import { buildDefaultParamsForVersion } from '#/features/bot-params/lib/prune-model-params' +import { usePruneModelParams } from '#/features/bot-params/model/use-prune-model-params' import { selectCurrentChat } from '#/features/chats/chats-slice' +import { usePredictPrice } from '#/features/predict-price/model/use-predict-price' import Title from '#/features/title/title' import { TutorialContext } from '#/features/tutorial-context/tutorial-context' import { NextPageWithLayout } from '#/pages/_app' import { DrawerCustom, useModel } from '#/shared' import model_api from '#/shared/api/models/api' -import { usePredictPrice } from '#/features/predict-price/model/use-predict-price' import { getDeviceType, getOs } from '#/shared/lib/helpers' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { SvgIcon } from '#/shared/ui/svg' @@ -49,80 +50,71 @@ const Page: NextPageWithLayout = () => { const desktop = deviceType === 'desktop' const { data } = useSession() const router = useRouter() + const { push } = router + const slug = router.query.slug as string | undefined const currentChat = useAppSelector(selectCurrentChat) const { messages, sendMessage, loading, getMessagesPagination, deleteMessage } = useModel(currentChat, showMessage, modelType) const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useDispatch() + const dispatch = useAppDispatch() + + usePruneModelParams(botParams?.parameters, version) const deleteMessageMemo = useCallback(deleteMessage, [currentChat, messages]) - const { push } = useRouter() + React.useEffect(() => { + if (!slug) { + return + } + + dispatch(replaceParams({})) + }, [dispatch, slug]) React.useEffect(() => { - if (data?.access) { - model_api.getBotParams(router.asPath.split('/')[2], data.access).then((res) => { - if (!res.title) return push('/404') - setBotParams(res) - setModelType(res.slug) - if (res.versions.length !== 0) { - setVersion(res.versions[0].slug) - dispatch( - setParametres( - res.parameters.reduce( - (a, v) => (v.versions.includes(res.versions[0].slug) ? { ...a, [v.key]: v.values.default } : { ...a }), - {} - ) - ) - ) - } else { - setVersion('') - dispatch(setParametres(res.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) - } - }) + if (!slug || !data?.access) { + return } - }, [data?.access, dispatch, push, router.asPath, router.query]) - const resetParams = () => { - if (botParams) { - dispatch(setParametres({})) - if (botParams.versions?.length !== 0) { - setVersion(botParams.versions[0].slug) - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => (v.versions.includes(botParams.versions[0].slug) ? { ...a, [v.key]: v.values.default } : { ...a }), - {} - ) - ) - ) + model_api.getBotParams(slug, data.access).then((res) => { + if (!res.title) return push('/404') + setBotParams(res) + setModelType(res.slug) + + if (res.versions.length !== 0) { + const initialVersion = res.versions[0].slug + setVersion(initialVersion) + dispatch(replaceParams(buildDefaultParamsForVersion(res.parameters, initialVersion))) } else { - setVersion(botParams.slug) - dispatch(setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) + setVersion('') + dispatch(replaceParams(buildDefaultParamsForVersion(res.parameters, ''))) } + }) + }, [data?.access, dispatch, push, slug]) + + const resetParams = () => { + if (!botParams) { + return + } + + if (botParams.versions?.length !== 0) { + const initialVersion = botParams.versions[0].slug + setVersion(initialVersion) + dispatch(replaceParams(buildDefaultParamsForVersion(botParams.parameters, initialVersion))) + } else { + setVersion(botParams.slug) + dispatch(replaceParams(buildDefaultParamsForVersion(botParams.parameters, ''))) } } const setDefaultParams = (newVersion?: string) => { - if (botParams) { - const targetVersion = newVersion !== undefined ? newVersion : version - - dispatch(setParametres({})) - if (targetVersion !== '') { - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => (v.versions.includes(targetVersion) ? { ...a, [v.key]: v.values.default } : { ...a }), - {} - ) - ) - ) - } else { - dispatch(setParametres(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) - } + if (!botParams) { + return } + + const targetVersion = newVersion !== undefined ? newVersion : version + dispatch(replaceParams(buildDefaultParamsForVersion(botParams.parameters, targetVersion))) } const viewMobileSettings = () => { @@ -133,32 +125,6 @@ const Page: NextPageWithLayout = () => { setOpenFiltersMobile(false) } - const onSendMessage = (input: string, required: (string | null)[]) => { - if (required.includes('text') && (input === '' || input === null)) { - showMessage('Введите сообщение!') - return false - } - if (required.includes('image') && file === null) { - showMessage('Прикрепите изображение!') - return false - } - - let data = { ...(includeParams || {}) } - if (version !== '') data = { ...data, ...{ version: version } } - - sendMessage({ - content: input, - file, - info: { - ...data, - }, - }) - return true - } - // theme removed: always dark - - // Подготавливаем данные для API вкладки - // Фильтруем параметры - оставляем только актуальные для текущей версии const filteredParams = useMemo(() => { if (!botParams?.parameters) { return {} @@ -187,6 +153,29 @@ const Page: NextPageWithLayout = () => { ) }, [botParams?.parameters, version, includeParams]) + const onSendMessage = useCallback( + (input: string, required: (string | null)[]) => { + if (required.includes('text') && (input === '' || input === null)) { + showMessage('Введите сообщение!') + return false + } + if (required.includes('image') && file === null) { + showMessage('Прикрепите изображение!') + return false + } + + const info = version !== '' ? { ...filteredParams, version } : { ...filteredParams } + + sendMessage({ + content: input, + file, + info, + }) + return true + }, + [file, filteredParams, sendMessage, showMessage, version] + ) + const showFileExample = useMemo(() => { if (!botParams?.inputs) return false @@ -206,7 +195,10 @@ const Page: NextPageWithLayout = () => { }) }, [botParams?.inputs, version]) - const predictPriceInfo = useMemo(() => ({ ...(includeParams || {}), ...(version ? { version } : {}) }), [includeParams, version]) + const predictPriceInfo = useMemo( + () => (version ? { ...filteredParams, version } : { ...filteredParams }), + [filteredParams, version] + ) const predictedPrice = usePredictPrice({ modelSlug: modelType, @@ -3,33 +3,66 @@ import { Box, Typography } from '@mui/material' import axios from 'axios' import Link from 'next/link' import { useRouter } from 'next/router' +import { signIn } from 'next-auth/react' import { getApiUrl } from '#/shared/lib/constants' +import { getApiDetail } from '#/shared/lib/helpers/decode-error' import { NextPageWithLayout } from '#/pages/_app' -import { signIn } from 'next-auth/react' + +const CONFIRM_FAILED_MESSAGE = + 'Не удалось подтвердить почту. Попробуйте позже или войдите вручную.' const Confirm: NextPageWithLayout = () => { const { push } = useRouter() const [success, setSuccess] = useState(null) + const [errorMessage, setErrorMessage] = useState(null) + + const showConfirmError = (message?: string | null) => { + setSuccess(false) + setErrorMessage(message?.trim() || CONFIRM_FAILED_MESSAGE) + } useEffect(() => { - let formData: any = new FormData() - const tokenData = window.location.search.slice(1).split('=')[1] - formData.append("token", tokenData) + const params = new URLSearchParams(window.location.search) + const tokenData = params.get('token') ?? params.get('email_token') + + if (!tokenData) { + showConfirmError('Ссылка для подтверждения недействительна') + return + } + + const confirmEmail = async () => { + try { + const { data } = await axios.post<{ access?: string; refresh?: string }>( + getApiUrl() + '/v2/auth/confirm', + { token: tokenData }, + { headers: { 'Content-Type': 'application/json' } } + ) + + if (!data?.access || !data?.refresh) { + showConfirmError() + return + } + + const resp = await signIn('tokens', { + access: data.access, + refresh: data.refresh, + redirect: false, + }) + + if (resp?.ok) { + setSuccess(true) + push('/') + return + } + + showConfirmError() + } catch (err) { + showConfirmError(getApiDetail(err)) + } + } - axios.post(getApiUrl() + '/auth/confirm', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, validateStatus: (status) => status < 400 - }).then(() => { - setSuccess(true) - setTimeout(() => signIn('email_token', { token: tokenData, redirect: false }) - .then(() => push('/')) - .catch((err) => push({ pathname: '/login', query: { error: err ?? '' } })), 5000) - }).catch((err) => { - setSuccess(false) - setTimeout(() => push({ pathname: '/login', query: { error: err?.response.data.detail ?? '' } }), 5000) - }) + void confirmEmail() }, []) return ( @@ -56,9 +89,7 @@ const Confirm: NextPageWithLayout = () => { ) : success !== null && !success ? ( <> - Произошла проблема при потверждении почты -
- Уже получили сообщение об ошибке, пожалуйста - повторите попытку позже + {errorMessage ?? CONFIRM_FAILED_MESSAGE}
Вернуться к авторизации @@ -50,8 +50,21 @@ const Login: NextPageWithLayout = () => { if (!error || error === '') return - const expectedError = ERRROR_YANDEX_TRANSLATE_MAPPING[error] - showMessage(expectedError ?? 'Не удалось выполнить вход, попробуйте позже') + const yandexError = ERRROR_YANDEX_TRANSLATE_MAPPING[error] + if (yandexError) { + showMessage(yandexError) + return + } + + const authError = Object.fromEntries( + Object.entries(ERROR_MAPPING).map(([message, slug]) => [slug, message]) + )[error] + if (authError) { + showMessage(authError) + return + } + + showMessage(error) }, []) async function onSubmit(data: Record) { @@ -1,112 +0,0 @@ -import React from 'react' -import { useForm } from 'react-hook-form' -import { Typography } from '@mui/material' - -import { useAppSelector } from '#/app/store/store' -import AttachSvg from '#/assets/svg/attach.svg?react' -import PlusSvg from '#/assets/svg/plus.svg?react' -import { useErrorReport } from '#/features/error-report/model/use-error-report' -import { CompanyAutocomplete } from '#/features/register-business/api/company-autocomplete' -import { companyNameOptions } from '#/features/register-business/lib/constatnts-step-legal-informative' -import { NextPageWithLayout } from '#/pages/_app' -import { c } from '#/shared' -import { useBodyScrollLock } from '#/shared/lib/hooks/use-body-scroll-lock' -import { CommonButton } from '#/shared/ui/button/ui' -import { CommonTextArea } from '#/shared/ui/common-textarea' - -import styles from './error-mobile.module.scss' - -const BusinessReportsMobile: NextPageWithLayout = () => { - const { error, isSend, setErrorMessage, handleFileChange, file, setFile, sendError, handleGoBackPage } = useErrorReport('business') - - const { companyName } = useAppSelector((state) => state.stepper.dataForCreate) - const [isAutocompleteOpen, setAutocompleteOpen] = React.useState(false) - - // хук для предотвращения скрола по странице - useBodyScrollLock(isAutocompleteOpen) - - const methods = useForm({ - defaultValues: { - companyName - }, - }) - - return ( - <> - {isSend ? ( -
-

Спасибо!

-

Ваше сообщение направлено нашим специалистам. Ответ придет на почту, указанную при регистрации

-
- ) : ( -
-

Запрос для компаний и партнеров

- - Опишите ваш сценарий — подберем решение и ответим за день. Работаем для корпораций, агентств и интеграторов - - - setAutocompleteOpen(true)} - onClose={() => setAutocompleteOpen(false)} - /> - - setErrorMessage(evt.target.value)} - buttomSlot={ - <> - - - } - /> - {file && ( -
-
- -
- {file.name} - {(file.size / 1024).toFixed(1)} KB -
-
- - -
- )} -
- - Отправить - - - Отмена - -
-
- )} - - ) -} - -export default BusinessReportsMobile @@ -11,7 +11,6 @@ import { getUserBalance } from '#/entities/balance' import { getAllInfo } from '#/entities/user-account' import { ChangePasswordPlate } from '#/features/change-password-corp' import { ErrorReportPlate } from '#/features/error-report' -import { BusinessErrorReportPlate } from '#/features/error-report/ui/business-error-report' import { change } from '#/features/pending' import { SlowLoading } from '#/features/slow-loading' import { Loader } from '#/shared' @@ -145,7 +144,6 @@ export const Layout: React.FC = ({ children, titlePage, isLoader, h {/* Дефолтные модалки */} -
) @@ -109,13 +109,6 @@ export const MainMenuMobile = memo(() => { link={'/reports-mobile'} icon={'/svg/side-menu/side-question'} /> -
{ const { isNextStepVisible } = useNextStep() const error_report = getModalById(ERROR_REPORT) - const business_error_report = getModalById(BUSINESS_ERROR_REPORT) - const match = useMediaQuery('(min-height:800px)') const [open, setOpen] = useState<'opened' | 'closed'>('closed') @@ -255,19 +253,6 @@ export const SideMenu = ({}) => { icon={'/svg/side-menu/side-question'} /> - { - business_error_report.setState(true) - }} - > - -