diff --git a/src/app/providers/StoreProvider/config/StateSchema.ts b/src/app/providers/StoreProvider/config/StateSchema.ts index a6a55ed1..ae393e9b 100644 --- a/src/app/providers/StoreProvider/config/StateSchema.ts +++ b/src/app/providers/StoreProvider/config/StateSchema.ts @@ -18,6 +18,7 @@ import type { ICartEntitySchema } from '@/entities/CartEntity/model/types/types' import type { IAboutUsSchema } from '@/pages/AboutUsPage/model/types/types' import type { IFeedbackSchema } from '@/features/Reviews/model/types/types' import type { TNumberOfPageSchema } from '@/widgets/Pagination/types/types' +import { ICreateAccountSchema } from '@/widgets/CreateAccount/model/types/types' export interface StateSchema { aboutUs: IAboutUsSchema @@ -42,6 +43,7 @@ export interface StateSchema { categoryFilters: ICategoryFiltersSchema feedbacks: IFeedbackSchema pagination: TNumberOfPageSchema + createAccount: ICreateAccountSchema } export interface ThunkExtraArg { diff --git a/src/app/providers/StoreProvider/config/store.ts b/src/app/providers/StoreProvider/config/store.ts index 665f26b4..d62690fe 100644 --- a/src/app/providers/StoreProvider/config/store.ts +++ b/src/app/providers/StoreProvider/config/store.ts @@ -23,6 +23,7 @@ import { aboutUsReducer } from '@/pages/AboutUsPage/model/slice/aboutUsSlice' import { cartEntityReducer } from '@/entities/CartEntity/model/slice/cartEntitySlice' import { feedbacksReducer } from '@/features/Reviews/model/slice/feedbacksSlice' import { paginationSliceReducer } from '@/widgets/Pagination/slice/paginationSlice' +import { createAccountReducer } from '@/widgets/CreateAccount/model/slice/loginSlice' export type RootState = StateSchema @@ -48,7 +49,8 @@ const rootReducer: ReducersMapObject = { getCategories: getCategoriesReducer, cartEntity: cartEntityReducer, categoryFilters: categoryFiltersSliceReducer, - pagination: paginationSliceReducer + pagination: paginationSliceReducer, + createAccount: createAccountReducer } export function createReduxStore(initialState: RootState) { diff --git a/src/app/router/AppRouter/ui/AppRouter.tsx b/src/app/router/AppRouter/ui/AppRouter.tsx index a12fa053..930d6587 100644 --- a/src/app/router/AppRouter/ui/AppRouter.tsx +++ b/src/app/router/AppRouter/ui/AppRouter.tsx @@ -8,6 +8,7 @@ import { CategoryPage } from '@/pages/CategoryPage/CategoryPage' import ComparePage from '@/pages/ComparePage/ComparePage' import ContactsPage from '@/pages/ContactsPage/ContactsPage' import CreateAccountPage from '@/pages/CreateAccountPage/CreateAccountPage' +import { CreateAccountSuccess } from '@/pages/CreateAccountSuccess/CreateAccountSuccess' import DeliveryPage from '@/pages/DeliveryPage/DeliveryPage' import ErrorPage from '@/pages/ErrorPage/ErrorPage' import { FavoritesPage } from '@/pages/FavoritesPage/FavoritesPage' @@ -23,8 +24,8 @@ import { ProductsPage } from '@/pages/ProductsPage/ProductsPage' import { ReviewsPage } from '@/pages/ReviewsPage/ReviewsPage' import RootPage from '@/pages/RootPage/RootPage' import SearchResultsPage from '@/pages/SearchResultsPage/SearchResultsPage' -import SubscriptionPage from '@/pages/SubscriptionPage/SubscriptionPage' import ShopNewsPage from '@/pages/ShopNewsPage/ShopNewsPage' +import SubscriptionPage from '@/pages/SubscriptionPage/SubscriptionPage' import { TermsPage } from '@/pages/TermsPage/TermsPage' import VouchersPage from '@/pages/VouchersPage/VouchersPage' import { Routes } from '@/shared/config/routerConfig/routes' @@ -137,6 +138,10 @@ export const AppRouter = createBrowserRouter([ path: Routes.ACCOUNT, element: }, + { + path: Routes.CREATE_ACCOUNT_SUCCESS, + element: + }, { path: Routes.LOGOUT, element: diff --git a/src/features/login/ui/LoginForm/LoginForm.tsx b/src/features/login/ui/LoginForm/LoginForm.tsx index 0ad6da4c..cda6be8b 100644 --- a/src/features/login/ui/LoginForm/LoginForm.tsx +++ b/src/features/login/ui/LoginForm/LoginForm.tsx @@ -49,6 +49,7 @@ const LoginForm: FC = ({ isModalOpen, handleClose, onLogin }) => } const onRegistration = () => { + handleClose && handleClose() navigate(Routes.REGISTRATION) } diff --git a/src/pages/CreateAccountPage/CreateAccountPage.module.scss b/src/pages/CreateAccountPage/CreateAccountPage.module.scss index 4e8d74f4..a17139ec 100644 --- a/src/pages/CreateAccountPage/CreateAccountPage.module.scss +++ b/src/pages/CreateAccountPage/CreateAccountPage.module.scss @@ -1,40 +1,41 @@ @use '@/shared/styles/utils/mixins' as media; .container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 65px 0; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 65px 0; } .wrapper { - display: flex; - align-items: center; - justify-content:space-between; - width: 45%; - margin-bottom: 20px; - @include media.respond-to('large') { - width: 80%; - } - @include media.respond-to('middle') { - width: 94%; - } + display: flex; + align-items: center; + justify-content: space-between; + width: 45%; + margin-bottom: 20px; + @include media.respond-to('large') { + width: 80%; + } + @include media.respond-to('middle') { + width: 94%; + } } .auth { - display: flex; - align-items: center; - justify-content: center; - column-gap: 10px; + display: flex; + align-items: center; + justify-content: center; + column-gap: 10px; } .button { - padding: 4px 14px; + padding: 4px 14px; } .paragraph { - @include media.respond-to('middle') { - display: none; - } -} \ No newline at end of file + @include media.respond-to('middle') { + display: none; + } +} diff --git a/src/pages/CreateAccountSuccess/CreateAccountSuccess.module.scss b/src/pages/CreateAccountSuccess/CreateAccountSuccess.module.scss new file mode 100644 index 00000000..7587c29c --- /dev/null +++ b/src/pages/CreateAccountSuccess/CreateAccountSuccess.module.scss @@ -0,0 +1,42 @@ +@use '../../shared/styles/utils/mixins' as media; +@use '../../shared/styles/utils/variables' as color; + +.createAccountPage { + width: 100%; + + &__pageDescriptor { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; + } + + &__container { + width: 100%; + display: flex; + justify-content: start; + align-items: start; + gap: 20px; + + @include media.respond-to('large') { + flex-direction: column; + gap: 10px; + } + } + + &__contentContainer { + width: 100%; + display: flex; + flex-direction: column; + gap: 50px; + } + + &__button { + width: 150px; + transition: opacity 0.3s; + + &:hover { + opacity: 0.8; + } + } +} diff --git a/src/pages/CreateAccountSuccess/CreateAccountSuccess.tsx b/src/pages/CreateAccountSuccess/CreateAccountSuccess.tsx new file mode 100644 index 00000000..42996c51 --- /dev/null +++ b/src/pages/CreateAccountSuccess/CreateAccountSuccess.tsx @@ -0,0 +1,88 @@ +import { FC, Suspense, useState } from 'react' +import { useNavigate } from 'react-router' + +import SideBarMenuModal from '@/features/SideBarMenuModal' +import { Routes } from '@/shared/config/routerConfig/routes' +import { useResize } from '@/shared/libs/hooks/useResize' +import Breadcrumbs from '@/shared/ui/Breadcrumbs/Breadcrumbs' +import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button/Button' +import Heading from '@/shared/ui/Heading/Heading' +import Modal from '@/shared/ui/Modal/Modal' +import Paragraph from '@/shared/ui/Paragraph/Paragraph' +import Spinner from '@/shared/ui/Spinner/Spinner' +import WrapperForMainContent from '@/shared/ui/WrapperForMainContent/WrapperForMainContent' +import { withAdaptiveSideBar } from '@/widgets/SideBarMenu' + +import styles from './CreateAccountSuccess.module.scss' + +const links = [ + { heading: 'Главная', href: '/' }, + { heading: 'Личный Кабинет', href: Routes.ACCOUNT }, + { heading: 'Успешно', href: '' } +] + +/** + * Страница успешной регистрации + * + */ +export const CreateAccountSuccess: FC = () => { + const { isScreenLg } = useResize() + const [isModalOpen, setIsModalOpen] = useState(false) + const [isModalClosing, setIsModalClosing] = useState(false) + const AdaptiveSideBar = withAdaptiveSideBar(isScreenLg) + const navigate = useNavigate() + + const handleClick = () => { + setIsModalOpen(true) + } + + const changeModalState = () => { + setIsModalOpen(!isModalOpen) + } + + const onGoOnHandle = () => { + navigate(Routes.LOGIN) + } + + return ( + +
+ Ваша учетная запись создана! + +
+
+ +
+ + Поздравляем! Ваш Личный Кабинет был успешно создан. Для окончания регистарции перейдите по ссылке + в письме! + + + После завершения регистрации Вы сможете воспользоваться дополнительными возможностями: просмотр + истории заказов, печать счета, изменение своей контактной информации и адресов доставки и многое + другое. + + Если у Вас есть какие-либо вопросы, напишите нам. + +
+
+ {isModalOpen && ( + + }> + + + + )} +
+ ) +} diff --git a/src/pages/ShopNewsPage/ShopNewsPage.tsx b/src/pages/ShopNewsPage/ShopNewsPage.tsx index 12e0fe3d..8b1c970d 100644 --- a/src/pages/ShopNewsPage/ShopNewsPage.tsx +++ b/src/pages/ShopNewsPage/ShopNewsPage.tsx @@ -1,6 +1,6 @@ -import WrapperForMainContent from '@/components/WrapperForMainContent/WrapperForMainContent' import Breadcrumbs from '@/shared/ui/Breadcrumbs/Breadcrumbs' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' +import WrapperForMainContent from '@/shared/ui/WrapperForMainContent/WrapperForMainContent' import ShopNewsWidget from '@/widgets/NewsBlock/ui/ShopNewsWidget/ShopNewsWidget' import styles from './ShopNewsPage.module.scss' diff --git a/src/pages/SubscriptionPage/SubscriptionPage.tsx b/src/pages/SubscriptionPage/SubscriptionPage.tsx index c1a27456..50e70217 100644 --- a/src/pages/SubscriptionPage/SubscriptionPage.tsx +++ b/src/pages/SubscriptionPage/SubscriptionPage.tsx @@ -6,11 +6,10 @@ import Breadcrumbs from '@/shared/ui/Breadcrumbs/Breadcrumbs' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' import Modal from '@/shared/ui/Modal/Modal' import Spinner from '@/shared/ui/Spinner/Spinner' +import WrapperForMainContent from '@/shared/ui/WrapperForMainContent/WrapperForMainContent' import DistributionForm from '@/widgets/DistributionForm/ui/DistributionForm' import { withAdaptiveSideBar } from '@/widgets/SideBarMenu' -import WrapperForMainContent from '../../components/WrapperForMainContent/WrapperForMainContent' - import styles from './SubscriptionPage.module.scss' const links = [ diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index bd3c1355..b8c3eb82 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -17,7 +17,8 @@ export enum ApiRoutes { DECREASE_PRODUCT_AMOUNT = 'cart/subtract/', REMOVE_PRODUCT = 'cart/delete/', RENEW_PRODUCT_AMOUNT = 'cart/', - USER = 'users/me' + USER = 'users/me', + CREATE_ACCOUNT = 'users' } export enum ApiErrorTypes { diff --git a/src/shared/config/routerConfig/routes.ts b/src/shared/config/routerConfig/routes.ts index cd47445c..6077c9f3 100644 --- a/src/shared/config/routerConfig/routes.ts +++ b/src/shared/config/routerConfig/routes.ts @@ -30,7 +30,8 @@ export enum Routes { FORGOT_PASSWORD = '/forgot-password', REGISTRATION = '/registration', SUBSCRIBE = '/subscribe', - NEWSLETTER = '/newsletter', ERROR = '*', + CREATE_ACCOUNT_SUCCESS = '/success', + NEWSLETTER = '/newsletter', SHOP_NEWS = '/shopnews' } diff --git a/src/widgets/CreateAccount/model/services/createAccount/createAccount.ts b/src/widgets/CreateAccount/model/services/createAccount/createAccount.ts new file mode 100644 index 00000000..805625ed --- /dev/null +++ b/src/widgets/CreateAccount/model/services/createAccount/createAccount.ts @@ -0,0 +1,21 @@ +import { createAsyncThunk } from '@reduxjs/toolkit' + +import { ThunkConfig } from '@/app/providers/StoreProvider/config/StateSchema' +import { apiErrorIdentify } from '@/shared/api/apiErrorIdentify' +import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types' + +import type { ICreateAccountResult, TCreateAccountPayload } from '../../types/types' + +export const createAccount = createAsyncThunk< + ICreateAccountResult, + TCreateAccountPayload, + ThunkConfig +>('createAccount/createAccount', async (values, thunkAPI) => { + const { rejectWithValue, extra } = thunkAPI + try { + const { data } = await extra.api.post(`api/${ApiRoutes.CREATE_ACCOUNT}/`, values) + return data + } catch (error) { + return rejectWithValue(apiErrorIdentify(error, ApiErrorTypes.DATA_EMPTY_ERROR)) + } +}) diff --git a/src/widgets/CreateAccount/model/slice/loginSlice.ts b/src/widgets/CreateAccount/model/slice/loginSlice.ts new file mode 100644 index 00000000..bd219a03 --- /dev/null +++ b/src/widgets/CreateAccount/model/slice/loginSlice.ts @@ -0,0 +1,40 @@ +import { createSlice } from '@reduxjs/toolkit' + +import { rejectedPayloadHandle } from '@/shared/api/rejectedPayloadHandle' + +import { createAccount } from '../services/createAccount/createAccount' +import { ICreateAccountSchema } from '../types/types' + +const initialState: ICreateAccountSchema = { + isLoading: false, + user: { + id: null, + email: null + } +} + +export const createAccountSlice = createSlice({ + name: 'createAccount', + initialState, + reducers: { + errorReset: state => { + state.error = undefined + } + }, + extraReducers: builder => { + builder + .addCase(createAccount.pending, state => { + state.error = undefined + state.isLoading = true + }) + .addCase(createAccount.fulfilled, state => { + state.isLoading = false + }) + .addCase(createAccount.rejected, (state, { payload }) => { + state.isLoading = false + state.error = rejectedPayloadHandle(payload) + }) + } +}) + +export const { actions: createAccountActions, reducer: createAccountReducer } = createAccountSlice diff --git a/src/widgets/CreateAccount/model/types.ts b/src/widgets/CreateAccount/model/types.ts deleted file mode 100644 index b5c92151..00000000 --- a/src/widgets/CreateAccount/model/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface ICreateAccountForm { - name: string - surname: string - email: string - tel: string - country: string - region: string - index: string - model: string - city: string - password: string - passwordConfirmation: string - subscription: string - agreement: boolean -} diff --git a/src/widgets/CreateAccount/model/types/types.ts b/src/widgets/CreateAccount/model/types/types.ts new file mode 100644 index 00000000..05860111 --- /dev/null +++ b/src/widgets/CreateAccount/model/types/types.ts @@ -0,0 +1,31 @@ +export interface ICreateAccountForm { + name?: string + surname?: string + email: string + tel?: string + country?: string + region?: string + index?: string + model?: string + city?: string + password: string + passwordConfirmation: string + subscription?: string + agreement?: boolean +} + +export type TCreateAccountPayload = Omit + +export interface ICreateAccountResult { + email: string + id: number +} + +export interface ICreateAccountSchema { + isLoading: boolean + user: { + id: null + email: null + } + error?: string | string[] +} diff --git a/src/widgets/CreateAccount/model/validation.ts b/src/widgets/CreateAccount/model/validation/validation.ts similarity index 62% rename from src/widgets/CreateAccount/model/validation.ts rename to src/widgets/CreateAccount/model/validation/validation.ts index 5c7ff967..985e1a56 100644 --- a/src/widgets/CreateAccount/model/validation.ts +++ b/src/widgets/CreateAccount/model/validation/validation.ts @@ -2,28 +2,22 @@ import * as Yup from 'yup' export const validationSchema = Yup.object().shape({ name: Yup.string() - .required('Введите имя') .min(2, 'Минимальная длина имени 6 символов') .max(64, 'Максимальная длина имени 64 символа'), surname: Yup.string() - .required('Введите фамилию') .min(1, 'Минимальная длина фамилии 1 символ') .max(64, 'Максимальная длина фамилии 64 символа'), email: Yup.string() .required('Введите электронную почту') .email('Укажите корректный адрес электронной почты'), - tel: Yup.string() - .required('Введите номер телефона') - .matches(/^\+7\d{10}$/, 'Номер телефона должен быть в формате +7XXXXXXXXXX (X - цифра)'), + tel: Yup.string().matches(/^\+7\d{10}$/, 'Номер телефона должен быть в формате +7XXXXXXXXXX (X - цифра)'), country: Yup.string(), region: Yup.string(), - index: Yup.number() - .min(6, 'Количество символов должно быть 6') - .max(6, 'Количество символов должно быть 6') - .typeError('Индекс указывается только цифрами'), + index: Yup.string().matches(/^[0-9]{6}$/, 'Индекс должен быть в формате XXXXXX (X - цифра)'), city: Yup.string(), password: Yup.string().required('Введите пароль'), passwordConfirmation: Yup.string() .required('Введите подтверждение пароля') - .oneOf([Yup.ref('password')], 'Пароли должны совпадать') + .oneOf([Yup.ref('password')], 'Пароли должны совпадать'), + agreement: Yup.bool().oneOf([true], 'Необходимо ознакомиться с политикой безопасности.') }) diff --git a/src/widgets/CreateAccount/ui/CreateAccountForm.module.scss b/src/widgets/CreateAccount/ui/CreateAccountForm.module.scss index 30bdcaff..8b33b521 100644 --- a/src/widgets/CreateAccount/ui/CreateAccountForm.module.scss +++ b/src/widgets/CreateAccount/ui/CreateAccountForm.module.scss @@ -15,7 +15,7 @@ width: 80%; } @include media.respond-to('middle') { - width: 94%; + width: 94%; } &__paragraph { @@ -25,7 +25,6 @@ &__title { margin-bottom: 20px; - &_second { margin-top: 20px; @@ -36,25 +35,10 @@ margin: 0 0 20px; padding: 0; - &_notRequired[data-no-star]::before { - content: ''; - } - &_agreement { margin-bottom: 0; line-height: 20px; } - - } - - &__label::before { - content: '*'; - color: var.$promo-color; - margin-right: 3px; - - &_notRequired[data-no-star]::before { - content: ''; - } } &__list { @@ -68,11 +52,11 @@ border-radius: 10px; margin: 5px 0 0; padding: 10px 16px; - + &_extra { width: 100%; - } - } + } + } &__input:focus { border: 2px solid var.$theme-primary-color; @@ -85,7 +69,16 @@ left: 17px; font-size: 12px; font-weight: 100; - color: var.$promo-color; + color: var.$promo-color; + + &_agreement { + top: -15px; + left: 0; + + @include media.respond-to('small') { + top: -40px; + } + } } &__radio { @@ -105,11 +98,15 @@ &__agreement { display: flex; align-items: flex-start; - column-gap: 10px; + column-gap: 10px; border-top: 1px solid var.$border-color; padding: 20px 0; margin-bottom: 15px; margin-top: 20px; + + @include media.respond-to('small') { + padding-top: 60px; + } } &__span { @@ -118,12 +115,13 @@ border: 0; color: var.$link-color; font-size: 14px; - &:hover { color: var.$link-color; - transition: opacity 0.25s, color 0.25s; + transition: + opacity 0.25s, + color 0.25s; opacity: 0.7; } } -} \ No newline at end of file +} diff --git a/src/widgets/CreateAccount/ui/CreateAccountForm.tsx b/src/widgets/CreateAccount/ui/CreateAccountForm.tsx index 1cd948c3..bad49690 100644 --- a/src/widgets/CreateAccount/ui/CreateAccountForm.tsx +++ b/src/widgets/CreateAccount/ui/CreateAccountForm.tsx @@ -1,14 +1,20 @@ import classNames from 'classnames' -import { ErrorMessage, Field, Form, Formik } from 'formik' +import { ErrorMessage, Field, Form, Formik, FormikHelpers } from 'formik' +import { FC } from 'react' +import { useNavigate } from 'react-router' +import { Routes } from '@/shared/config/routerConfig/routes' +import { useAppDispatch } from '@/shared/libs/hooks/store' import { Button, ButtonDesign, ButtonSize, ButtonTheme } from '@/shared/ui/Button/Button' import Checkbox from '@/shared/ui/Checkbox/Checkbox' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' import { Input } from '@/shared/ui/Input/Input' import Label from '@/shared/ui/Label/Label' +import { RequiredFieldTitle } from '@/widgets/FeedbackForm/ui/RequiredFieldTitle/RequiredFieldTitle' -import { ICreateAccountForm } from '../model/types' -import { validationSchema } from '../model/validation' +import { createAccount } from '../model/services/createAccount/createAccount' +import { ICreateAccountForm } from '../model/types/types' +import { validationSchema } from '../model/validation/validation' import styles from './CreateAccountForm.module.scss' @@ -28,37 +34,59 @@ const initialValues: ICreateAccountForm = { agreement: false } -const subscription = [ +/*TODO Добавить по готовности бэкенда + const subscription = [ { label: 'Да', value: 'Да' }, { label: 'Нет', value: 'Нет' } -] +] */ const countries = ['---Выберите---', 'Белоруссия (Беларусь)', 'Российская Федерация'] /** * Страница регистрации */ -const CreateAccountForm = () => { +const CreateAccountForm: FC = () => { + const navigate = useNavigate() + const dispatch = useAppDispatch() const handleRedirect = () => { //TODO } const openModal = () => { //TODO } + + const handleSubmit = async (values: ICreateAccountForm, helpers: FormikHelpers) => { + const result = await dispatch(createAccount(values)) + if (result.meta.requestStatus === 'fulfilled') { + helpers.resetForm() + navigate(Routes.CREATE_ACCOUNT_SUCCESS) + } + } + return ( { - setSubmitting(false) - resetForm() - }}> + onSubmit={handleSubmit}> {({ isSubmitting }) => (
Основные данные + @@ -81,23 +108,9 @@ const CreateAccountForm = () => { name="surname" id="surname" placeholder="Фамилия" - required /> - @@ -143,7 +155,6 @@ const CreateAccountForm = () => { name="region" id="region" placeholder="Регион / Область" - required /> @@ -174,7 +185,6 @@ const CreateAccountForm = () => { name="city" id="city" placeholder="Город" - required /> @@ -182,7 +192,7 @@ const CreateAccountForm = () => { Ваш пароль + {/*TODO Добавить по готовности бэкенда Рассылка новостей @@ -227,7 +238,7 @@ const CreateAccountForm = () => { ) })} - + */}
{ type="checkbox" />