From 24dfacd033b711486e80a47f71c5b05b6a117456 Mon Sep 17 00:00:00 2001 From: Kseniya Date: Mon, 15 Apr 2024 14:49:08 +0300 Subject: [PATCH 01/66] development-195-category-list-api-WIP --- src/entities/CategoryCard/CategoryCard.tsx | 28 +++++++++++++++++-- src/entities/CategoryCard/getCategoryCard.tsx | 24 ++++++++++++++++ .../CategoryCardList/CategoryCardList.tsx | 4 +-- 3 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 src/entities/CategoryCard/getCategoryCard.tsx diff --git a/src/entities/CategoryCard/CategoryCard.tsx b/src/entities/CategoryCard/CategoryCard.tsx index 0df8054a..bee27782 100644 --- a/src/entities/CategoryCard/CategoryCard.tsx +++ b/src/entities/CategoryCard/CategoryCard.tsx @@ -1,19 +1,41 @@ import { type FC } from 'react' -import FM from '@/assets/images/categoryPage/FM.webp' +// import axios from 'axios' +// import FM from '@/assets/images/categoryPage/FM.webp' import Paragraph from '@/shared/ui/Paragraph/Paragraph' import styles from './CategoryCard.module.scss' +import getCatgoryCard from './getCategoryCard' + +// import { ApiRoutes } from '@/shared/api/types' /** * Карточка категории */ export const CategoryCard: FC = () => { + // function getCatgoryCard() { + + // const [categoryState, setCategoryState] = useState(); + + // useEffect(() => { + // const apiUrlCategory = `api/${ApiRoutes.CATEGORIES}`; + // axios.get(apiUrlCategory).then((resp) => { + // const allCards = resp.data; + // setCategoryState(allCards); + // }); + // }, [setCategoryState]); + // return ( + //
+ + //
+ // ); + // } + return (
- FM-трансмиттеры - FM-трансмиттеры + {getCatgoryCard.name} + getCatgoryCard.name
) } diff --git a/src/entities/CategoryCard/getCategoryCard.tsx b/src/entities/CategoryCard/getCategoryCard.tsx new file mode 100644 index 00000000..242daf10 --- /dev/null +++ b/src/entities/CategoryCard/getCategoryCard.tsx @@ -0,0 +1,24 @@ +import axios from 'axios' +import { useEffect, useState } from 'react' + +import { ApiRoutes } from '@/shared/api/types' + +function getCatgoryCard() { + const [categoryState, setCategoryState] = useState() + + useEffect(() => { + const apiUrlCategory = `api/${ApiRoutes.CATEGORIES}` + axios.get(apiUrlCategory).then(resp => { + const allCards = resp.data + setCategoryState(allCards) + }) + }, [setCategoryState]) + + useEffect(() => { + console.log(categoryState) + }) + + return
+} + +export default getCatgoryCard diff --git a/src/widgets/CategoryCardList/CategoryCardList.tsx b/src/widgets/CategoryCardList/CategoryCardList.tsx index ebaf4582..71f9c015 100644 --- a/src/widgets/CategoryCardList/CategoryCardList.tsx +++ b/src/widgets/CategoryCardList/CategoryCardList.tsx @@ -12,6 +12,7 @@ export const CategoryCardList: FC = () => { return (
+ {/* @@ -19,8 +20,7 @@ export const CategoryCardList: FC = () => { - - + */}
) } From c040b9910f86dfc79084b9f5aaafc6ffad1dc9de Mon Sep 17 00:00:00 2001 From: Kseniya Date: Thu, 18 Apr 2024 13:29:19 +0300 Subject: [PATCH 02/66] enhancement-api/selectors/types-WIP --- src/entities/CategoryCard/CategoryCard.tsx | 45 +++++++------------ src/entities/CategoryCard/getCategoryCard.tsx | 39 ++++++++-------- src/entities/CategoryCard/selectors.tsx | 5 +++ src/entities/CategoryCard/types.tsx | 10 +++++ 4 files changed, 49 insertions(+), 50 deletions(-) create mode 100644 src/entities/CategoryCard/selectors.tsx create mode 100644 src/entities/CategoryCard/types.tsx diff --git a/src/entities/CategoryCard/CategoryCard.tsx b/src/entities/CategoryCard/CategoryCard.tsx index bee27782..5cd8db29 100644 --- a/src/entities/CategoryCard/CategoryCard.tsx +++ b/src/entities/CategoryCard/CategoryCard.tsx @@ -1,41 +1,28 @@ -import { type FC } from 'react' +import { FC, useEffect } from 'react' +import { useSelector, useDispatch } from 'react-redux' -// import axios from 'axios' -// import FM from '@/assets/images/categoryPage/FM.webp' import Paragraph from '@/shared/ui/Paragraph/Paragraph' import styles from './CategoryCard.module.scss' -import getCatgoryCard from './getCategoryCard' +import { getCategoryCard } from './getCategoryCard' +import { selectCategory } from './selectors' // селектор selectCategory для извлечения данных категории -// import { ApiRoutes } from '@/shared/api/types' +export const CategoryCard: FC<{ categoryCard: string }> = ({ categoryCard }) => { + const dispatch = useDispatch() + const category = useSelector(state => selectCategory(state, categoryCard)) // передаём selectCategory состояние и имя категории -/** - * Карточка категории - */ - -export const CategoryCard: FC = () => { - // function getCatgoryCard() { - - // const [categoryState, setCategoryState] = useState(); - - // useEffect(() => { - // const apiUrlCategory = `api/${ApiRoutes.CATEGORIES}`; - // axios.get(apiUrlCategory).then((resp) => { - // const allCards = resp.data; - // setCategoryState(allCards); - // }); - // }, [setCategoryState]); - // return ( - //
- - //
- // ); - // } + useEffect(() => { + dispatch(getCategoryCard(categoryCard)) + }, [dispatch, categoryCard]) return (
- {getCatgoryCard.name} - getCatgoryCard.name + {category && ( + <> + {category.name} + {category.name} + + )}
) } diff --git a/src/entities/CategoryCard/getCategoryCard.tsx b/src/entities/CategoryCard/getCategoryCard.tsx index 242daf10..aec89a0b 100644 --- a/src/entities/CategoryCard/getCategoryCard.tsx +++ b/src/entities/CategoryCard/getCategoryCard.tsx @@ -1,24 +1,21 @@ -import axios from 'axios' -import { useEffect, useState } from 'react' +import { createAsyncThunk } from '@reduxjs/toolkit' -import { ApiRoutes } from '@/shared/api/types' +import { ThunkConfig } from '@/app/providers/StoreProvider/config/StateSchema' +import { apiErrorIdentify } from '@/shared/api/apiErrorIdentify' +import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types' +import { SEARCH_CATEGORY } from '@/shared/constants/constants' -function getCatgoryCard() { - const [categoryState, setCategoryState] = useState() +import { TCategory } from '../../models/CategoryModel' - useEffect(() => { - const apiUrlCategory = `api/${ApiRoutes.CATEGORIES}` - axios.get(apiUrlCategory).then(resp => { - const allCards = resp.data - setCategoryState(allCards) - }) - }, [setCategoryState]) - - useEffect(() => { - console.log(categoryState) - }) - - return
-} - -export default getCatgoryCard +export const getCategoryCard = createAsyncThunk>( + SEARCH_CATEGORY, + async (name: string, thunkAPI) => { + const { rejectWithValue, extra } = thunkAPI + try { + const { data } = await extra.api.get(`api/${ApiRoutes.CATEGORIES}?category=${name}`) + return data + } catch (error) { + return rejectWithValue(apiErrorIdentify(error, ApiErrorTypes.DATA_EMPTY_ERROR)) + } + } +) diff --git a/src/entities/CategoryCard/selectors.tsx b/src/entities/CategoryCard/selectors.tsx new file mode 100644 index 00000000..34602bc3 --- /dev/null +++ b/src/entities/CategoryCard/selectors.tsx @@ -0,0 +1,5 @@ +import { RootState } from './types' // Подставьте путь к вашему корневому типу состояния + +export const selectCategory = (state: RootState, categoryId: string): Category | undefined => { + return state.categories.find(category => category.id === categoryId) +} diff --git a/src/entities/CategoryCard/types.tsx b/src/entities/CategoryCard/types.tsx new file mode 100644 index 00000000..6b2a6f4c --- /dev/null +++ b/src/entities/CategoryCard/types.tsx @@ -0,0 +1,10 @@ +// Тип данных для категории +export interface Category { + id: string + name: string +} + +// Тип данных для состояния +export interface RootState { + categories: Category[] // массив категорий +} From d945c224b4496413a818dc048c84bb130cc1ca19 Mon Sep 17 00:00:00 2001 From: Kseniya Date: Thu, 18 Apr 2024 18:36:14 +0300 Subject: [PATCH 03/66] WIP-CategoryCard.tsx/getCategoryCard --- src/entities/CategoryCard/CategoryCard.tsx | 3 ++- src/entities/CategoryCard/selectors.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/entities/CategoryCard/CategoryCard.tsx b/src/entities/CategoryCard/CategoryCard.tsx index 5cd8db29..1f38c504 100644 --- a/src/entities/CategoryCard/CategoryCard.tsx +++ b/src/entities/CategoryCard/CategoryCard.tsx @@ -6,10 +6,11 @@ import Paragraph from '@/shared/ui/Paragraph/Paragraph' import styles from './CategoryCard.module.scss' import { getCategoryCard } from './getCategoryCard' import { selectCategory } from './selectors' // селектор selectCategory для извлечения данных категории +import { RootState } from './types' export const CategoryCard: FC<{ categoryCard: string }> = ({ categoryCard }) => { const dispatch = useDispatch() - const category = useSelector(state => selectCategory(state, categoryCard)) // передаём selectCategory состояние и имя категории + const category = useSelector((state: RootState) => selectCategory(state, categoryCard)) useEffect(() => { dispatch(getCategoryCard(categoryCard)) diff --git a/src/entities/CategoryCard/selectors.tsx b/src/entities/CategoryCard/selectors.tsx index 34602bc3..a4ea5c52 100644 --- a/src/entities/CategoryCard/selectors.tsx +++ b/src/entities/CategoryCard/selectors.tsx @@ -1,4 +1,5 @@ -import { RootState } from './types' // Подставьте путь к вашему корневому типу состояния +import { RootState } from './types' +import { Category } from './types' export const selectCategory = (state: RootState, categoryId: string): Category | undefined => { return state.categories.find(category => category.id === categoryId) From 14b6de24f937eabbe5d85e72362b6a56f01cf8e1 Mon Sep 17 00:00:00 2001 From: Kseniya Date: Sat, 20 Apr 2024 20:18:35 +0300 Subject: [PATCH 04/66] categorySlice,card,list-WIP --- .../CategoryCard/CategoryCard.stories.tsx | 18 ++++-- src/entities/CategoryCard/CategoryCard.tsx | 21 +++---- src/entities/CategoryCard/categorySlice.tsx | 49 ++++++++++++++++ src/entities/CategoryCard/getCategoryCard.tsx | 12 ++-- src/entities/CategoryCard/types.tsx | 15 +++++ src/pages/CategoryPage/CategoryPage.tsx | 2 +- src/shared/constants/constants.ts | 2 + .../CategoryCardList/CategoryCardList.tsx | 57 +++++++++++++++---- 8 files changed, 139 insertions(+), 37 deletions(-) create mode 100644 src/entities/CategoryCard/categorySlice.tsx diff --git a/src/entities/CategoryCard/CategoryCard.stories.tsx b/src/entities/CategoryCard/CategoryCard.stories.tsx index 0b63f792..a0829c9f 100644 --- a/src/entities/CategoryCard/CategoryCard.stories.tsx +++ b/src/entities/CategoryCard/CategoryCard.stories.tsx @@ -1,20 +1,28 @@ import type { Meta, StoryObj } from '@storybook/react' import card1 from '../../assets/images/categoryCards/img-categories-01-210x263.webp' +import { TCategory } from '../../models/CategoryModel' import { CategoryCard } from './CategoryCard' const meta = { title: 'entities/CategoryCard', component: CategoryCard -} satisfies Meta +} as Meta export default meta type Story = StoryObj -export const Default: Story = { - args: { - src: card1, - name: 'FM-трансмиттеры' +interface CategoryCardArgs { + category: TCategory +} + +export const Default: Story = (args: CategoryCardArgs) => + +Default.args = { + category: { + image: card1, + name: 'FM-трансмиттеры', + slug: 'fm-transmitters' } } diff --git a/src/entities/CategoryCard/CategoryCard.tsx b/src/entities/CategoryCard/CategoryCard.tsx index 1f38c504..7c564354 100644 --- a/src/entities/CategoryCard/CategoryCard.tsx +++ b/src/entities/CategoryCard/CategoryCard.tsx @@ -1,26 +1,21 @@ -import { FC, useEffect } from 'react' -import { useSelector, useDispatch } from 'react-redux' +import { FC } from 'react' import Paragraph from '@/shared/ui/Paragraph/Paragraph' -import styles from './CategoryCard.module.scss' -import { getCategoryCard } from './getCategoryCard' -import { selectCategory } from './selectors' // селектор selectCategory для извлечения данных категории -import { RootState } from './types' +import { TCategory } from '../../models/CategoryModel' -export const CategoryCard: FC<{ categoryCard: string }> = ({ categoryCard }) => { - const dispatch = useDispatch() - const category = useSelector((state: RootState) => selectCategory(state, categoryCard)) +import styles from './CategoryCard.module.scss' - useEffect(() => { - dispatch(getCategoryCard(categoryCard)) - }, [dispatch, categoryCard]) +interface CategoryCardProps { + category: TCategory +} +export const CategoryCard: FC = ({ category }) => { return (
{category && ( <> - {category.name} + {category.name} {category.name} )} diff --git a/src/entities/CategoryCard/categorySlice.tsx b/src/entities/CategoryCard/categorySlice.tsx new file mode 100644 index 00000000..33b6c2dd --- /dev/null +++ b/src/entities/CategoryCard/categorySlice.tsx @@ -0,0 +1,49 @@ +import { createSlice } from '@reduxjs/toolkit' + +import { rejectedPayloadHandle } from '@/shared/api/rejectedPayloadHandle' +// import { apiErrorIdentify } from '@/shared/api/apiErrorIdentify'; +// import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types'; +import { REDUCER_CATEGORY } from '@/shared/constants/constants' + +import { getCategoryCard } from './getCategoryCard' +// import { TCategory } from '../../models/CategoryModel'; +import { ICategorySchema } from './types' + +const initialState: ICategorySchema = { + isLoading: false, + category: { + name: '', + count: 0, + next: '', + previous: '', + results: [] + } +} + +export const categoriesProductsSlice = createSlice({ + name: REDUCER_CATEGORY, + initialState, + reducers: {}, + extraReducers: builder => { + builder + .addCase(getCategoryCard.pending, state => { + state.isLoading = true + state.error = null + }) + .addCase(getCategoryCard.fulfilled, (state, { payload }) => { + state.isLoading = false + state.category = payload + }) + .addCase(getCategoryCard.rejected, (state, { payload }) => { + state.isLoading = false + if (payload !== null && typeof payload === 'object') { + state.error = rejectedPayloadHandle(payload) + } else { + state.error = null // обработка для случая, когда payload === null + } + }) + } +}) + +// Экспортируем редуктор среза +export const { actions: categoriesActions, reducer: categoriesProductsReducer } = categoriesProductsSlice diff --git a/src/entities/CategoryCard/getCategoryCard.tsx b/src/entities/CategoryCard/getCategoryCard.tsx index aec89a0b..fb904760 100644 --- a/src/entities/CategoryCard/getCategoryCard.tsx +++ b/src/entities/CategoryCard/getCategoryCard.tsx @@ -3,16 +3,16 @@ 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 { SEARCH_CATEGORY } from '@/shared/constants/constants' +import { ACTION_GET_СATEGORY } from '@/shared/constants/constants' -import { TCategory } from '../../models/CategoryModel' +import { CategoryInfo } from './types' -export const getCategoryCard = createAsyncThunk>( - SEARCH_CATEGORY, - async (name: string, thunkAPI) => { +export const getCategoryCard = createAsyncThunk>( + ACTION_GET_СATEGORY, + async (id: number, thunkAPI) => { const { rejectWithValue, extra } = thunkAPI try { - const { data } = await extra.api.get(`api/${ApiRoutes.CATEGORIES}?category=${name}`) + const { data } = await extra.api.get(`api/${ApiRoutes.CATEGORIES}`) return data } catch (error) { return rejectWithValue(apiErrorIdentify(error, ApiErrorTypes.DATA_EMPTY_ERROR)) diff --git a/src/entities/CategoryCard/types.tsx b/src/entities/CategoryCard/types.tsx index 6b2a6f4c..87d32bf8 100644 --- a/src/entities/CategoryCard/types.tsx +++ b/src/entities/CategoryCard/types.tsx @@ -2,9 +2,24 @@ export interface Category { id: string name: string + image: string +} + +export interface CategoryInfo { + name: string + count: number + next: string + previous: string + results: Category[] } // Тип данных для состояния export interface RootState { categories: Category[] // массив категорий } + +export interface ICategorySchema { + isLoading: boolean + category: CategoryInfo + error?: string | string[] | null | undefined +} diff --git a/src/pages/CategoryPage/CategoryPage.tsx b/src/pages/CategoryPage/CategoryPage.tsx index 489adc2a..a6321779 100644 --- a/src/pages/CategoryPage/CategoryPage.tsx +++ b/src/pages/CategoryPage/CategoryPage.tsx @@ -1,6 +1,6 @@ import Heading from '@/shared/ui/Heading/Heading' import Paragraph from '@/shared/ui/Paragraph/Paragraph' -import { CategoryCardList } from '@/widgets/CategoryCardList/CategoryCardList' +import CategoryCardList from '@/widgets/CategoryCardList/CategoryCardList' import NavigationLink from '@/widgets/NavigationLink/NavigationLink' import styles from './CategoryPage.module.scss' diff --git a/src/shared/constants/constants.ts b/src/shared/constants/constants.ts index 82b96e43..d879f756 100644 --- a/src/shared/constants/constants.ts +++ b/src/shared/constants/constants.ts @@ -35,7 +35,9 @@ export const BRANDS_FOR_MAIN_NUMBER: number = 6 // Actions export const ACTION_GET_SHOP_NEWS = 'get-shop-news' export const ACTION_GET_BLOG_POSTS = 'get-blog-posts' +export const ACTION_GET_СATEGORY = 'get-all-category' // Reducers export const REDUCER_SHOP_NEWS = 'shopNews' export const REDUCER_BLOG_POSTS = 'shopBlogPosts' +export const REDUCER_CATEGORY = 'allcategory' diff --git a/src/widgets/CategoryCardList/CategoryCardList.tsx b/src/widgets/CategoryCardList/CategoryCardList.tsx index 71f9c015..f9f28aa4 100644 --- a/src/widgets/CategoryCardList/CategoryCardList.tsx +++ b/src/widgets/CategoryCardList/CategoryCardList.tsx @@ -1,26 +1,59 @@ -import type { FC } from 'react' +import { FC } from 'react' import { CategoryCard } from '@/entities/CategoryCard/CategoryCard' +import { TCategory } from '../../models/CategoryModel' + import styles from './CategoryCardList.module.scss' /** * Список категорий */ -export const CategoryCardList: FC = () => { +const CategoryCardList: FC = () => { + const category: TCategory = { + id: 1, + name: '', + slug: '', + image: '' + } + return (
- - {/* - - - - - - - - */} +
) } + +export default CategoryCardList + +// import React, { FC, useEffect } from 'react'; +// import { useDispatch, useSelector } from 'react-redux'; +// import { getCategoryCard } from '../../entities/CategoryCard/getCategoryCard'; +// import { RootState } from '../../entities/CategoryCard/types'; +// import { TCategory } from '../../models/CategoryModel'; +// import { CategoryCard } from '@/entities/CategoryCard/CategoryCard'; +// import styles from './CategoryCardList.module.scss'; + +// const CategoryCardList: FC = () => { +// const dispatch = useDispatch(); + +// // Вызываем вашу thunk-функцию для получения данных с бэка +// useEffect(() => { +// dispatch(getCategoryCard('category')); +// }, [dispatch]); + +// // Получаем данные из Redux store +// const categories: TCategory | undefined = useSelector((state: RootState) => state.categories); + +// return ( +//
+// {/* Проверяем, что данные получены, и отображаем их */} +// {categories && ( +// +// )} +//
+// ); +// }; + +// export default CategoryCardList; From e50190cf751ff9e351b2ebce9929e1c81624c4a3 Mon Sep 17 00:00:00 2001 From: kirill-k88 Date: Sun, 21 Apr 2024 12:55:43 +0300 Subject: [PATCH 05/66] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20breadcrumbs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/FavoritesPage/FavoritesPage.module.scss | 7 +++++-- src/pages/FavoritesPage/FavoritesPage.tsx | 11 ++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/pages/FavoritesPage/FavoritesPage.module.scss b/src/pages/FavoritesPage/FavoritesPage.module.scss index a97c1975..d4a58af9 100644 --- a/src/pages/FavoritesPage/FavoritesPage.module.scss +++ b/src/pages/FavoritesPage/FavoritesPage.module.scss @@ -1,3 +1,6 @@ -.heading { - align-self: flex-start; +.pageDescriptor { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; } diff --git a/src/pages/FavoritesPage/FavoritesPage.tsx b/src/pages/FavoritesPage/FavoritesPage.tsx index 2e426205..555b1ecc 100644 --- a/src/pages/FavoritesPage/FavoritesPage.tsx +++ b/src/pages/FavoritesPage/FavoritesPage.tsx @@ -1,4 +1,5 @@ import WrapperForMainContent from '@/components/WrapperForMainContent/WrapperForMainContent' +import Breadcrumbs from '@/shared/ui/Breadcrumbs/Breadcrumbs' import Heading from '@/shared/ui/Heading/Heading' import Subheading from '@/shared/ui/Subheading/Subheading' @@ -8,9 +9,17 @@ import styles from './FavoritesPage.module.scss' * Страница с избранными товарами */ const FavoritesPage = () => { + const links = [ + { heading: 'Главная', href: '/' }, + { heading: 'Избранные товары', href: '' } + ] + return ( - Избранные товары +
+ Избранные товары + +
В разработке
) From d5ba4315b61ae5d1d02bbd0504077e2f6bb604de Mon Sep 17 00:00:00 2001 From: kirill-k88 Date: Sun, 21 Apr 2024 14:43:33 +0300 Subject: [PATCH 06/66] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=81=D0=B5=D1=82=D0=BA=D1=83=20=D1=81=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=B1=D0=B0=D1=80=D0=B0=D0=BD=D0=BD=D1=8B=D0=BC=D0=B8=20=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=80=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FavoritesPage/FavoritesPage.module.scss | 7 ++++++ src/pages/FavoritesPage/FavoritesPage.tsx | 18 ++++++++++++-- .../model/functions/functions.ts | 17 +++++++++++++ src/pages/FavoritesPage/model/types/types.ts | 24 +++++++++++++++++++ src/shared/constants/sessionStorage.ts | 3 ++- 5 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 src/pages/FavoritesPage/model/functions/functions.ts create mode 100644 src/pages/FavoritesPage/model/types/types.ts diff --git a/src/pages/FavoritesPage/FavoritesPage.module.scss b/src/pages/FavoritesPage/FavoritesPage.module.scss index d4a58af9..d4eddf68 100644 --- a/src/pages/FavoritesPage/FavoritesPage.module.scss +++ b/src/pages/FavoritesPage/FavoritesPage.module.scss @@ -4,3 +4,10 @@ flex-direction: column; gap: 10px; } + +.favoritePage_list { + max-width: calc(292px * 3 + 60px); + display: flex; + flex-wrap: wrap; + gap: 30px; +} diff --git a/src/pages/FavoritesPage/FavoritesPage.tsx b/src/pages/FavoritesPage/FavoritesPage.tsx index 555b1ecc..69174aac 100644 --- a/src/pages/FavoritesPage/FavoritesPage.tsx +++ b/src/pages/FavoritesPage/FavoritesPage.tsx @@ -1,9 +1,11 @@ import WrapperForMainContent from '@/components/WrapperForMainContent/WrapperForMainContent' +import { ECardView } from '@/shared/model/types/common' import Breadcrumbs from '@/shared/ui/Breadcrumbs/Breadcrumbs' import Heading from '@/shared/ui/Heading/Heading' -import Subheading from '@/shared/ui/Subheading/Subheading' +import { ProductsList } from '@/widgets/ProductsList/ProductsList' import styles from './FavoritesPage.module.scss' +import { getFavoriteProductsFromStorage } from './model/functions/functions' /** * Страница с избранными товарами @@ -13,6 +15,7 @@ const FavoritesPage = () => { { heading: 'Главная', href: '/' }, { heading: 'Избранные товары', href: '' } ] + const favoriteProducts = getFavoriteProductsFromStorage() return ( @@ -20,7 +23,18 @@ const FavoritesPage = () => { Избранные товары
- В разработке +
+ +
) } diff --git a/src/pages/FavoritesPage/model/functions/functions.ts b/src/pages/FavoritesPage/model/functions/functions.ts new file mode 100644 index 00000000..ea7b3c21 --- /dev/null +++ b/src/pages/FavoritesPage/model/functions/functions.ts @@ -0,0 +1,17 @@ +import { SESSION_STORAGE } from '@/shared/constants/sessionStorage' + +import { TProduct } from '../types/types' + +/** + * Функция возвращает список избранных товаров favoriteProducts из текущей сессии session storage. + */ +export const getFavoriteProductsFromStorage = (): TProduct[] => { + /* const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]' + const favoriteProducts: TProduct[] = JSON.parse(favoriteProductsStr) as TProduct[] + + return favoriteProducts */ + const viewedProductsStr = sessionStorage.getItem(SESSION_STORAGE.VIEWED) || '[]' + const viewedProducts: TProduct[] = JSON.parse(viewedProductsStr) as TProduct[] + + return viewedProducts +} diff --git a/src/pages/FavoritesPage/model/types/types.ts b/src/pages/FavoritesPage/model/types/types.ts new file mode 100644 index 00000000..ce05e6bf --- /dev/null +++ b/src/pages/FavoritesPage/model/types/types.ts @@ -0,0 +1,24 @@ +export interface IObjectWithImage { + image: string + index?: number +} + +export type TImgList = Array + +export type TProduct = { + label_popular: boolean + label_hit: boolean + id: number + category: string + brand: string + price: number + name: string + slug: string + description: string + code: number + wb_urls: string + quantity: number + is_deleted: boolean + wholesale: number + images: TImgList +} diff --git a/src/shared/constants/sessionStorage.ts b/src/shared/constants/sessionStorage.ts index df984640..6d99ccd5 100644 --- a/src/shared/constants/sessionStorage.ts +++ b/src/shared/constants/sessionStorage.ts @@ -1,3 +1,4 @@ export enum SESSION_STORAGE { - VIEWED = 'viewedProducts' + VIEWED = 'viewedProducts', + FAVORITE = 'favoriteProducts' } From c63e3113c84540432bc22b2814730c786a26e3ba Mon Sep 17 00:00:00 2001 From: kirill-k88 Date: Sun, 21 Apr 2024 17:25:18 +0300 Subject: [PATCH 07/66] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B2=20session=20storage=20=D0=B8=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Favorite/model/functions/functions.ts | 61 +++++++++++++++++ .../Favorite/model/hooks/useFavorite.ts | 19 ++++++ .../Favorite}/model/types/types.ts | 6 ++ src/pages/FavoritesPage/FavoritesPage.tsx | 20 +++++- .../model/functions/functions.ts | 11 +--- src/shared/constants/constants.ts | 3 + src/widgets/Product/Product.tsx | 17 +++-- src/widgets/ProductItem/ProductItem.tsx | 65 ++++++++++++++++++- 8 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 src/entities/Favorite/model/functions/functions.ts create mode 100644 src/entities/Favorite/model/hooks/useFavorite.ts rename src/{pages/FavoritesPage => entities/Favorite}/model/types/types.ts (80%) diff --git a/src/entities/Favorite/model/functions/functions.ts b/src/entities/Favorite/model/functions/functions.ts new file mode 100644 index 00000000..b9d57e6b --- /dev/null +++ b/src/entities/Favorite/model/functions/functions.ts @@ -0,0 +1,61 @@ +import { FAVORITE_PRODUCTS_LIMIT } from '@/shared/constants/constants' +import { SESSION_STORAGE } from '@/shared/constants/sessionStorage' + +import { TProduct } from '../types/types' + +/** + * Ф-я проверяет наличие продукта в массив избраных продуктов в session storage + * @param {TProduct} product продукт + * @return {boolean} true/false в зависимости от нахождения в избранном + */ +export const isInFavoriteProducts = (product: TProduct): boolean => { + const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]' + const favoriteProducts: TProduct[] = JSON.parse(favoriteProductsStr) as TProduct[] + + if (product && product.slug && includesProduct(product, favoriteProducts)) { + return true + } + + return false +} + +/** + * Ф-я добавляет продукт в массив избраных продуктов в session storage + * @param {TProduct} product продукт + */ +export const addToFavoriteProducts = (product: TProduct): void => { + const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]' + const favoriteProducts: TProduct[] = JSON.parse(favoriteProductsStr) as TProduct[] + + if (product && product.slug && !includesProduct(product, favoriteProducts)) { + if (favoriteProducts.length === FAVORITE_PRODUCTS_LIMIT) { + favoriteProducts.shift() + } + favoriteProducts.push(product) + console.log(favoriteProducts) + sessionStorage.setItem(SESSION_STORAGE.FAVORITE, JSON.stringify(favoriteProducts)) + } +} + +/** + * Ф-я удаляет продукт из массива избраных продуктов в session storage, если он в нем есть + * @param {TProduct} product продукт + */ +export const removeFromFavoriteProducts = (product: TProduct): void => { + const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]' + const favoriteProducts: TProduct[] = JSON.parse(favoriteProductsStr) as TProduct[] + + if (product && product.slug && includesProduct(product, favoriteProducts)) { + favoriteProducts.splice(indexOfProduct(product, favoriteProducts), 1) + + sessionStorage.setItem(SESSION_STORAGE.FAVORITE, JSON.stringify(favoriteProducts)) + } +} + +function includesProduct(product: TProduct, favoriteProducts: TProduct[]): boolean { + return favoriteProducts.some(p => p.slug === product.slug) +} + +function indexOfProduct(product: TProduct, favoriteProducts: TProduct[]): number { + return favoriteProducts.findIndex(p => p.slug === product.slug) +} diff --git a/src/entities/Favorite/model/hooks/useFavorite.ts b/src/entities/Favorite/model/hooks/useFavorite.ts new file mode 100644 index 00000000..e57aadf6 --- /dev/null +++ b/src/entities/Favorite/model/hooks/useFavorite.ts @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react' + +import { isInFavoriteProducts } from '../functions/functions' +import { TProduct } from '../types/types' + +/** + * Hook для хранения состояния продукта в избранном + * @param {TProduct} product - продукт + * @returns {isLiked, setIsLiked} - состояние нахождения продукта в избранном + */ +export const useFavorite = (product: TProduct) => { + const [isLiked, setIsLiked] = useState(isInFavoriteProducts(product)) + + useEffect(() => { + setIsLiked(isInFavoriteProducts(product)) + }, [product]) + + return { isLiked, setIsLiked } +} diff --git a/src/pages/FavoritesPage/model/types/types.ts b/src/entities/Favorite/model/types/types.ts similarity index 80% rename from src/pages/FavoritesPage/model/types/types.ts rename to src/entities/Favorite/model/types/types.ts index ce05e6bf..c8b1d290 100644 --- a/src/pages/FavoritesPage/model/types/types.ts +++ b/src/entities/Favorite/model/types/types.ts @@ -22,3 +22,9 @@ export type TProduct = { wholesale: number images: TImgList } + +export type TProductSchema = { + product: TProduct + isLoading?: boolean + error?: string | string[] +} diff --git a/src/pages/FavoritesPage/FavoritesPage.tsx b/src/pages/FavoritesPage/FavoritesPage.tsx index 69174aac..9ecc4f33 100644 --- a/src/pages/FavoritesPage/FavoritesPage.tsx +++ b/src/pages/FavoritesPage/FavoritesPage.tsx @@ -1,4 +1,7 @@ +import { useEffect, useState } from 'react' + import WrapperForMainContent from '@/components/WrapperForMainContent/WrapperForMainContent' +import { TProduct } from '@/entities/Favorite/model/types/types' import { ECardView } from '@/shared/model/types/common' import Breadcrumbs from '@/shared/ui/Breadcrumbs/Breadcrumbs' import Heading from '@/shared/ui/Heading/Heading' @@ -11,11 +14,26 @@ import { getFavoriteProductsFromStorage } from './model/functions/functions' * Страница с избранными товарами */ const FavoritesPage = () => { + const [favoriteProducts, setFavoriteProducts] = useState([]) + const links = [ { heading: 'Главная', href: '/' }, { heading: 'Избранные товары', href: '' } ] - const favoriteProducts = getFavoriteProductsFromStorage() + + useEffect(() => { + setFavoriteProducts(getFavoriteProductsFromStorage()) + window.addEventListener('storage', handleStorage) + + return () => { + window.removeEventListener('storage', handleStorage) + } + }, []) + + const handleStorage = () => { + console.log('хэндал стораджа') + setFavoriteProducts(getFavoriteProductsFromStorage()) + } return ( diff --git a/src/pages/FavoritesPage/model/functions/functions.ts b/src/pages/FavoritesPage/model/functions/functions.ts index ea7b3c21..92ad6dd9 100644 --- a/src/pages/FavoritesPage/model/functions/functions.ts +++ b/src/pages/FavoritesPage/model/functions/functions.ts @@ -1,17 +1,12 @@ +import type { TProduct } from '@/entities/Favorite/model/types/types' import { SESSION_STORAGE } from '@/shared/constants/sessionStorage' -import { TProduct } from '../types/types' - /** * Функция возвращает список избранных товаров favoriteProducts из текущей сессии session storage. */ export const getFavoriteProductsFromStorage = (): TProduct[] => { - /* const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]' + const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]' const favoriteProducts: TProduct[] = JSON.parse(favoriteProductsStr) as TProduct[] - return favoriteProducts */ - const viewedProductsStr = sessionStorage.getItem(SESSION_STORAGE.VIEWED) || '[]' - const viewedProducts: TProduct[] = JSON.parse(viewedProductsStr) as TProduct[] - - return viewedProducts + return favoriteProducts } diff --git a/src/shared/constants/constants.ts b/src/shared/constants/constants.ts index ecb8f8cc..50185809 100644 --- a/src/shared/constants/constants.ts +++ b/src/shared/constants/constants.ts @@ -51,6 +51,9 @@ export const REDUCER_CATEGORIES_PRODUCTS = 'shopCategoriesProducts' //Product page export const VIEWED_PRODUCTS_LIMIT = 10 +//Favorite page +export const FAVORITE_PRODUCTS_LIMIT = 10 + //Feedback form export const NAME_LENGTH_MIN_LIMIT = 2 export const NAME_LENGTH_MAX_LIMIT = 30 diff --git a/src/widgets/Product/Product.tsx b/src/widgets/Product/Product.tsx index 57c15ba4..30adef09 100644 --- a/src/widgets/Product/Product.tsx +++ b/src/widgets/Product/Product.tsx @@ -1,6 +1,11 @@ import { type FC, useState } from 'react' import IconCart from '@/assets/icons/IconCart.svg' +import { + addToFavoriteProducts, + removeFromFavoriteProducts +} from '@/entities/Favorite/model/functions/functions' +import { useFavorite } from '@/entities/Favorite/model/hooks/useFavorite' import { CardPreviewHeader } from '@/features/CardPreviewHeader/CardPreviewHeader' import { ProductAvailability } from '@/features/ProductAvailability/ProductAvailability' import { ProductImgCarousel } from '@/features/ProductImgCarousel/ProductImgCarousel' @@ -16,13 +21,19 @@ import { PopupImg } from './ui/PopupImg/PopupImg' * @param product TProductProps - информация о выбранном товаре */ export const Product: FC = ({ product }) => { - const [isLiked, setIsLiked] = useState(false) + const { isLiked, setIsLiked } = useFavorite(product) const [isInCompared, setIsInCompared] = useState(false) const [isInCart, setIsInCart] = useState(false) const [showPopup, setShowPopup] = useState(false) const handleLike = () => { - setIsLiked(!isLiked) + if (!isLiked) { + addToFavoriteProducts(product) + setIsLiked(true) + } else { + removeFromFavoriteProducts(product) + setIsLiked(false) + } } const handleAddToCompared = () => { @@ -49,8 +60,6 @@ export const Product: FC = ({ product }) => { />
- {/* @TODO: Завести shared/ui-компоненты под типографику - https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/77 */}
{`${product.price} ₽`} diff --git a/src/widgets/ProductItem/ProductItem.tsx b/src/widgets/ProductItem/ProductItem.tsx index c8c6c979..011cff37 100644 --- a/src/widgets/ProductItem/ProductItem.tsx +++ b/src/widgets/ProductItem/ProductItem.tsx @@ -2,6 +2,11 @@ import classnames from 'classnames' import { FC, useState } from 'react' import { Link } from 'react-router-dom' +import { + addToFavoriteProducts, + removeFromFavoriteProducts +} from '@/entities/Favorite/model/functions/functions' +import { useFavorite } from '@/entities/Favorite/model/hooks/useFavorite' import { ProductAvailability } from '@/features/ProductAvailability/ProductAvailability' import { WidgetButtonsFunctions } from '@/features/WidgetButtonsFunctions/WidgetButtonsFunctions' import { WidgetButtonsPurchase } from '@/features/WidgetButtonsPurchase/WidgetButtonsPurchase' @@ -61,7 +66,23 @@ export const ProductItem: FC = ({ quantity }) => { const [isInCart, setIsInCart] = useState(false) - const [isLiked, setIsLiked] = useState(false) + const { isLiked, setIsLiked } = useFavorite({ + id: 123, + category: '', + wb_urls: '', + is_deleted: false, + wholesale: 0, + name, + price, + brand, + slug, + description, + code, + images, + label_popular, + label_hit, + quantity + }) const [isInCompared, setIsInCompared] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false) @@ -70,12 +91,52 @@ export const ProductItem: FC = ({ const changeModalState = () => { setIsModalOpen(!isModalOpen) } + const handleAddToCart = () => { setIsInCart(!isInCart) } const handleLike = () => { - setIsLiked(!isLiked) + if (!isLiked) { + //TODO часть свойств зглушка, доделать после мерджа с добавлением в корзину + addToFavoriteProducts({ + id: 123, + category: '', + wb_urls: '', + is_deleted: false, + wholesale: 0, + name, + price, + brand, + slug, + description, + code, + images, + label_popular, + label_hit, + quantity + }) + setIsLiked(true) + } else { + removeFromFavoriteProducts({ + id: 123, + category: '', + wb_urls: '', + is_deleted: false, + wholesale: 0, + name, + price, + brand, + slug, + description, + code, + images, + label_popular, + label_hit, + quantity + }) + setIsLiked(false) + } } const handleAddToCompared = () => { From 2f4e62b2ffa9cdc0af3df4aa1092d9dcacdfee4a Mon Sep 17 00:00:00 2001 From: kirill-k88 Date: Mon, 22 Apr 2024 20:23:13 +0300 Subject: [PATCH 08/66] =?UTF-8?q?=D0=94=D0=BE=D0=B0=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D1=80=D0=BE=D1=81=D0=BB=D1=83=D1=88=D0=B8?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20storage=20=D0=BD=D0=B0=20=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=86=D0=B5=20=D0=B8=D0=B7=D0=B1=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Favorite/model/functions/functions.ts | 22 +++++++++--- .../Favorite/model/hooks/useFavorite.ts | 29 ++++++++++------ .../Favorite/model/hooks/useWithFavorie.ts | 34 +++++++++++++++++++ src/pages/FavoritesPage/FavoritesPage.tsx | 21 ++---------- .../model/functions/functions.ts | 12 ------- src/widgets/Product/Product.tsx | 4 +-- src/widgets/ProductItem/ProductItem.tsx | 4 +-- 7 files changed, 77 insertions(+), 49 deletions(-) create mode 100644 src/entities/Favorite/model/hooks/useWithFavorie.ts delete mode 100644 src/pages/FavoritesPage/model/functions/functions.ts diff --git a/src/entities/Favorite/model/functions/functions.ts b/src/entities/Favorite/model/functions/functions.ts index b9d57e6b..76633d1e 100644 --- a/src/entities/Favorite/model/functions/functions.ts +++ b/src/entities/Favorite/model/functions/functions.ts @@ -4,7 +4,7 @@ import { SESSION_STORAGE } from '@/shared/constants/sessionStorage' import { TProduct } from '../types/types' /** - * Ф-я проверяет наличие продукта в массив избраных продуктов в session storage + * Ф-я проверяет наличие продукта в массив избранных продуктов в session storage * @param {TProduct} product продукт * @return {boolean} true/false в зависимости от нахождения в избранном */ @@ -20,7 +20,7 @@ export const isInFavoriteProducts = (product: TProduct): boolean => { } /** - * Ф-я добавляет продукт в массив избраных продуктов в session storage + * Ф-я добавляет продукт в массив избранных продуктов в session storage * @param {TProduct} product продукт */ export const addToFavoriteProducts = (product: TProduct): void => { @@ -32,13 +32,14 @@ export const addToFavoriteProducts = (product: TProduct): void => { favoriteProducts.shift() } favoriteProducts.push(product) - console.log(favoriteProducts) + sessionStorage.setItem(SESSION_STORAGE.FAVORITE, JSON.stringify(favoriteProducts)) + window.dispatchEvent(new Event('storage')) } } /** - * Ф-я удаляет продукт из массива избраных продуктов в session storage, если он в нем есть + * Ф-я удаляет продукт из массива избранных продуктов в session storage, если он в нем есть * @param {TProduct} product продукт */ export const removeFromFavoriteProducts = (product: TProduct): void => { @@ -49,6 +50,7 @@ export const removeFromFavoriteProducts = (product: TProduct): void => { favoriteProducts.splice(indexOfProduct(product, favoriteProducts), 1) sessionStorage.setItem(SESSION_STORAGE.FAVORITE, JSON.stringify(favoriteProducts)) + window.dispatchEvent(new Event('storage')) } } @@ -59,3 +61,15 @@ function includesProduct(product: TProduct, favoriteProducts: TProduct[]): boole function indexOfProduct(product: TProduct, favoriteProducts: TProduct[]): number { return favoriteProducts.findIndex(p => p.slug === product.slug) } + +/** + * Функция возвращает список избранных товаров favoriteProducts из текущей сессии session storage. + * + * @return {TProduct[]} - массив продуктов в избранном + */ +export const getFavoriteProductsFromStorage = (): TProduct[] => { + const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]' + const favoriteProducts: TProduct[] = JSON.parse(favoriteProductsStr) as TProduct[] + + return favoriteProducts +} diff --git a/src/entities/Favorite/model/hooks/useFavorite.ts b/src/entities/Favorite/model/hooks/useFavorite.ts index e57aadf6..a22fcbd4 100644 --- a/src/entities/Favorite/model/hooks/useFavorite.ts +++ b/src/entities/Favorite/model/hooks/useFavorite.ts @@ -1,19 +1,28 @@ import { useEffect, useState } from 'react' -import { isInFavoriteProducts } from '../functions/functions' -import { TProduct } from '../types/types' +import { getFavoriteProductsFromStorage } from '../functions/functions' +import type { TProduct } from '../types/types' /** - * Hook для хранения состояния продукта в избранном - * @param {TProduct} product - продукт - * @returns {isLiked, setIsLiked} - состояние нахождения продукта в избранном + * Hook для получения продуктов из избранного + * + * @returns {TProduct[]} состояние favoriteProducts с массивом продуктов в избранном */ -export const useFavorite = (product: TProduct) => { - const [isLiked, setIsLiked] = useState(isInFavoriteProducts(product)) +export const useFavorite = () => { + const [favoriteProducts, setFavoriteProducts] = useState([]) useEffect(() => { - setIsLiked(isInFavoriteProducts(product)) - }, [product]) + setFavoriteProducts(getFavoriteProductsFromStorage()) + window.addEventListener('storage', handleStorage) - return { isLiked, setIsLiked } + return () => { + window.removeEventListener('storage', handleStorage) + } + }, []) + + const handleStorage = () => { + setFavoriteProducts(getFavoriteProductsFromStorage()) + } + + return favoriteProducts } diff --git a/src/entities/Favorite/model/hooks/useWithFavorie.ts b/src/entities/Favorite/model/hooks/useWithFavorie.ts new file mode 100644 index 00000000..b0adafcc --- /dev/null +++ b/src/entities/Favorite/model/hooks/useWithFavorie.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from 'react' + +import { + addToFavoriteProducts, + isInFavoriteProducts, + removeFromFavoriteProducts +} from '../functions/functions' +import type { TProduct } from '../types/types' + +/** + * Hook для добавления/удаления, проверки наличия продукта в избранном + * + * @param {TProduct} product - продукт + * @returns {object} - состояние isLiked нахождения продукта в избранном и функцию handleLike для добавления/удаления в/из избранное + */ +export const useWithFavorite = (product: TProduct) => { + const [isLiked, setIsLiked] = useState(isInFavoriteProducts(product)) + + useEffect(() => { + setIsLiked(isInFavoriteProducts(product)) + }, [product]) + + const handleLike = () => { + if (!isLiked) { + addToFavoriteProducts(product) + setIsLiked(true) + } else { + removeFromFavoriteProducts(product) + setIsLiked(false) + } + } + + return { isLiked, handleLike } +} diff --git a/src/pages/FavoritesPage/FavoritesPage.tsx b/src/pages/FavoritesPage/FavoritesPage.tsx index 9ecc4f33..68e93f94 100644 --- a/src/pages/FavoritesPage/FavoritesPage.tsx +++ b/src/pages/FavoritesPage/FavoritesPage.tsx @@ -1,40 +1,23 @@ -import { useEffect, useState } from 'react' - import WrapperForMainContent from '@/components/WrapperForMainContent/WrapperForMainContent' -import { TProduct } from '@/entities/Favorite/model/types/types' +import { useFavorite } from '@/entities/Favorite/model/hooks/useFavorite' import { ECardView } from '@/shared/model/types/common' import Breadcrumbs from '@/shared/ui/Breadcrumbs/Breadcrumbs' import Heading from '@/shared/ui/Heading/Heading' import { ProductsList } from '@/widgets/ProductsList/ProductsList' import styles from './FavoritesPage.module.scss' -import { getFavoriteProductsFromStorage } from './model/functions/functions' /** * Страница с избранными товарами */ const FavoritesPage = () => { - const [favoriteProducts, setFavoriteProducts] = useState([]) + const favoriteProducts = useFavorite() const links = [ { heading: 'Главная', href: '/' }, { heading: 'Избранные товары', href: '' } ] - useEffect(() => { - setFavoriteProducts(getFavoriteProductsFromStorage()) - window.addEventListener('storage', handleStorage) - - return () => { - window.removeEventListener('storage', handleStorage) - } - }, []) - - const handleStorage = () => { - console.log('хэндал стораджа') - setFavoriteProducts(getFavoriteProductsFromStorage()) - } - return (
diff --git a/src/pages/FavoritesPage/model/functions/functions.ts b/src/pages/FavoritesPage/model/functions/functions.ts deleted file mode 100644 index 92ad6dd9..00000000 --- a/src/pages/FavoritesPage/model/functions/functions.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { TProduct } from '@/entities/Favorite/model/types/types' -import { SESSION_STORAGE } from '@/shared/constants/sessionStorage' - -/** - * Функция возвращает список избранных товаров favoriteProducts из текущей сессии session storage. - */ -export const getFavoriteProductsFromStorage = (): TProduct[] => { - const favoriteProductsStr = sessionStorage.getItem(SESSION_STORAGE.FAVORITE) || '[]' - const favoriteProducts: TProduct[] = JSON.parse(favoriteProductsStr) as TProduct[] - - return favoriteProducts -} diff --git a/src/widgets/Product/Product.tsx b/src/widgets/Product/Product.tsx index fc10e853..119e96c1 100644 --- a/src/widgets/Product/Product.tsx +++ b/src/widgets/Product/Product.tsx @@ -2,7 +2,7 @@ import { type FC, useState } from 'react' import IconCart from '@/assets/icons/IconCart.svg' import { useProductInCart } from '@/entities/CartEntity/model/hooks/cartHooks' -import { useFavorite } from '@/entities/Favorite/model/hooks/useFavorite' +import { useWithFavorite } from '@/entities/Favorite/model/hooks/useWithFavorie' import { CardPreviewHeader } from '@/features/CardPreviewHeader/CardPreviewHeader' import { ProductAvailability } from '@/features/ProductAvailability/ProductAvailability' import { ProductImgCarousel } from '@/features/ProductImgCarousel/ProductImgCarousel' @@ -18,7 +18,7 @@ import { PopupImg } from './ui/PopupImg/PopupImg' * @param {TProductProps} product - информация о выбранном товаре */ export const Product: FC = ({ product }) => { - const { isLiked, handleLike } = useFavorite(product) + const { isLiked, handleLike } = useWithFavorite(product) const [isInCompared, setIsInCompared] = useState(false) const { isInCart, handleAddToCart } = useProductInCart(product.slug, product.id) const [showPopup, setShowPopup] = useState(false) diff --git a/src/widgets/ProductItem/ProductItem.tsx b/src/widgets/ProductItem/ProductItem.tsx index 97f9cc93..280f06ec 100644 --- a/src/widgets/ProductItem/ProductItem.tsx +++ b/src/widgets/ProductItem/ProductItem.tsx @@ -3,7 +3,7 @@ import { type FC, useState } from 'react' import { Link } from 'react-router-dom' import { useProductInCart } from '@/entities/CartEntity/model/hooks/cartHooks' -import { useFavorite } from '@/entities/Favorite/model/hooks/useFavorite' +import { useWithFavorite } from '@/entities/Favorite/model/hooks/useWithFavorie' import { ProductAvailability } from '@/features/ProductAvailability/ProductAvailability' import { WidgetButtonsFunctions } from '@/features/WidgetButtonsFunctions/WidgetButtonsFunctions' import { WidgetButtonsPurchase } from '@/features/WidgetButtonsPurchase/WidgetButtonsPurchase' @@ -69,7 +69,7 @@ export const ProductItem: FC = ({ const [isModalOpen, setIsModalOpen] = useState(false) const [isModalClosing, setIsModalClosing] = useState(false) const { isInCart, handleAddToCart } = useProductInCart(slug, id) - const { isLiked, handleLike } = useFavorite({ + const { isLiked, handleLike } = useWithFavorite({ id, category: '', wb_urls: '', From 5605f87943950ad5fa07e8d82f89911b5c271f40 Mon Sep 17 00:00:00 2001 From: Kseniya Date: Thu, 25 Apr 2024 16:53:40 +0300 Subject: [PATCH 09/66] category-page --- src/entities/CategoryCard/CategoryCard.tsx | 6 ++- .../CategoryCardList/CategoryCardList.tsx | 48 +++---------------- 2 files changed, 12 insertions(+), 42 deletions(-) diff --git a/src/entities/CategoryCard/CategoryCard.tsx b/src/entities/CategoryCard/CategoryCard.tsx index 7c564354..ed33804a 100644 --- a/src/entities/CategoryCard/CategoryCard.tsx +++ b/src/entities/CategoryCard/CategoryCard.tsx @@ -2,6 +2,7 @@ import { FC } from 'react' import Paragraph from '@/shared/ui/Paragraph/Paragraph' +import card1 from '../../assets/images/categoryCards/img-categories-01-210x263.webp' import { TCategory } from '../../models/CategoryModel' import styles from './CategoryCard.module.scss' @@ -15,7 +16,10 @@ export const CategoryCard: FC = ({ category }) => {
{category && ( <> - {category.name} + {/* {category.name} */}{' '} + {/* когда будут картинки в бэке, можно будет воспользоваться этой строчкой */} + {category.name}{' '} + {/* временная заглушка */} {category.name} )} diff --git a/src/widgets/CategoryCardList/CategoryCardList.tsx b/src/widgets/CategoryCardList/CategoryCardList.tsx index f9f28aa4..9b59179d 100644 --- a/src/widgets/CategoryCardList/CategoryCardList.tsx +++ b/src/widgets/CategoryCardList/CategoryCardList.tsx @@ -1,9 +1,9 @@ import { FC } from 'react' +import { useSelector } from 'react-redux' +import { selectCategories } from '@/entities/Category/selectors/categorySelectors' import { CategoryCard } from '@/entities/CategoryCard/CategoryCard' -import { TCategory } from '../../models/CategoryModel' - import styles from './CategoryCardList.module.scss' /** @@ -11,49 +11,15 @@ import styles from './CategoryCardList.module.scss' */ const CategoryCardList: FC = () => { - const category: TCategory = { - id: 1, - name: '', - slug: '', - image: '' - } + const categories = useSelector(selectCategories) return ( -
- +
+ {categories.map(category => ( + + ))}
) } export default CategoryCardList - -// import React, { FC, useEffect } from 'react'; -// import { useDispatch, useSelector } from 'react-redux'; -// import { getCategoryCard } from '../../entities/CategoryCard/getCategoryCard'; -// import { RootState } from '../../entities/CategoryCard/types'; -// import { TCategory } from '../../models/CategoryModel'; -// import { CategoryCard } from '@/entities/CategoryCard/CategoryCard'; -// import styles from './CategoryCardList.module.scss'; - -// const CategoryCardList: FC = () => { -// const dispatch = useDispatch(); - -// // Вызываем вашу thunk-функцию для получения данных с бэка -// useEffect(() => { -// dispatch(getCategoryCard('category')); -// }, [dispatch]); - -// // Получаем данные из Redux store -// const categories: TCategory | undefined = useSelector((state: RootState) => state.categories); - -// return ( -//
-// {/* Проверяем, что данные получены, и отображаем их */} -// {categories && ( -// -// )} -//
-// ); -// }; - -// export default CategoryCardList; From 3ecb7d2445bbc1e6ffe589136aa1b905e5aa2c28 Mon Sep 17 00:00:00 2001 From: Kseniya Date: Thu, 25 Apr 2024 16:56:02 +0300 Subject: [PATCH 10/66] styles --- src/entities/CategoryCard/CategoryCard.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/entities/CategoryCard/CategoryCard.module.scss b/src/entities/CategoryCard/CategoryCard.module.scss index 82ca7080..7e4c55f4 100644 --- a/src/entities/CategoryCard/CategoryCard.module.scss +++ b/src/entities/CategoryCard/CategoryCard.module.scss @@ -24,4 +24,5 @@ margin-bottom: 17px; margin-left: auto; margin-right: auto; + text-align: center; } From b3e256fdee4cf8f03be60c089db4ac950628ea77 Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Thu, 25 Apr 2024 20:54:09 +0300 Subject: [PATCH 11/66] enhancement_333_sidebar_menu --- .../images/sideBarMenu/IconArrowDown.svg | 3 ++ .../CartEdit/ui/CartEdit/CartEdit.module.scss | 4 +- src/pages/FormReturnPage/FormReturnPage.tsx | 18 +++++-- src/widgets/SideBar/index.tsx | 2 + src/widgets/SideBar/model/types/types.ts | 8 +++ src/widgets/SideBar/ui/SideBar.module.scss | 50 +++++++++++++++++ src/widgets/SideBar/ui/SideBar.stories.tsx | 48 +++++++++++++++++ src/widgets/SideBar/ui/SideBar.tsx | 49 +++++++++++++++++ src/widgets/SideBarMenu/index.tsx | 2 + src/widgets/SideBarMenu/model/data/data.ts | 54 +++++++++++++++++++ src/widgets/SideBarMenu/model/types/types.ts | 15 ++++++ .../SideBarMenu/ui/SideBarMenu.module.scss | 41 ++++++++++++++ .../SideBarMenu/ui/SideBarMenu.stories.tsx | 34 ++++++++++++ src/widgets/SideBarMenu/ui/SideBarMenu.tsx | 52 ++++++++++++++++++ 14 files changed, 374 insertions(+), 6 deletions(-) create mode 100644 src/assets/images/sideBarMenu/IconArrowDown.svg create mode 100644 src/widgets/SideBar/index.tsx create mode 100644 src/widgets/SideBar/model/types/types.ts create mode 100644 src/widgets/SideBar/ui/SideBar.module.scss create mode 100644 src/widgets/SideBar/ui/SideBar.stories.tsx create mode 100644 src/widgets/SideBar/ui/SideBar.tsx create mode 100644 src/widgets/SideBarMenu/index.tsx create mode 100644 src/widgets/SideBarMenu/model/data/data.ts create mode 100644 src/widgets/SideBarMenu/model/types/types.ts create mode 100644 src/widgets/SideBarMenu/ui/SideBarMenu.module.scss create mode 100644 src/widgets/SideBarMenu/ui/SideBarMenu.stories.tsx create mode 100644 src/widgets/SideBarMenu/ui/SideBarMenu.tsx diff --git a/src/assets/images/sideBarMenu/IconArrowDown.svg b/src/assets/images/sideBarMenu/IconArrowDown.svg new file mode 100644 index 00000000..65d87109 --- /dev/null +++ b/src/assets/images/sideBarMenu/IconArrowDown.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/features/CartEdit/ui/CartEdit/CartEdit.module.scss b/src/features/CartEdit/ui/CartEdit/CartEdit.module.scss index c1f7cbbf..0829ec83 100644 --- a/src/features/CartEdit/ui/CartEdit/CartEdit.module.scss +++ b/src/features/CartEdit/ui/CartEdit/CartEdit.module.scss @@ -71,7 +71,7 @@ display: flex; align-items: center; justify-content: center; - width: 50px; // bug поменяла 20 на 50 для того,чтобы svg отображался в storybook, при 20px в storybook svg сжимается сильно и пропадает, на сайте padding от
-
Burger menu
+
diff --git a/src/widgets/SideBar/index.tsx b/src/widgets/SideBar/index.tsx new file mode 100644 index 00000000..dcbb04b0 --- /dev/null +++ b/src/widgets/SideBar/index.tsx @@ -0,0 +1,2 @@ +import SideBar from './ui/SideBar' +export default SideBar diff --git a/src/widgets/SideBar/model/types/types.ts b/src/widgets/SideBar/model/types/types.ts new file mode 100644 index 00000000..bb8962ce --- /dev/null +++ b/src/widgets/SideBar/model/types/types.ts @@ -0,0 +1,8 @@ +import { ReactElement } from 'react' + +export interface ISideBar { + title?: string + isVisible?: boolean + onClick?: () => void + children?: ReactElement | JSX.Element | JSX.Element[] +} diff --git a/src/widgets/SideBar/ui/SideBar.module.scss b/src/widgets/SideBar/ui/SideBar.module.scss new file mode 100644 index 00000000..da85054d --- /dev/null +++ b/src/widgets/SideBar/ui/SideBar.module.scss @@ -0,0 +1,50 @@ +@use '@/shared/styles/utils/variables' as var; +@use '@/shared/styles/utils/mixins' as media; + +.sideBar { + display: flex; + flex-direction: column; + min-width: 100%; + border-radius: 5px; + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + background: var.$body-bg; + border-radius: 6px; + padding: 10px 10px 10px 15px; + cursor: pointer; + } + + &__headerText { + color: var.$body-color; + transition: 0.25s; + } + + &__headerArrow { + transition: transform 0.25s; + } + + &__headerArrow_active { + transform: rotate(180deg); + } + + &__header:hover &__headerText { + color: var.$header-color; + } + + &__header:hover &__headerArrow path { + fill: var.$header-color; + } + + &__children { + padding: 15px 0 20px; + transition: 0.5s; + + &_close { + padding: 0; + } + } +} diff --git a/src/widgets/SideBar/ui/SideBar.stories.tsx b/src/widgets/SideBar/ui/SideBar.stories.tsx new file mode 100644 index 00000000..28e5d189 --- /dev/null +++ b/src/widgets/SideBar/ui/SideBar.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import SideBar from './SideBar' + +const meta = { + title: 'widgets/SideBar', + component: SideBar, + tags: ['autodocs'], + args: {} +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = () => { + return ( + + ) +} + +Default.args = {} diff --git a/src/widgets/SideBar/ui/SideBar.tsx b/src/widgets/SideBar/ui/SideBar.tsx new file mode 100644 index 00000000..93aa620f --- /dev/null +++ b/src/widgets/SideBar/ui/SideBar.tsx @@ -0,0 +1,49 @@ +import { useState, type FC } from 'react' + +import ArrowIcon from '@/assets/images/sideBarMenu/IconArrowDown.svg' +import Paragraph from '@/shared/ui/Paragraph/Paragraph' + +import { ISideBar } from '../model/types/types' + +import styles from './SideBar.module.scss' + +/** + * Компонент SideBar - кнопка, раскрывающаяся в бургер меню + * @param {title} string - название разворачивающейся кнопки; + * @param {isVisible} boolean - атрибут дающий видимость иконке стрелочки; + * @param {onClick} function - название разворачивающейся кнопки; * + * @param {children} JSX.Element - контент; + */ + +const SideBar: FC = ({ title, isVisible, onClick, children }) => { + const [isActive, setIsActive] = useState(false) + + const handleClick = () => { + setIsActive(!isActive) + } + + return ( +
+
+ {title} + {isVisible && ( + + )} +
+ {isVisible && ( +
+ {isActive && children} +
+ )} +
+ ) +} + +export default SideBar diff --git a/src/widgets/SideBarMenu/index.tsx b/src/widgets/SideBarMenu/index.tsx new file mode 100644 index 00000000..ea3306bb --- /dev/null +++ b/src/widgets/SideBarMenu/index.tsx @@ -0,0 +1,2 @@ +import SideBarMenu from './ui/SideBarMenu' +export default SideBarMenu diff --git a/src/widgets/SideBarMenu/model/data/data.ts b/src/widgets/SideBarMenu/model/data/data.ts new file mode 100644 index 00000000..1aa6dd15 --- /dev/null +++ b/src/widgets/SideBarMenu/model/data/data.ts @@ -0,0 +1,54 @@ +import { Routes } from '@/shared/config/routerConfig/routes' + +import { IData } from '../types/types' + +const user: IData[] = [ + { + title: 'Мои данные', + routes: [ + { subtitle: 'Личный Кабинет', route: Routes.HOME }, // '/my-account' - данного роута пока нет + { subtitle: 'Изменить контактную информацию', route: Routes.HOME }, // '/edit-account' - данного роута пока нет + { subtitle: 'Изменить свой пароль', route: Routes.HOME }, // '/change-password' - данного роута пока нет + { subtitle: 'Изменить мои адреса', route: Routes.HOME }, // '/address-book' - данного роута пока нет + { subtitle: 'Посмотреть закладки', route: Routes.HOME } // '/wishlist' - данного роута пока нет + ] + } +] + +const noUser: IData[] = [ + { + title: 'Мои данные', + routes: [ + { subtitle: 'Вход', route: Routes.LOGIN }, + { subtitle: 'Регистрация', route: Routes.HOME }, // '/create-account' - данного роута пока нет + { subtitle: 'Забыли пароль?', route: Routes.HOME }, // '/forgot-password' - данного роута пока нет + { subtitle: 'Личный Кабинет', route: Routes.HOME } // '/my-account' - данного роута пока нет + ] + } +] + +const forAll: IData[] = [ + { + title: 'Мои заказы', + routes: [ + { subtitle: 'История заказов', route: Routes.HOME }, // '/order-history' - данного роута пока нет + { subtitle: 'Файлы для скачивания', route: Routes.HOME }, // '/downloads' - данного роута пока нет + { subtitle: 'Бонусные баллы: 0', route: Routes.HOME }, // '/reward-points' - данного роута пока нет + { subtitle: 'Запросы на возврат', route: Routes.HOME }, // '/returns' - данного роута пока нет + { subtitle: 'История транзакций', route: Routes.HOME }, // '/transactions' - данного роута пока нет + { subtitle: 'Периодические платежи', route: Routes.HOME } // '/index.php?route=account/recurring' - данного роута пока нет + ] + }, + { + title: 'Мой партнерский аккаунт', + routes: [{ subtitle: 'Регистрация партнерского аккаунта', route: Routes.HOME }] + }, // '/index.php?route=account/affiliate/add' - данного роута пока нет + { + title: 'Подписка', + routes: [{ subtitle: 'Подписаться или отказаться от рассылки новостей', route: Routes.HOME }] + } // '/newsletter' - данного роута пока нет +] + +export const userData = user.concat(forAll) + +export const noUserData = noUser.concat(forAll) diff --git a/src/widgets/SideBarMenu/model/types/types.ts b/src/widgets/SideBarMenu/model/types/types.ts new file mode 100644 index 00000000..897c778a --- /dev/null +++ b/src/widgets/SideBarMenu/model/types/types.ts @@ -0,0 +1,15 @@ +export interface ISideBarMenu { + user: string + handleLogOut: () => void +} + +export interface IData { + title?: string + routes?: Ilinks[] +} + +interface Ilinks { + subtitle?: string + route?: string + to?: string +} diff --git a/src/widgets/SideBarMenu/ui/SideBarMenu.module.scss b/src/widgets/SideBarMenu/ui/SideBarMenu.module.scss new file mode 100644 index 00000000..aed26e2a --- /dev/null +++ b/src/widgets/SideBarMenu/ui/SideBarMenu.module.scss @@ -0,0 +1,41 @@ +@use '@/shared/styles/utils/variables' as var; + +.sideBar { + display: flex; + flex-direction: column; + gap: 5px; + width: 100%; + max-width: 340px; + background: var.$white; + border-radius: 10px; + padding: 30px; + + &__heading { + padding-bottom: 10px; + } + + &__list { + display: flex; + flex-direction: column; + gap: 5px; + } + + &__sublinks { + display: flex; + flex-direction: column; + gap: 15px; + padding-left: 15px; + list-style: none; + } + + &__sublink { + font-size: 15px; + color: var.$body-color; + cursor: pointer; + transition: 0.25s; + } + + &__sublink:hover { + color: var.$header-color; + } +} diff --git a/src/widgets/SideBarMenu/ui/SideBarMenu.stories.tsx b/src/widgets/SideBarMenu/ui/SideBarMenu.stories.tsx new file mode 100644 index 00000000..9c81739c --- /dev/null +++ b/src/widgets/SideBarMenu/ui/SideBarMenu.stories.tsx @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { useState } from 'react' + +import SideBarMenu from './SideBarMenu' + +const meta = { + title: 'widgets/SideBarMenu', + component: SideBarMenu, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = () => { + const [user, setUser] = useState('Elon Musk') + + const handleLogOut = () => { + setUser('') + } + + return ( +
+ +
+ ) +} + +Default.args = { + user: 'Elon Musk' +} diff --git a/src/widgets/SideBarMenu/ui/SideBarMenu.tsx b/src/widgets/SideBarMenu/ui/SideBarMenu.tsx new file mode 100644 index 00000000..0260439a --- /dev/null +++ b/src/widgets/SideBarMenu/ui/SideBarMenu.tsx @@ -0,0 +1,52 @@ +import { FC } from 'react' + +import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' +import Link from '@/shared/ui/Link/Link' +import SideBar from '@/widgets/SideBar' + +import { userData, noUserData } from '../model/data/data' +import { ISideBarMenu } from '../model/types/types' + +import styles from './SideBarMenu.module.scss' + +/** + * Компонент SideBarMenu раскрывающийся в бургер меню + * @param {user} string - название разворачивающейся кнопки; + * @param {handleLogOut} function - название разворачивающейся кнопки; * + */ + +const SideBarMenu: FC = ({ user, handleLogOut }) => { + const data = user ? userData : noUserData + + return ( +
+ {user && ( + + {user} + + )} + +
    + {data && + data.map((item, index) => ( +
  • + +
      + {item.routes?.map((el, i) => ( +
    • + + {el.subtitle} + +
    • + ))} +
    +
    +
  • + ))} +
+ {user && } +
+ ) +} + +export default SideBarMenu From b159e5574e041c57ddadd3575c6806b2e313c798 Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Fri, 26 Apr 2024 17:38:11 +0300 Subject: [PATCH 12/66] enhancement_306_header_logo_skeleton --- src/shared/ui/logo/Logo.tsx | 14 ++++------- src/shared/ui/logo/logo.module.scss | 7 ------ .../model/skeleton/LogoSkeleton.module.scss | 4 ++++ .../ui/logo/model/skeleton/LogoSkeleton.tsx | 23 +++++++++++++++++++ src/shared/ui/logo/model/types/types.ts | 6 +++++ src/widgets/Footer/Footer.tsx | 14 +++++++++-- src/widgets/Header/Header.tsx | 8 ++++++- 7 files changed, 56 insertions(+), 20 deletions(-) create mode 100644 src/shared/ui/logo/model/skeleton/LogoSkeleton.module.scss create mode 100644 src/shared/ui/logo/model/skeleton/LogoSkeleton.tsx create mode 100644 src/shared/ui/logo/model/types/types.ts diff --git a/src/shared/ui/logo/Logo.tsx b/src/shared/ui/logo/Logo.tsx index 11900fa6..2cb396af 100644 --- a/src/shared/ui/logo/Logo.tsx +++ b/src/shared/ui/logo/Logo.tsx @@ -4,12 +4,8 @@ import { Routes } from '@/shared/config/routerConfig/routes' import Link from '@/shared/ui/Link/Link' import styles from './logo.module.scss' +import { TLogoProps } from './model/types/types' -type TLogoProps = { - image: string - width: string - height: string -} /** * @param {string} image - путь к логотипу на сервере * @param {string} width - ширина логотипа @@ -18,11 +14,9 @@ type TLogoProps = { const Logo: FC = ({ image, width, height }) => { return ( -
- - Logo - -
+ + Logo + ) } diff --git a/src/shared/ui/logo/logo.module.scss b/src/shared/ui/logo/logo.module.scss index a0c9c826..5b014c2f 100644 --- a/src/shared/ui/logo/logo.module.scss +++ b/src/shared/ui/logo/logo.module.scss @@ -1,11 +1,4 @@ -.logo { - width: 138px; - height: 46px; -} - .link { display: flex; - width: 138px; - height: 46px; margin-right: 92px; } diff --git a/src/shared/ui/logo/model/skeleton/LogoSkeleton.module.scss b/src/shared/ui/logo/model/skeleton/LogoSkeleton.module.scss new file mode 100644 index 00000000..6bd6b680 --- /dev/null +++ b/src/shared/ui/logo/model/skeleton/LogoSkeleton.module.scss @@ -0,0 +1,4 @@ +.skeleton { + display: flex; + margin-right: 92px; +} diff --git a/src/shared/ui/logo/model/skeleton/LogoSkeleton.tsx b/src/shared/ui/logo/model/skeleton/LogoSkeleton.tsx new file mode 100644 index 00000000..1e776bb0 --- /dev/null +++ b/src/shared/ui/logo/model/skeleton/LogoSkeleton.tsx @@ -0,0 +1,23 @@ +import { FC } from 'react' +import Skeleton from 'react-loading-skeleton' +import 'react-loading-skeleton/dist/skeleton.css' + +import { TLogoProps } from '../types/types' + +import styles from './LogoSkeleton.module.scss' + +/** + * Компонент LogoSkeleton - заставка, пока не загрузится картинка логотипа + * @param {string} width - ширина скелетона + * @param {string} height - высота скелетона + */ + +const LogoSkeleton: FC = ({ width, height }) => { + return ( +
+ +
+ ) +} + +export default LogoSkeleton diff --git a/src/shared/ui/logo/model/types/types.ts b/src/shared/ui/logo/model/types/types.ts new file mode 100644 index 00000000..f4e43d57 --- /dev/null +++ b/src/shared/ui/logo/model/types/types.ts @@ -0,0 +1,6 @@ +export type TLogoProps = { + image?: string + width: string + height: string + logo?: string +} diff --git a/src/widgets/Footer/Footer.tsx b/src/widgets/Footer/Footer.tsx index 43e1d3a8..cc08be75 100644 --- a/src/widgets/Footer/Footer.tsx +++ b/src/widgets/Footer/Footer.tsx @@ -1,4 +1,6 @@ import { useState, useEffect } from 'react' +import Skeleton from 'react-loading-skeleton' +import 'react-loading-skeleton/dist/skeleton.css' import { useDispatch, useSelector } from 'react-redux' import { AppDispatch } from '@/app/providers/StoreProvider/config/store' @@ -8,6 +10,7 @@ import SubscribeForm from '@/features/SubscribeForm/SubscribeForm' import { Button } from '@/shared/ui/Button/Button' import Link from '@/shared/ui/Link/Link' import Logo from '@/shared/ui/logo/Logo' +import LogoSkeleton from '@/shared/ui/logo/model/skeleton/LogoSkeleton' import Modal from '@/shared/ui/Modal/Modal' import Paragraph from '@/shared/ui/Paragraph/Paragraph' @@ -20,6 +23,7 @@ function Footer() { const coreBaseData = useSelector(getCoreBaseFooterSelector) const [isModalOpen, setIsModalOpen] = useState(false) const [isModalClosing, setIsModalClosing] = useState(false) + const logo = coreBaseData.footer.main_logo.image const changeModalState = () => { setIsModalOpen(!isModalOpen) @@ -45,8 +49,14 @@ function Footer() {
- - {coreBaseData.footer.company_info} + {!logo ? ( + + ) : ( + + )} + + {coreBaseData.footer.company_info || } +
diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 93e316af..d6dc9dc4 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -17,6 +17,7 @@ import CatalogLink from '@/shared/ui/CatalogLink/CatalogLink' import ContextMenuElement from '@/shared/ui/ContextMenuElement/ContextMenuElement' import Link from '@/shared/ui/Link/Link' import Logo from '@/shared/ui/logo/Logo' +import LogoSkeleton from '@/shared/ui/logo/model/skeleton/LogoSkeleton' import Modal from '@/shared/ui/Modal/Modal' import Paragraph from '@/shared/ui/Paragraph/Paragraph' import CatalogNodeItem from '@/widgets/CatalogNodeItem/CatalogNodeItem' @@ -37,6 +38,7 @@ function Header() { const [isModalOpen, setIsModalOpen] = useState(false) const [isModalClosing, setIsModalClosing] = useState(false) const phoneNumber = coreBaseData.header.support.phone_number + const logo = coreBaseData.header.main_logo.image const changeModalState = () => { setIsModalOpen(!isModalOpen) @@ -147,7 +149,11 @@ function Header() {
- + {!logo ? ( + + ) : ( + + )}
From 46895ea6e6687b6676e81786d58750dd95dc920b Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Sat, 27 Apr 2024 15:52:40 +0300 Subject: [PATCH 13/66] enhancement_308_header_categories_skeleton --- .../Category/selectors/categorySelectors.ts | 2 ++ src/entities/Category/slice/categorySlice.ts | 4 +++ src/entities/Category/types/types.ts | 1 + .../model/skeleton/CatalogLinkSkeleton.tsx | 17 ++++++++++ src/widgets/Header/Header.tsx | 31 +++++++++++++------ 5 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 src/shared/ui/CatalogLink/model/skeleton/CatalogLinkSkeleton.tsx diff --git a/src/entities/Category/selectors/categorySelectors.ts b/src/entities/Category/selectors/categorySelectors.ts index 7b939f51..66b7d092 100644 --- a/src/entities/Category/selectors/categorySelectors.ts +++ b/src/entities/Category/selectors/categorySelectors.ts @@ -6,3 +6,5 @@ export const selectDisplayedCategories = (state: RootState) => state.category.di export const selectCategoryId = (state: RootState) => state.categoryId.categoryId export const selectCategorySlug = (state: RootState) => state.categorySlug.categorySlug + +export const getLoading = (state: RootState) => state.category.isLoading diff --git a/src/entities/Category/slice/categorySlice.ts b/src/entities/Category/slice/categorySlice.ts index 60c465e4..0df9227e 100644 --- a/src/entities/Category/slice/categorySlice.ts +++ b/src/entities/Category/slice/categorySlice.ts @@ -8,6 +8,7 @@ import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types' import type { Category, CategorySchema } from '../types/types' const initialState: CategorySchema = { + isLoading: false, categories: [], displayedCategories: [], error: undefined @@ -34,13 +35,16 @@ const categorySlice = createSlice({ extraReducers: builder => { builder .addCase(fetchCategories.pending, state => { + state.isLoading = true state.error = undefined }) .addCase(fetchCategories.fulfilled, (state, action) => { + state.isLoading = false state.categories = action.payload state.displayedCategories = action.payload.filter((c: Category) => c.is_visible_on_main === true) }) .addCase(fetchCategories.rejected, (state, { payload }) => { + state.isLoading = false state.error = rejectedPayloadHandle(payload) }) } diff --git a/src/entities/Category/types/types.ts b/src/entities/Category/types/types.ts index dc802b6d..fd8605e2 100644 --- a/src/entities/Category/types/types.ts +++ b/src/entities/Category/types/types.ts @@ -1,4 +1,5 @@ export interface CategorySchema { + isLoading: boolean categories: Category[] displayedCategories: Category[] error?: string | string[] diff --git a/src/shared/ui/CatalogLink/model/skeleton/CatalogLinkSkeleton.tsx b/src/shared/ui/CatalogLink/model/skeleton/CatalogLinkSkeleton.tsx new file mode 100644 index 00000000..835bdf45 --- /dev/null +++ b/src/shared/ui/CatalogLink/model/skeleton/CatalogLinkSkeleton.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react' +import Skeleton from 'react-loading-skeleton' +import 'react-loading-skeleton/dist/skeleton.css' + +const CatalogLinkSkeleton: FC = () => { + return ( + <> + {Array(4) + .fill(0) + .map((_, i) => ( + + ))} + + ) +} + +export default CatalogLinkSkeleton diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 93e316af..0ea87e2e 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -3,7 +3,11 @@ import { useEffect, useMemo, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { AppDispatch } from '@/app/providers/StoreProvider/config/store' -import { selectCategories, selectDisplayedCategories } from '@/entities/Category/selectors/categorySelectors' +import { + getLoading, + selectCategories, + selectDisplayedCategories +} from '@/entities/Category/selectors/categorySelectors' import { fetchCategories } from '@/entities/Category/slice/categorySlice' import HeaderAccount from '@/entities/HeaderAccount/HeaderAccount' import CallBack from '@/features/CallBack' @@ -14,6 +18,7 @@ import IconCategories from '@/shared/icons/IconCategories.svg' import { linkItems } from '@/shared/mockData/catalogListData' import { headerAccountData } from '@/shared/mockData/headerAccountData' import CatalogLink from '@/shared/ui/CatalogLink/CatalogLink' +import CatalogLinkSkeleton from '@/shared/ui/CatalogLink/model/skeleton/CatalogLinkSkeleton' import ContextMenuElement from '@/shared/ui/ContextMenuElement/ContextMenuElement' import Link from '@/shared/ui/Link/Link' import Logo from '@/shared/ui/logo/Logo' @@ -38,6 +43,8 @@ function Header() { const [isModalClosing, setIsModalClosing] = useState(false) const phoneNumber = coreBaseData.header.support.phone_number + const isCategoriesLoading = useSelector(getLoading) + const changeModalState = () => { setIsModalOpen(!isModalOpen) } @@ -163,15 +170,19 @@ function Header() {
- {displayedCategories.map(category => ( - - {category.name} - - ))} + {isCategoriesLoading ? ( + + ) : ( + displayedCategories.map(category => ( + + {category.name} + + )) + )}
From c52c13291db3e3a823a369ea572dc68163ae605b Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Tue, 30 Apr 2024 14:56:47 +0300 Subject: [PATCH 14/66] fix-1 enhancement_308_header_categories_skeleton --- .../model/skeleton/CatalogLinkSkeleton.tsx | 17 ------------ .../ui/skeleton/CatalogLinkSkeleton.tsx | 26 +++++++++++++++++++ src/widgets/Header/Header.tsx | 10 +++++-- src/widgets/Header/header.module.scss | 5 ++++ 4 files changed, 39 insertions(+), 19 deletions(-) delete mode 100644 src/shared/ui/CatalogLink/model/skeleton/CatalogLinkSkeleton.tsx create mode 100644 src/shared/ui/CatalogLink/ui/skeleton/CatalogLinkSkeleton.tsx diff --git a/src/shared/ui/CatalogLink/model/skeleton/CatalogLinkSkeleton.tsx b/src/shared/ui/CatalogLink/model/skeleton/CatalogLinkSkeleton.tsx deleted file mode 100644 index 835bdf45..00000000 --- a/src/shared/ui/CatalogLink/model/skeleton/CatalogLinkSkeleton.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { FC } from 'react' -import Skeleton from 'react-loading-skeleton' -import 'react-loading-skeleton/dist/skeleton.css' - -const CatalogLinkSkeleton: FC = () => { - return ( - <> - {Array(4) - .fill(0) - .map((_, i) => ( - - ))} - - ) -} - -export default CatalogLinkSkeleton diff --git a/src/shared/ui/CatalogLink/ui/skeleton/CatalogLinkSkeleton.tsx b/src/shared/ui/CatalogLink/ui/skeleton/CatalogLinkSkeleton.tsx new file mode 100644 index 00000000..014d2a44 --- /dev/null +++ b/src/shared/ui/CatalogLink/ui/skeleton/CatalogLinkSkeleton.tsx @@ -0,0 +1,26 @@ +import { FC } from 'react' +import Skeleton from 'react-loading-skeleton' +import 'react-loading-skeleton/dist/skeleton.css' + +interface ICatalogLinkSkeleton { + key: number + width: number + height: number +} + +/** + * Компонент CatalogLinkSkeleton - заставка, пока не загрузятся CatalogLink + * @param {number} key - индекс списка + * @param {number} width - ширина скелетона + * @param {number} height - высота скелетона + */ + +const CatalogLinkSkeleton: FC = ({ key, width, height }) => { + return ( +
  • + +
  • + ) +} + +export default CatalogLinkSkeleton diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 0ea87e2e..f6670b8e 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -18,7 +18,7 @@ import IconCategories from '@/shared/icons/IconCategories.svg' import { linkItems } from '@/shared/mockData/catalogListData' import { headerAccountData } from '@/shared/mockData/headerAccountData' import CatalogLink from '@/shared/ui/CatalogLink/CatalogLink' -import CatalogLinkSkeleton from '@/shared/ui/CatalogLink/model/skeleton/CatalogLinkSkeleton' +import CatalogLinkSkeleton from '@/shared/ui/CatalogLink/ui/skeleton/CatalogLinkSkeleton' import ContextMenuElement from '@/shared/ui/ContextMenuElement/ContextMenuElement' import Link from '@/shared/ui/Link/Link' import Logo from '@/shared/ui/logo/Logo' @@ -171,7 +171,13 @@ function Header() {
    {isCategoriesLoading ? ( - +
      + {Array(4) + .fill(0) + .map((_, i) => ( + + ))} +
    ) : ( displayedCategories.map(category => ( Date: Tue, 30 Apr 2024 18:10:11 +0300 Subject: [PATCH 15/66] fix-1 enhancement_333_sidebar_menu --- src/{widgets => features}/SideBar/index.tsx | 0 .../SideBar/ui/SideBar.module.scss | 0 .../SideBar/ui/SideBar.stories.tsx | 2 +- .../SideBar/ui/SideBar.tsx | 29 +++++++++------ .../sideBarProfileData.ts} | 8 ++--- src/widgets/SideBar/model/types/types.ts | 8 ----- src/widgets/SideBarMenu/model/types/types.ts | 15 -------- src/widgets/SideBarMenu/ui/SideBarMenu.tsx | 35 ++++++++++--------- 8 files changed, 40 insertions(+), 57 deletions(-) rename src/{widgets => features}/SideBar/index.tsx (100%) rename src/{widgets => features}/SideBar/ui/SideBar.module.scss (100%) rename src/{widgets => features}/SideBar/ui/SideBar.stories.tsx (97%) rename src/{widgets => features}/SideBar/ui/SideBar.tsx (62%) rename src/{widgets/SideBarMenu/model/data/data.ts => mockData/sideBarProfileData.ts} (96%) delete mode 100644 src/widgets/SideBar/model/types/types.ts delete mode 100644 src/widgets/SideBarMenu/model/types/types.ts diff --git a/src/widgets/SideBar/index.tsx b/src/features/SideBar/index.tsx similarity index 100% rename from src/widgets/SideBar/index.tsx rename to src/features/SideBar/index.tsx diff --git a/src/widgets/SideBar/ui/SideBar.module.scss b/src/features/SideBar/ui/SideBar.module.scss similarity index 100% rename from src/widgets/SideBar/ui/SideBar.module.scss rename to src/features/SideBar/ui/SideBar.module.scss diff --git a/src/widgets/SideBar/ui/SideBar.stories.tsx b/src/features/SideBar/ui/SideBar.stories.tsx similarity index 97% rename from src/widgets/SideBar/ui/SideBar.stories.tsx rename to src/features/SideBar/ui/SideBar.stories.tsx index 28e5d189..a9489c90 100644 --- a/src/widgets/SideBar/ui/SideBar.stories.tsx +++ b/src/features/SideBar/ui/SideBar.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react' import SideBar from './SideBar' const meta = { - title: 'widgets/SideBar', + title: 'features/SideBar', component: SideBar, tags: ['autodocs'], args: {} diff --git a/src/widgets/SideBar/ui/SideBar.tsx b/src/features/SideBar/ui/SideBar.tsx similarity index 62% rename from src/widgets/SideBar/ui/SideBar.tsx rename to src/features/SideBar/ui/SideBar.tsx index 93aa620f..3354f438 100644 --- a/src/widgets/SideBar/ui/SideBar.tsx +++ b/src/features/SideBar/ui/SideBar.tsx @@ -1,21 +1,28 @@ -import { useState, type FC } from 'react' +import { ReactElement, useState, type FC } from 'react' import ArrowIcon from '@/assets/images/sideBarMenu/IconArrowDown.svg' import Paragraph from '@/shared/ui/Paragraph/Paragraph' -import { ISideBar } from '../model/types/types' - import styles from './SideBar.module.scss' +export interface ISideBar { + key?: number + title?: string + isVisible?: boolean + onClick?: () => void + children?: ReactElement | JSX.Element | JSX.Element[] +} + /** * Компонент SideBar - кнопка, раскрывающаяся в бургер меню - * @param {title} string - название разворачивающейся кнопки; - * @param {isVisible} boolean - атрибут дающий видимость иконке стрелочки; - * @param {onClick} function - название разворачивающейся кнопки; * - * @param {children} JSX.Element - контент; + * @param {number} key - индекс списка; + * @param {string} title - название разворачивающейся кнопки; + * @param {boolean} isVisible - атрибут дающий видимость иконке стрелочки; + * @param {function} onClick - название разворачивающейся кнопки; * + * @param {JSX.Element} children - контент; */ -const SideBar: FC = ({ title, isVisible, onClick, children }) => { +const SideBar: FC = ({ key, title, isVisible, onClick, children }) => { const [isActive, setIsActive] = useState(false) const handleClick = () => { @@ -23,8 +30,8 @@ const SideBar: FC = ({ title, isVisible, onClick, children }) => { } return ( -
    -
    +
  • +
    {title} {isVisible && ( = ({ title, isVisible, onClick, children }) => { {isActive && children}
    )} -
  • + ) } diff --git a/src/widgets/SideBarMenu/model/data/data.ts b/src/mockData/sideBarProfileData.ts similarity index 96% rename from src/widgets/SideBarMenu/model/data/data.ts rename to src/mockData/sideBarProfileData.ts index 1aa6dd15..97ef7cdf 100644 --- a/src/widgets/SideBarMenu/model/data/data.ts +++ b/src/mockData/sideBarProfileData.ts @@ -1,8 +1,6 @@ import { Routes } from '@/shared/config/routerConfig/routes' -import { IData } from '../types/types' - -const user: IData[] = [ +const user = [ { title: 'Мои данные', routes: [ @@ -15,7 +13,7 @@ const user: IData[] = [ } ] -const noUser: IData[] = [ +const noUser = [ { title: 'Мои данные', routes: [ @@ -27,7 +25,7 @@ const noUser: IData[] = [ } ] -const forAll: IData[] = [ +const forAll = [ { title: 'Мои заказы', routes: [ diff --git a/src/widgets/SideBar/model/types/types.ts b/src/widgets/SideBar/model/types/types.ts deleted file mode 100644 index bb8962ce..00000000 --- a/src/widgets/SideBar/model/types/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ReactElement } from 'react' - -export interface ISideBar { - title?: string - isVisible?: boolean - onClick?: () => void - children?: ReactElement | JSX.Element | JSX.Element[] -} diff --git a/src/widgets/SideBarMenu/model/types/types.ts b/src/widgets/SideBarMenu/model/types/types.ts deleted file mode 100644 index 897c778a..00000000 --- a/src/widgets/SideBarMenu/model/types/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface ISideBarMenu { - user: string - handleLogOut: () => void -} - -export interface IData { - title?: string - routes?: Ilinks[] -} - -interface Ilinks { - subtitle?: string - route?: string - to?: string -} diff --git a/src/widgets/SideBarMenu/ui/SideBarMenu.tsx b/src/widgets/SideBarMenu/ui/SideBarMenu.tsx index 0260439a..03d4489f 100644 --- a/src/widgets/SideBarMenu/ui/SideBarMenu.tsx +++ b/src/widgets/SideBarMenu/ui/SideBarMenu.tsx @@ -1,14 +1,17 @@ import { FC } from 'react' +import SideBar from '@/features/SideBar' +import { userData, noUserData } from '@/mockData/sideBarProfileData' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' import Link from '@/shared/ui/Link/Link' -import SideBar from '@/widgets/SideBar' - -import { userData, noUserData } from '../model/data/data' -import { ISideBarMenu } from '../model/types/types' import styles from './SideBarMenu.module.scss' +export interface ISideBarMenu { + user: string + handleLogOut: () => void +} + /** * Компонент SideBarMenu раскрывающийся в бургер меню * @param {user} string - название разворачивающейся кнопки; @@ -29,19 +32,17 @@ const SideBarMenu: FC = ({ user, handleLogOut }) => {
      {data && data.map((item, index) => ( -
    • - -
        - {item.routes?.map((el, i) => ( -
      • - - {el.subtitle} - -
      • - ))} -
      -
      -
    • + +
        + {item.routes?.map((el, i) => ( +
      • + + {el.subtitle} + +
      • + ))} +
      +
      ))}
    {user && } From 1d816e5968f1f9c8c9803f8beee8701b9a8fc5ca Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Tue, 30 Apr 2024 21:05:12 +0300 Subject: [PATCH 16/66] fix-2 enhancement_333_sidebar_menu --- src/features/SideBar/ui/SideBar.tsx | 18 ++++++++++++------ src/widgets/SideBarMenu/ui/SideBarMenu.tsx | 21 +++++++++++++++++---- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/features/SideBar/ui/SideBar.tsx b/src/features/SideBar/ui/SideBar.tsx index 3354f438..c883c6be 100644 --- a/src/features/SideBar/ui/SideBar.tsx +++ b/src/features/SideBar/ui/SideBar.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useState, type FC } from 'react' +import { KeyboardEvent, ReactElement, useState, type FC } from 'react' import ArrowIcon from '@/assets/images/sideBarMenu/IconArrowDown.svg' import Paragraph from '@/shared/ui/Paragraph/Paragraph' @@ -6,7 +6,6 @@ import Paragraph from '@/shared/ui/Paragraph/Paragraph' import styles from './SideBar.module.scss' export interface ISideBar { - key?: number title?: string isVisible?: boolean onClick?: () => void @@ -15,23 +14,30 @@ export interface ISideBar { /** * Компонент SideBar - кнопка, раскрывающаяся в бургер меню - * @param {number} key - индекс списка; * @param {string} title - название разворачивающейся кнопки; * @param {boolean} isVisible - атрибут дающий видимость иконке стрелочки; * @param {function} onClick - название разворачивающейся кнопки; * * @param {JSX.Element} children - контент; */ -const SideBar: FC = ({ key, title, isVisible, onClick, children }) => { +const SideBar: FC = ({ title, isVisible, onClick, children }) => { const [isActive, setIsActive] = useState(false) const handleClick = () => { setIsActive(!isActive) } + const onKeyDown = (e: KeyboardEvent) => { + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault() + e.stopPropagation() + handleClick() + } + } + return ( -
  • -
    +
  • +
    {title} {isVisible && ( = ({ user, handleLogOut }) => { const data = user ? userData : noUserData + const onKeyDown = (e: KeyboardEvent) => { + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault() + e.stopPropagation() + + console.log('Link') + } + } + return (
    {user && ( @@ -29,14 +38,18 @@ const SideBarMenu: FC = ({ user, handleLogOut }) => { )} -
      +
        {data && data.map((item, index) => ( -
          +
            {item.routes?.map((el, i) => (
          • - + {el.subtitle}
          • From 468a6535eb695882379203f2565eaa27ced81118 Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Tue, 30 Apr 2024 23:30:25 +0300 Subject: [PATCH 17/66] fix-3 enhancement_333_sidebar_menu --- src/features/SideBar/ui/SideBar.tsx | 2 +- src/widgets/SideBarMenu/ui/SideBarMenu.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/SideBar/ui/SideBar.tsx b/src/features/SideBar/ui/SideBar.tsx index c883c6be..49a6ce60 100644 --- a/src/features/SideBar/ui/SideBar.tsx +++ b/src/features/SideBar/ui/SideBar.tsx @@ -41,7 +41,7 @@ const SideBar: FC = ({ title, isVisible, onClick, children }) => { {title} {isVisible && ( )}
    diff --git a/src/widgets/SideBarMenu/ui/SideBarMenu.tsx b/src/widgets/SideBarMenu/ui/SideBarMenu.tsx index e52d00e0..1dbcf895 100644 --- a/src/widgets/SideBarMenu/ui/SideBarMenu.tsx +++ b/src/widgets/SideBarMenu/ui/SideBarMenu.tsx @@ -24,7 +24,6 @@ const SideBarMenu: FC = ({ user, handleLogOut }) => { const onKeyDown = (e: KeyboardEvent) => { if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() - e.stopPropagation() console.log('Link') } From f1e13cb8c99b23f109530066776232079ac212a9 Mon Sep 17 00:00:00 2001 From: Kseniya Date: Thu, 2 May 2024 15:01:52 +0300 Subject: [PATCH 18/66] enhancement/categoryCard, categorySlice --- .../categoryCards/placeholder-1200x800.png | Bin 0 -> 194114 bytes src/entities/CategoryCard/CategoryCard.tsx | 7 ++----- src/entities/CategoryCard/categorySlice.tsx | 6 +----- 3 files changed, 3 insertions(+), 10 deletions(-) create mode 100644 src/assets/images/categoryCards/placeholder-1200x800.png diff --git a/src/assets/images/categoryCards/placeholder-1200x800.png b/src/assets/images/categoryCards/placeholder-1200x800.png new file mode 100644 index 0000000000000000000000000000000000000000..ad23c7749e0639f127cd25f4b77447e570f20e9f GIT binary patch literal 194114 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

    ;4iQ9Yj4|U}vfMA5G2%Rtu$+{_CWScZ426H;1 z(s88ShBIQi)KsZ#t8t=Plvj#k4Z{i+b0Sd@D_6I2E3z$gSH2F_5vl#YS2`Mnl>hKK z>7J<#G5V|)*|`*R<9SoL2RnVV;XI|MAi%|-dWYKMS#1XMpRRlQ#zMxe`nTtlvSM|u zuI(8a9PqJm%HERFx@#(tk6EGQnRzYQbZLvTB-z<3A{l2xC9!db;$*q)c$i z_L#)Eg9yZ1MHf~>l}sinZ@Si}<3fo8*A7`9%KCqe_CiuHM+8&3h<1Otbd6HPik|#Y zI!!*}zeKpZ%tFU@M{B#iz}C0>pOjtB5SJEf)Cx{@*JIJhYs=K+@xZ9u;}sc>Urxm8 zu)fJdvE9_1)zs7wK!;(ID0)}p2W&{@wXIb|Nc+T#%5;5E5R3!2&s{EL(4K`r8YH?b zi0IR|6XoMkvTQweMFH2gWs8*@nUaQiv&xhLcV27uI`u6u@H3s$IIBLp^@O5aNtZ(<2yG=*evj_N&wMQD<8X#ERaYs5t@P^?Wx(;DYL<0!e?k zuhUU)aac#!C@wm(ay^Gs>l3plB;!kwL)7*8!P{Riht0C7M_ED-)+%=8Hu37)oZRI_w6500dl`xwvn z;6kisgvzJlJG;~`RSD0EBV&cajbP%R!v`Y5ALwMWA|=TQ&Bu#Sgi@bq4?Hsdt%;j* zk|`)Xinb=dGG~bzKq7*y)5^c(cx(~(Iu3IY%WQ>^+IHwz*vP+kB2M!SLv3H16CKCC zj*iLjaf@X0k+b@_8wS+ukWxO3Mhx~G4=^VL4pIC+~e60((X>eEW#b28<^mY|3bde*CVHB>xg6C3iV#$5jO zIgyruOREu+P^5M%$2_^nh;`5XZ}QoSidq504_-!k*jr+n9v-baN(kj=_7&eg3B!+1 z5+6QoPp|Gj%aHgE!e4R{S~B*8acjh@k%yNM-9k2(sy;TqNWm~Tv#CI8%(~G+D+q(r zTf-!pyBUw%ib^$yJi6k21;FdE45ZGp;5<{?CCXRynP~_yF&)*x&FKbes#|y-P6qWs z%dR7xqvcyaHtxrW-#=10AmRf{Gtw{Dc0}eP6j+^Yyy9>8e~7+QsR&yuM|cLeX0CWm z_jE81fZR#XaWFoXUO?ZyJ@{Tl_e*+CM%N}`&3zes5o~vwt+yS#pXYvYz|xuD;6N4; z(s!_k{UA%M&M{}(Rnagb2n=V3=NiXCg&!4+21yiwV6@zJ@=ht=ks_!QxkkZZP=9<2 zWby(LYpxNEB*gBCDXa5*()*=HvkFsxzw}E_{YO*-R72^Kj^?U%fud*HbH6V|osFgq zVhBPRd|2wUSp@3(se@^ewLHGw{wvWoI4HDTW?z$)vf8U%KOg+-1`OtO-YbVA&wb*9 zPQS0HSk&X+-mPGIy7GYEAAZ~4VDXiiJ1@ZAnw|GI2uVI>Es|akifc$vUOSbIR>nAn z=6|_6lOHLfi7r$?*uwBg2BGF1VYffs5Q7_0a}<^W^f@b6rDV@rUN6edtfBF;^8{i06>A11*<|5_7JN)6Z3CdmtxAuu8BeiVo2W%ST zSwWhuJ)G~D@x8p4DPidoyN&QJd6r`^=alMBx8M3f2J7j$BXXq(q1V$*0AAOtLt|C8 z8x_@0lRxI%>_&bOSB51i%Z`GYY96a9yYGHkWtw~n%3)j6UBP0@-SVzQCSQmOS&7%9 zyPeNF`3d#aO&&{_g_+cY*)A!2ey~|1R)<7x=U`-tPkccY*)A z!2hejKOR(-@k0ynyV(C-?EmiO|L*4h?&kmQ=Kt>I|L*4h?&kmQ=Kt>I|L*4h?&kmQ z=Kt>I|L*4h?&kmQ=Kq%K|Ca0j|4y#|uA*|AmwJj$LhGgOGMlGgpX>7-=Ib_pntz~S z8p@Y~l2)C--l;H3ejgUBWAWrcUf{szH{uUvY~^8ilikqR%Z!N+MKCEMw~wM#=KNV@}s~#1JBK%L7i#uBdh9+)8nI9V6I#jPYLag zTsb`SL%u&{z9HY|ql@5Syg2%1+tY`RwY71V?Hd`z;!XsIi>175vVOh)e*3H*-;+MA zX}XiziWW@m0i7rHUYrx{6<U zhi6#hi$5xe1+9MvyMJJ@Vag#uSvP#(C7){E`6ct(3X>gW!gsLx6z6BLqA955qmCH0 zcBj&GbxYrdvwb^I7{sjqdO~reJIV&0&#ztoTw0a~voZAXhgq7M;ySY&`VcO}XviXW zhFZ^NsRcdLsUAH;)g0+#e0jY~^SG{GVHZv&dmW;2V0F;a)xf{Vb}3D5{r%MalrI%F z&>3Q*oz(TQw}A<><)lMllV3Hi?UoUG*dIO6r_7RJzZduJZT${|%N%?S*XP6btE_n@ zhjMaGjw(+Mgnn1~@*IEAI5XBP8bYr-EF4|^MmJeP{93nnhor`E8M<0C>3RvCgP&FR zj8nN!3eszM?{nf)cOt~^0g(3dJ(}}uD%>3CGVB)no6OS@=9u154 zZTS38{U2T+lP{uz_3ftq?=_ytM;vxA)ad=3V{k}*A-w#%^0UeslKoGyzsrqf2|`u9 zJ>yCAxATkfB`iJlZ7tU-qiqeHo?iotwd3@?`V$cQ74Hfv7CdRi`*?qme z7i(-vUVIHW^AwHq)SX(C4-5#%o?T8U#z1SVBxd|k&lgDn%I~QJWKsyS@kAniA5lNs zAyvE-<<%g5FR%0ZqlzKCBwzpREEMZn=~OuGGBN<`rx4X^V?v7dWUYh(+`&iJn zftQF7P2mXPFi~U;B>o~ zGpepGyYspib%j8Ps~8f$r0rBJ&Hk4vHTW6Nj$Ak^ooOT9r`F=?K}AX2M?&UIo4n!I zZZ_`3XF0ee0w3L;&yNHh`>oFJ&yMkV`9<(f*qYVA-LqPsUt7c9FQw6M$UbYOTMl?t z1*KD;c1^)F`lqHGR@c|p_nRd2R?kAaqI_LTiehKF_2o@&EbE;$Gu z{h|G;sqk%!)QfpKgRpmLsmiJ+9DvwUTN19R!B0uu_i~r3rkxJmo^XL6bHAK*W3K0W zO_*f5ic40?O`=SM%2-8nEY&W$oT-6@XG0&ETr&f5TNP+?$gSAWhw6DK!OKPqBHHHU zZwvfKqg%Z}M1UvUI=-MJG+`E6rab(7`>o-{&?6|f6iXr^&K0NN4Mh}oF1XgwtjX$ zo&EY_jFOVlT#qTv%`7K=uIKRi^XF~R-D6F{>`kis5O>2{<>$dOrwR>#|F4XT8BZbL zl*M$~Bf(%WOGIMg=;-JiCTm=0oo(aAFKf`7HbzR{UlKViH@Ki-iOlX*7%p(J3faox z$D^sE>v9#PS8*XX=VItn+Cjae1c40Q^c@=_PKvZM{L{!#jJE~)N#r(c!h?m3R6 z4#B5}xl1HcGI*QlkM30se8)9C;_c&O%}ght`{>8-Xrxbh36nX(-YhQ!Jh7DufWh=j zN=j&NaKtr9HZ{^bO?HQYl*Bui_2f;7^Y>t4F{d|tmdV6o(AM27F8vhdiw=uhx{c(j zi~GZV&icV6z)H92L`M2`Y>^1#qz zLq58V;YW%ArQo)3j%0($O$DyrwY4?!N>LQ}UXScuVLV4EodhG*OanX&ToFfdk~Kl5 z^NMb1r~3v@tiPXMHPvvS(rs)GJ{S|lD-}f>qHVO@jJzF@w}rbrNQ8yIlALhUmPmyd zzU*w-SiV>Uu^ZbF9-yua>nPGKS=JBV+q-e2HWQ!SJqgwESiXqZyc!Nz-}O8qI;~&k zX5S>EO^oLS+n6$AG=E8GopPx2{K@kcJ7)&d-V}kz45E@(Y&8l!cfU=~^(A)xDZ-{# zRo!h!72cM_$748<015^JlCnM~<_HR>QfXTi2oy4Gj&s z;N(jr>FWuPP|@b^ZM9r+-sh{Ao+K#6>pY8mBSWs5v_S{NwD&ygN-?G0cb-RUWoj;I z7f1lz}Y~7sVVKLucnJFb;v`v%M3Q=bqq%SS}`2y0N_+@o-?4Ivzsd3FfvG@#R z5W!_yBl{6kpTuLnzlepk?bDv4W{!}kG7xVoOC7j4$~Oy-MZ^y+zm|A#=ECTW#%(|Zm7r^0ltj^p3q4WNgiBQ!|Y+L<5J{x(kb#{Iu>DQk(O zrSemkwQaswGxCe!^%l_=8dQu`(B~wblbIhy^nK2e79*XX2EyAM-wr^s~IO?>VZ*5i#K%N3*HzpV-(*U zG1Klme@UQE3A&7C+n6kMzS-fD#h8MV>E4(rkEq)vJ4I-HZldrwWfZqE32;;IO#JBB zSZ_49GMe_YwY4?rp5VQxtvF?oX{b4qLst`4?-5}@qOWL5aM$XvTE1UPCT20a{{v4~ zo2hegWJTkJ6^%%4aoOo39hR;^lSx#Br(VYT`1rUI7Ydx8-Wl)c&m6FK#3;)jgG#Wd z<&8ll8rU+G-f_&>7aJadRlD`LdOgw6xUX?}i^TQ>0vw+*+GQ$lnmnww&j5o(3R;V% z4!qV&5?UYlySs!joR;?;L%~S}xkoYc#euBsv9uo99hqaR>H6w}d#^NT_*V?_SIF&@ z+hOk2NVqw1&0gA%(rrCt=t{ouC3bF&XpUAQ0*OE-q z2l(flGMsXb`mI%Y&WBV2&sl=TXA3+`DD-P$qrVe*Y`=K4@FkwH8?q~yIOb4wke_Gf z;Z)cv5pPM$#L*YMt;*?FXk)GBCl|a~k00A+C2hjEa)vW-y9UhuU3k430SvOE3;0YY z_L=QW35D5fb+`@E=9w+|GGl(8W$%eA!a!=kSmT4dyY-(%ts^FdsaP{EcD;qqQ(fsR z7%#o5g@QT>PioJ?EB(l)vPym(>a!0fDs56ZUZ5-=XQE=)(TiJ3-|FCttS!s5+qH|s zCWRNBS0-Xsa^s^aD&rj<5|zN)2QN0`etMW1e5GECqA2c~He>(u#B<;&x9>x*h1RD; zq8zUtyrn2GLSV8!?zmxdaJh$ObHJfiLJA8H!8#P#U^fR~id?H*q*cr7c5IHm#hNuv zT2VeA!gpTm1!K=KbB{m6!@M2gC{R6#L@yATuW!TqHQTyVLn2ZZ);I$ zI`2jC19qw$4^`*o2R58BzU?Utw(L3dMCYTEsDv}q$dyTgWu~stPRzEd1DD$W#pFbv zHE0&b7%X^a_%*iba^zuSF4v5k0F2TO5p@ktm84ZD9XL8_*y?md5CmI@L~$>yeIC3X z{b97o7yBhZDHl{F#I?ZHp)pD#WwwDLiuXC|Yq%c-;z1~!%ll_6`cm)x)FSzOZ|;)W zgh|l$sV-$|9W|%%ThU`4fQ>1y_j7Y|Z{#b00|TiP#lSV9c+OIiy9wnRj5b7H82K2< z6#qhm-N2*?^f}3wxmU(+o8VjP*SN z(bgt$aP>urx47{6dRxM*T_u$h>Cm3x;^HEOeOI;8gQzS9nUxpFudWC8 zJC0)S)hyR(94X16kU~IFNViw#@D8!Tl{KfXaxPg;1Q60(`7C^BS)jCc# zX=3}Uf=+ygmD_>Y9PRu6G|u8YA9gk{FOo>Y^|Ects@_L5^Qz5??%gVgQCZx)T-#&t z27!mJ?mqeg#lkspcu=ns*&}=Cb6Va))|(wq?fOq#gE=(5c%nYxwRDS;J?rdc+Be0~ zSbHy@ExeRxPs_8FBeNSLprCqfEOOj!nEe%TkR5q%?t~XfyeB*F?(9~pT_R{VaSOVG ze3zYmGHr^g4+_ctXF4MVyrojcsAs_yKE}@qPB=Mq1l)o(Bd)eV(}8*Z^tMh;F z$e2cBdKxpUY31PUw(0@vHupRT9IrMd))jK}3 z_h#Xjd2MI|!}@$_5W1Wf?V1-8+)J`_c(KE?BBo0R<7f^6HU~@jLaWYnD|oXU7vE*H zF)*sHaUkxeVPVhM5&xo`p`E>VzZm=3Ea%vrBc)2xP$%FQ0)v78ue9FbW@|J(|Hd(s z4a+{``$tZTA!m9WF5{*K!Lr0*PyeX+xt>v-pc%j0AFgE+0URIX$MIt{7UTVpFweHN zi+b8amhGpsU|tO_nyrn1r=pG@lt;&_uRSgNw+!bmelMIq#r%ymvu@?O8vCxzabok0 zabikw=;Sex{m$t~e+wqNW?&tItie_E(039zHfKsTZMLU+k?eX!xXu~O6GC~$Si!A8 zqe@O6sb7EWAsjyybUciI`t;`o{U*q17~6npN_?eHMpRu(7>Kcp-1iTuhdR48@9@*A zF!pjm?73fa%z53hRCUT+=3&pMbTHz{E!^?Km6a851+IA`^ERYWI+Pw9Jjn5uVCh45 zN4*L+Jcj^-N2^{=$2RqvTJ^EjynctP)OeJ+w|J+!a9%tWidhP(XfJKg#!u`13%a{A zdrO#%omW;?-hii6k}~z!=9SY-d2d3()Vfx3u5%bHX)24m3}8p2zXdMeDA&F*Z>r#; zWxFPtl}9*~)QSK#3uOvUIYzq|QT6pf!d+MmreGdNdwkd3y~~zvc4r2OJ{xW$bqb1# zfnmjZGbIg8%QQ@00w;?C0&JsTSpgTFVJ<7|Dfp{`D%*37u3Ixk=$|zQn+vOJhI#m% z#P1ibAZfA`3^@N+dGGFcIPF&K9?i^>{${mTk+22-FKEU@f|vSC);-; zhRZ05${QAg!cWDa*_J{5bwssZn$uomCe3q8T3~FgX2-Fg6KBy&_z&MDJ4tuvV_z;N zd>_R>(xybCwKmA*^)j9gJ|jI__FD&LOavo&=WIKZjEB)J#XJo6@1$Og58UVXlsUN} z!x0lVdAzU7Q^E(-mVkc)pWQ7F+t+IkkFFg)+5dsv?#55Z@1PpZq$55ZdHhI9!xI&E zzx-#73C9}6ZxELE#okvh!xZysihHmMufmg`C*+1*GwnN2#NHVVY`I_@Z+Vf`0^S}s zwTGaLOT?SsJ-J}!ob1z6C(CYI#ygGEeTtCLf`EKR-oHIDP=Y;y6q-;ugirFrr@zbgoVB+E5Spu`MF$!&mf9%x30b3wlV}lxjI9|Fv96mM!>Xy=aLFiR?iU-~9W4rk5J@_(-76t5;Ck6s=U*uJd zR;`*wl#e{wY&5ITZ<#$9n)cl<<}5qG%@(l#wx4f5OxmC57;M9A{Wy`VNbNA%kE4^P zz9|KPD#)ZzIaH$j$xtrWlR|hLb=X)#FucD@k@F_*$c*=y5h}PEO}hlE7ku71v|Z}T z6;#@Jm(=@%si7AO7C2{g=l*1Fypg8yB@-nFU>^D8RTF--w{F_1s||!1fl5?yv;A%EFKahJ#-m5y?<~V1Zx=6!lvd7B7_OQ|R^EW# z=q7S?aCP<$alMtxCdZ7)L2UHa$urAI`W?ZbMyX~}kcSAv>#Zaj9=!9mxj=MVLMptc zVi$DvI;PrJd2!F|mxAtZZ+!Bn@8B!+z;!6!h5^}SqY0g00Cd*WZbbFyT9|i4jGVJM zk-LO(^TV&KEv?Nr> z5M#0v$y6ic08XGktix8TM)&5|zSu;keooz_&yPONvCgE$txp!q#iPeIf$VrBB+lns z`8O}nE&yLnQREYeMCyQ^#mt%vlhpaFvjZG$x+wCfSJ;*9gzSZ{&PTsjj=ab zREfMTIq6iJ-1Z?VZ6)yzQM)9t^JXZ*Ev%|wSySXVdK?IZ zSljE`wIJjP>itU;JnD#tCO%d^Y|^sBD~c0d2FHB^;ND^@^k?=6C0Vp-lNl_a5Z{{e zEGNv;v8KWiO_&HUB|9p1YtqZ{qC<`u-0txaLGkgffWsj77TL__T2^FzKDZWsI$L-C z!lQx#C{M&n3gg=N3+yHn%%q`IBJ)8>ew0wq8hW0Bo6aeT6yEC@Qp%-(H>HIdirLU zS?vAJ!l{5rH~Si)01wRZ**LiJ*P%+c=ifr(m5HaWy_o8aOS{|XIsJpfr(;vy|2$7c zeet3vrL0$viG^mtN6+RI%JuY#j@c!=>py&CCd+P^cVo z5VeE(nAf-=sZizCx5BnlvvnA*w`0z>UL(A~T6hPlfdv8Uf}Tp;w4Lgms;Y>POo$#$ zw^GVizF;&3+8(q_LnQ9#vPTf~A!%OThAq~T)Mqd@s#Epog2TDz{*$f4d<^&P_S&^R z{_Z>*j9@b7g0f21eErxgB056QhrNP44I47h_=03itSHyT$1<{PR*<^&eqa9SMaHuA zG9bt{tjNx?**CPsn&3$B#x#zd+by@-Lx(j}y4&MGwIT5ekWqjpCat?|V`I~-DR|3} z!%YF$RMs)MxBrJ#VQ__14$cp_NdsnJCFp6^<0`*Q_St&P&#xmn?Xe(JQ+WcNIy%pH z?|EDL*g4}66Z-P*pM5NAzis+hD#Z3|co}Bk_ww;YPDIDQ|A(uU@mbI1%m@h0&CZMU zY_{U`T=72*%^JaHH%|_b$Z6nv2?(Lbw%-FPrpf! z6zA)iVn^ayfU~81ap`x1yE(VH8sKG%ypP!MHdI7~%1G6;NHP^ragr5I-llwPE_fEJ;VjNrPJ+vAAJ@%-8Cc=F1I+i|noh6`GGwpGB0Yvs~+5*wgN zz`QinT|X@sD@VQ83@(i&Tk5jy%-YKQg5^oomwcYoQBaOHoO=u(4m7l5VJ8LFl9w@7 zn~ezP1Ib9;Lm>L;FS49+`qNa+SjQ*Xuq5Z6gO^|TS6As9>E)IG8$&$>qZ}(+@OD6N zAb?crGb8ib)E42$jF234g_HMUd}x2G^>)AgIFbTQu$j*zQ)c>cwK6^4^-vK8s-3;`PIFxiqFFb_icG29*b$ z_A#ic=5$I;#`tm1rz5VnY(FF_p)oMMm_55YnW2lXnttiihKLjfmk6eec?uzRY2K$+ z#Djeqqn9N6sj3UeM`l0=V57O*A;}qGZv)-tcJ5CKzpQH9rWO(wrv zUNcwsBvKJB9D=lJoXx-*ocJKU(eYQ5$ZbJbX*&fF?Oj;6rsdS=Bi zaP#JGmHBS!+ePlQuI%Tk9*EiWbDpAq#7i!VY0ttZW+j4Bvwms$z}*7Lux{WNQTbCm)IFi+WbvoQ*4u(?mnkwI+{$l2x9 z2$jAFkaE_dD{|xKf)YyR=~Z7UlyzT>IxC#B7G2YTLM?ks<}A$$Impk&+f6O|8vv~s z=s!P-Y-+!0Im;(aYrnDmhl}(`m^RHt+S{WIl;M{D?kH;Y<^2y=Np@vW`vBCv##{;B zN`%O2s`%kqii(XKn$NS_F&Rh~fZ~i@syJk|OHqFxTo5M8tT>O+&{p|9|?}PG60NZg?7QWPetzjqLx3mi@EM{%K`;nQ`8D zLY?g;AfXz>*Z{JFlb#-)dx8|sAZ2?14eH@-02Q&63U2!qipc}A1hmX4Nj4Yb8kGOl9Mf8u-6zN0bCX{H9qBrF8j}^h z7KQv60DW)|S73E5_tBrBWfy=+ooYv($5Z%ydmW49AF}wZccyL!%uK`wnVsBsX$<9m zn5*jX4w=#nHg^eE@|DtPb7_dm|7fV*x)$CA^qHW^3QFr!N29f+xKa1}((CH`hV3j~ zVQ z?V^!1(rRdENRrM}7-FOvadO#9g2|j@(iA@_SXPw(gvXbD@`TUBx8DtT%}Y|TJ%*IFq$@3!Eee{+d3c^C^ExsUZ!1?3QkEakISw5?eY|z_ zr0qi|fLm1E{?q-v|L%tSyP2xO$B+NT{ry<=cL#8;uBIt})F_oB@u0Ou=CX3M>}=Pq$DeLt2Pd{S>1AbPLs_ zzb*g6EcEfjbb;}!_MWZAa8)^m3o&btI9G1fTGMJ(aED*DxmKqldqP^dsB;@r7hkH~ zf4Ii04n2;0uP-ZY%l6Wz_UHstO0Ma*vdL(9V=*RkoGpJ}@u~lwivMRjc{fd`?s=2d zy*W>Gi@8%|vl{;qfc`PL9zq0Pnrt-Z|ce+IHqyD3{J#rcl;md$y8MeKNDDGW5%tr6^ z{6AghpPlvNi=plx0CYIO3P9W&@k0I;;~%6&pb`ez>&2FX^o^^+asy>ApC*rAKVa>c zn92a^OMuSTsIq=NmLXJo?cgzeV-?e_)PY5uy9T9NjD8;;+id zqnSrU(39JHL3x+dp?TzP_kuBNhh?ToJ0&gaC2j5?;-Ytxka$o|-VQuzJTS<}(iNB{ z$BuJsE=Sp69Mm~^&^g))s9Y2jEg4kh(X(aS_j1% z?e{>nrKqTw(}DA35AlE0T5f#0YYJ3aw0}<~qbgHraUNs5YpZ_9h7tep+}RBy{+y%o zqS4Fk50*q89JGH0_AX`g&>8PVJnetUkN6 zgSL*x(dgO(R6=Ol3YMQ_iH8;gBzIen!cwBK~+Uy?6o#2S;P$-LhA> zyYs)hxo;wdp7ud7MNv64LOrb+;cEgd=#;9SpYW{C1{%sSa~0X)mdh{hT@};0EA>D5 z{6{E&9S>|_|8>6y`jf@4c09afGXZIkCKpgj@1&etcu*vuvDUw6EF#CPwLq+?PhFCsjG_rm`I?D5g z5?A!sBc~XGsog(seJRD`RY0(PeLV4Ih!kK5r;w%s?=)+k#BnR8b!bfc)aKB*8BhXB zmN26=SFG0e1YbUrt&>AzM2JKms!M*giX2_XJqHRjg!YfgD|G-vep;c>=6fWXd$s?* zN$^W>Ib;5JiF&|1p8v6D-*f4mFXt=~N;(jEwj?`K zVMmTF6tS{x0`D0$w;keOmWy1yuH?IVzI zkR`(tFcTW0dz=>ZTZP3Nu66Zmu5ne-R#`J0or{6E`2bpHq5S$vn6rfJz^ij?r5P=3*NtHb%5W9R8BmFveH1C zy6Q@@MTWfr#b2`Lx#O+jG|>y~PvX&JX0v!ZgZE}p$#&6jQa|3x=deZ1;0-ir{;W1o zSE$c7dbbah*4Q@r!GA8?(5qe9;KmQ{8JhZ1^uJiNv)A?ATk@>;nLvw|PCmxU8GbPV zZE7)5gE*j{pDc4Uv5{Nj`3G)^T`wZaV2+}}#)9mc^Umb}b`@C4K4|w1#{_A=V8Y$TGmV>s|r}~}*n#=pvKj{$vKUN_U^FTR5 z`_8JZ>YpUFY>R$X9EQVIbw-NXhiFhluhxzjl8Ypw-L;~tJ$4DdUi(f5W<`95{Dy~U3O0*tif_zK6*i`REuAz_(sS0Uz#4TYuRsG6!g2cP3&HWR|d)!EEwriVOX#0Dg`2Xv| zp=GuCDe!5~1hFM1YV1yJF%ky>h+?(Z%=~)*r;rXRNY5xW!kGUZZqyOa+!fx5; z*^6U7>ZQ>zHOwm(u591lqaVHWwG@ca+{$3>_l% z6Eg47toy#s`+m$DP=I=w0Wt6TlDaB1E#o3h_yGAHuz&pUzpa-eNEEOe|5IM5pa%* zpy6kmDd?HT!$7x3)?v{6A*VvM@G7O@r`bw!#v?tH}DCyjj1 z3=1t=i*aGOw7*rGzs%Y{xa`WzknA5T(`F#kA7YGGPgZm*xpi{qJ{MD_ZXfi#!5xvG zMv|&z2>v*&XcO7_gTrGIb~VnqSMBvE@vl`nWoi@E7?;zqYpVA*M@iz`xc=+zH2)Io z(G`n20Mb7BW~26JNA?WtP>pOL)vS?i0s^}^w$+VqFT2A#2q7{|ukiloKE-D)=X9q8 zIrD9>ljcE@?hRw`x9P1s?~eh@l5*+(&7yS?Uy2=2ZWa9ku*5&w?bLJooX@m?#-GcV zgml5;ErYU+rEAvv5`Qe->Wi{MP17@NF5LSyAXl7#eFORpO0Dq!q3q2Aq3*jkV2My7 zl6}dZY!O0+vM(X~KKATeC}S)MA^Tp|vSyv^lC6}n#MsAPc4iXBzP}%L_kBP2?|Gl+ z_x|4b+f2=TKj(9y+W@?RfUG!U zeRt8e!uw&9-4$>AGTV6x?Ej!q3SB;pB`tjbNM=`H79s{cTgv$#>dtya8;5HeqHwkK z?xlM4nPo75PU+9v6ny+oTHP2;?bm5CDTw7!G@>8jXej-s5LM^=|zDD4A!4AJBQTvmF%JvJ`ZW|8x24{5B=OX=eA?>fc<;!;Gf?%ktNU ziXvxCh#MOl3*J|GC6f=G72wzRtL=mNtZcu7itSGZ%~>XiO(@S=Q#XuS;l-#4Y#D}t zB(x0w3EVGhYdDD*s1FSp{$32LnLVeU6!)*iuo3dk3m#lbmcWf^6k!0mceO0`H$hv* zsh*K5nG}HzWYgM}f}l+()x(MJ&rL#~e5XRvbsa2!{TG~~9u9{&`zzAC9LEDmfrYno+w`5!k8*)!-2 zz&Z=QX2pnl$(1n{_8nYzn0%4zUG`J$B$p9974wtlvwg>||4T`Vd#?6J1ccP$$^Y{f z|DRiiC5`03rdi0Hc=_>Q4f%gtG>cej*q848xOJM{gq6)+kKeQ}OB4nuCFE2$;v04AFxOpsc-mz=h%dnO`B#prpM}R-s(1ggN?Z zn}1+}gK|sjHIoT&uK9~eEuf7Wab0knqt3KvmbqH|l7Br}=RlHp%{=x>=T;-fZALOn z;n%G`QUN=p1y=%!(Ar3SsaR_^N<%$8TP0C;xz?s}A0b2O1<3W-r&Fv~4e8EaH0PR6 zo~PH%ofzO93)}Y@B zj7UQGkJq>&*L$ka9LaG3)uOIg2tz%8%cvAX(bv%L;3Uj6-rA~FzHxiPDu3) zftAt3-a{#$?ybd81O9uf6x-#~r0NvqnIQwW#!Cb|v}9qdl=w1s zYoT`MWDhghvMcf8pVNL|mP?--X-b&l@f<4ag!k!Y<#uLR@IP=^5`8U++nRJ2Cymb@ zck0%c@Ge)m6l3h$GjcamV@6c=sLMIMJ|*`R(?FEk8!T)&KQ-GUu!Dg5eC$P8djjn! z_Z+fxeQ4DC{_AuZ#su-HHWh4fc~XsEu5i27&#>3TE4=i8l`-Y}I%wQ(VooePaO|xb zh7Lv4VY(-<`cWn2+l@6iH}&sSKTdAg8sWDVqCT7W+Rxp;_{lYXAwj3zn?73cR;Grf z2tDCz+WQ4PEYy53C%yzsykwwu^*GE>hz(rPZy%fXCZlaez>4#v2(Vc_d*A$rducw3itS31 zRnc~za6D`0UcbVUy>9L50Iz!DzM?T_vG)_Jc-J**pAupi@%_Bm?5gOf6wZM@;GdUF z&d#{YdGi+Dm-vsW#^{1LQzM1&R&Dl@C^zVdUfk)ekCNJe|9e}Hm-=38jcZ`O@{byBhQPBZ;Mpm{D+ zIcIi1Sqw6lwm-bLC!i&)xcE)+O}stji!(KVVW%R@`F*N&1u!9G^FVoheZEu1MQ~w3 z0-Wu2c@v-i!`bFnRpFJ3oK1*mF!Ay%0_P_5`u9u|fBc8%0jn<6n5+%T&yz zAPsA~XQiiiEt5sty-Y%8f|`zQ^{wEgq}0z%Xaouz61kt=S3BQTexBs4Ebet8@MgS@ z^}YGlmTtFEodZAOu9dg!7bMcYk&Mt5WTI^_UF<>x>k%AjsHKNg zp|r4tve>1x>L2g&(OE7-9xdPHj8ZK7WxK*+cwH#JxLm)RN0)jJz1gQGC^L5wN5Xdj zL+qdtj5K^3^Lk;+p3Dg2DDad3!Vz5%`^2oPO^wuJ_QhRQo7kJtVG9|tdJKWK+37c7aw4!-qQgtyE(Pqhp=@mTY+aMx=9-)@|t zsol-N?TplT4|T7p?;me||F=j`zVN+u*IhiMFato9l!l}dAyQzBH5XL zi~nvu(y*oPF&;_?Mjxcui6Z2jxFQNW<9^^E&9AzFtIZHkP`3@PC}y15Nvz+vzm+#Ei) z8Dn{NGj;^7*tdE(i_qWKqMF(M z^h(Ehq3~G4A?%f37ry*Ot9f5iml%^>)hC7DwCcFl?(4u}xICZN+{y=0kk&Qm8T{#b zl+xZgQo;D_$LhxJTl1V_{^G8`cHyJWVnMA9TgNV!##npm2^zA(puYQbyI~J3gdQp> zd(um_U*vpQ+G*n1p>5zPA+*5iAmU$*)nJ?x+XGa?nQVVd>( z=xfQLiUr*eCPL*6Bby^#Eu#LMbPfIM8$Kc1zW&ty3g&b}xLFjDW}@tS6PpiWuIxdf zy*We!hB?vfMYs{eFR^7f$ffJmQj)z!{{%6O@E%}rnek>!-W4eDJVjJCj}3>3=Qsg#TUCOrA%Ojs26t<`tH8i5Xth=);&84Rof%t z7SV6B=91o#@xJ`V&hq=L58O#cB)X;JrYqWYGzRUIklM%sco$&0-n)@k()4J1%2I;u0%_HNNQfqBR9BK~`w57O* zGApAc1}~4?#ib7GLpXJoj4`8K3wu5qND4`A8=FSm$;jx`i`|8dL^f9Ccd5*HQ&*#HtZf`z^)|sHlOlX_I1BFrylASSDX>@J(JrwbluiHKA zt_Y8CwB92= z$D1jE>A0iOT+f${X#I*V&G;pxcW3y}#&tdB7Ugj3ThL8KwAoeU97S>Q^PWfN!Aa|? zBt4j`SChe6-p8<1NF)lRtYfpp26EPYr2QS%qN18-rn95r2=nZC04}0^-XN*p+Qyqq zW78I}q?L^7*=y*r+wKj}-e@K}=WGIls{sPS8i~69H z6;259QQ&kSTtY{L3A$N|?w6Ov;Z{ff|k z0!7CmykCZ+Zh16IcsC!Kx@gQBEm~-2^Uw=I-n50g{fqo{t5qMPsz%@X7UK|IH5atU zvMNJ7?FEZdOa>@581M6w_w2<-iFUH$N+;W+WJ)v|q4XP}OQnY#E3P#k6unfucWcr7 zZ=B2ywVT74XmsdIDhN93z#faqeDVFTb+2kqJ&4Qd*(I`<-5G1@-`&yem1Q&iGVNfHNCs zS`bGx;$OzT)>)zsL;UjYU>ZW*MWOq7Ux>B4BA`6D!4)d-^bwAO(Nhq6Yv=8h`!eA{ zo5Y#yHylD+goI0OTm)tM4DH~+A)A70zaVRjBKvSwdi;s-SzA1>v-T;ilwWE+kaLrl$Ow;g>m+rSGJ!yF;j+~ zYc=wv_YVz>{K|I4SlOL*gK*tXke}R=n_t7JQX9fKc9tBYxsvih#t{IO-*DRka($Mzh`5b~~&5vpPX4nKl*Bo5W3&`2#QFr5Msf`-^ zqe?$9e$Qrxq3za&ipZQW7_4m6$Q*<%J$&KXSia&bnfXl_Bq>V$sWhqY2~v`GmKoIx z2df0h9j$XkYYUi74&d|rJ~oX@h%)Qq=4mhWg{I@$G7m#-E7&<>sAJei$kI@+1FyJ_M-EZc}E!vEx0Oiy5 zNiRtV@72@gwO7+wp1LjIdu!J3F3tW0?AV!?D-`S3tGy9Uif6h(L~)>lvzpgzeBloZlc%)DPE4dT`W3P?jyz3hNl<6d`M*w@zVkb z?KiliIAn!iu9+y1@cCViD~SK$A?q+@HLusz6xU9s3Dt)?+2#fZo3~fa?~N{$g(*!J zYd)A(YZ&4rhvdBt5kr>2IX~+BBMAmUBcp|cUaNJ_(dZTq7SUQVlpr1%;8jU8&pr?he z>nr7jufzd?}OX4 zq_Q^DQ#vI#UcUDULh^WbcPQUS9~YG1_yIz6cWNGk{K<3C;2(ecP))CX8P1`-%!L6B zh<=|)*x$uf=k#$sR8{rJxzPQY#7By`gaZQ6-2BpHa6?PMTrCNDr%{Xp=^)Pd*;y<& zEG+Cck_Vu|+%?)*#!r`BR&uQ zh3m)LVgaU04ar=!l!)Q5rTpRSRJXF7Id6X7d&P-KS(4qxOxl=^?x^7jPqPwo*~pW) zgQSAGBy$s+dw;;rKz@63&<%TXE8!(QdaLGMqlUrjbAE_!<%P!&*)v+w-UjVz9jLO6 z&_wW;>vTb!4*Uj({fo%O6~BIamfMfQ?YMt!R z3Ey(o$zp+fX~gf2-_+md0ZA56Z8*o`OOSRCM)t6@oe40P`H6_Os-wAzX1C(vlS;+$ zjs1Hc>u75pnDc(w$wx5Yw~jSOC_DFK&w@2I1|CTL7eE!64Ptf*xuj{b+wh3M@ylgR zni3X77&;;@iXhUbl9-lXW|iK}6Iz=WiS~(Fx!I`}lCzU8i;@!uUGGgV|81vv)k6Cj

    lxH( zRJ9uH++Ri)InTRuz0mkk#ZP<#HFS0Av zEV9#QF+2&zn(vJ0aa!RXJL+D*{hYLLZ$?r#zXZha%*KY+P!X?5%s7wBm@%ES*VKsF z&IkA}r@F=TsjI?4$?@_15`Dz-$}e)!dx=pZX)G-%j*Wu`r(KmH06}yVRq~y;1fxtX zrI^ET5}jYWHI0ON*UwLYoN|Ht{2jLzySAI`>~qpV*3_w_CR)EA)F4Unh$em$m z-FNLCzi?!Bq(rVdVpHVEFmg0P0r!P1uUf2MU#Uq8?wuAQ# zz0@Hg(m}iq6c}(4GUj{%(1GiiVO~Q%YSvXwEJdQ*fPk3{65U&Osd{WWm9J0}=N)%o zTD{LGI$U$?LqtfD%iP1skC)X?xcaW z`DOUht(?dcOJ<>j0k*8_??U^A0ie7vkPFy5VUfU(ywCQIgWHH*nD22n+$@dJUJ^C$ zI2?unenrGMV7!KQg>xlmSS{@vzZoYil6Hf3DlOi2<=#Si&hOwA%IS2} z(Qq}gwU@unna#m=QH7VIjxIS13QUCj$Kbo? z-1yuq#UNAHqIN&ipux%$;|HJpOOJ@j{LaAg%qS;%TA z3~_b(i^uM)Hf~x9vH;JVm&9c?jwkJ6rb99|{z|`J+V`lMv=@rL*$4*#68pwQwBJDc zsBvLb!Ag3sZN*Oc;Ds%tN@xF0yEjYjL8P0XWmGk0zFSp@PuN8!nBrCTuqUrMaNi7m zJU;D6C)QB=7hCy{XuV`_lPFIx7od3LO=3Fpzn_7z3g zzGJ-cG)_1zXxeO>>@)6c5AsozIQhkT>12Us_k7At@Qg z@Wp=mG026R+Ypa4QP66&s_>!y5c=3FJ_gGk-}ylhpz!Oxz9=qF=Lm;rnXzK4SOb}qp%VEsWE zabi7mO;lWh;@ZbuQ^A{Zo7+;T#>)b>j60nXD#Q6Jo9b)GGfl@}tA_02G`iZ_iL{Iz zhUq1j3F7IaYM7KlwxO{@dEq^ygN8T5NGUCKZ2~H@8*N|jPKpQI zqHT4oxrpGTR-hm*&$-dNGpSnPFD$I>zUGhl{{qWBBq&+$lLI3nlrd}LJ_8H#qe-TYcgw`Y>|t8Mrk??E4eqVPP7Zks zBh;G(DOlzWgkK`ADNk+c(4;P~R>IX=z1IBVKNT{0#Mky$ufB;cfcRx-DVI#|`39k{ zInrsDB_DsKnYI=7XYjr~g}oktI2H@4=Z<9!NzXp^(&RVo_|)>r;)-(lEB|Sv$Cm!T z;buM}UHpPnE@|j*@hmWneMyvzPw8Q|-0c94e}ry{QuRMe+DFdHf0VR8C>v&J@Sc8T z`uXH8pfG|OWoIu2@kWYBL5)AivyuJ)c)G&Q^fqSicnK+Dj@s*|Zro+v4j5*=tsUis z+PnHuF&jPOPQ1A<E}>aUAwj~^PulSlZMY#rI=ARFWM}_TR)>O0}Ii&rOm1j6q|WZhWW?d z_{*VP(0cnRnlSQu(eFRA;z&wCfKETVdzqH9Wn$H$#`x2tDiXLc>n;8lpXY9J1h^Ut zf4MSbF|oFb!p$ptJkkmjz~oMn7pC-tc2EKBOo-(wW8{Nj{|!t>a>_0B19cA& z)$Wp+7FA;*#Eb-#l;TS`Se<&ci?z?@pmRNyf7-I^Sfk5)Hh!mD-@xiF+)yC({gmsO z6~Id}^sCjm!IiC_+p_c$ToB$Z-xDiKMf0Di--^e4f9M~IM;xi3?^K$I6x!O?h^!Hf z7hN{#v-$Pl3+ZL)e%NL46-LR@5jQvc+MT;xZc}oxiLE3uWM!G-##%0t%kA_0kQ%4@ zeM!o*=I-^aErRX^VR>l30lve;#?Cw%vf#AB|9TXr!lF`uO~wu3bOz8WSQod|J58wy zT2i+^m%*8MxxYr>I^YNIxYkw?-fM0@1MKZRCmj!*y^ft7l>g?kTzXWz+p|Ez?nPF7 z`Z33AJGdg^7kveV?9$}F^FlK68l(DwTxRn_q(Ev{@*;J2Sl?D+7I{yA^00bkeB2k{ zCVFowFw+)BzCP1NWQah6UkPxFRpF0~5zR3b{<3J7-^B7fF%S;%Hb~UzOZn|yCEgv+ zE27}4Ac6{u3(1hHu-`wcpYo))YxrW*%j?(B3JI#|zLYAw8G z?#H~3PFYXek#azNAavk!QiB%Pi9T^$FtO%=#5y$LvgRal-tIgww=5`97dxlE0D;x; ztPM|j#^!Ghn3k-VDIsXBo`jCHV3(6`3a&f`*u9(xzXZyDM7eBW5y&#%%ZhqKJq|rkr zw(;UCKx4CA zb7et@yunQ$6vgfNK+7f6CLn!w;`{pAFuTG`o@w2{7ya3qgZ0MhgH2r7a6n_Z0=cqx z!hnvPT8W%06YO<`FTC7o`@ZtAJXR-dT06jn?gBdM$s3OI$2T?g^Z>66W{HGc@jiM^ zrfAv^xw~#W;nFX5kT=1PrG%f9y<B-m&p-jvh7lyXLz2NP&Vw)tt*O zHXZczEYpdO99e(yN30)UMw>*rtPdS;k4SbMtewMM6_M3X-rwO{Ldv zBhGah%vs?^zAOoe)N3t^{>bwb|76@!=KGaYCl)wVAi$w=Bwqc*(W$0-vTBTIuHACCvZEBwurJvTs83&}V^5@r# zr^iT)`$XgFRh|7WcjZ=XH{MB(?U6|F`^MTIfNWipEA~sD(;u|U#oDRK{aqKY_o`Gl zO;=AZ&FjerQzKm4QBKT)w`{~yZ{UO^w<;MEwxpa ziX$trx*?idY?lz0y5AjZo-aJ66Z1!Q_#m=zl>OfD#=jWoH@&rDZ(&?lRjC|esGG_= zP2xy3T(&#aDh*3K{?<5+8NTXYF7k7JzPPLRF^qn{b)l%4p3b(0MwGVn00#vAT65!w zHiLRlE|RlIw}J^LmLE)IjZ%U#axPr$n#&^fF*6=kbD$CI#Kx;YveN=}d^XkjTQuN> zQ4ZriSK!Sz#w2L{{4!rq8ZtkRT3iScNvY;!@vIgp?7aK87e@cL7iI#jlQ~(SQEAuw zG4F3H3{yfrkB4I?nl)i%p9@DF*GeCYoPDJ{)Bm=2(ko&=Z^e|l#0itZq?Vh!)Ke1S z_uvj$b=igB`#wx@Zu|FK#7&?1fl;Gi_8uWa%y`WfHkB^5YU2RJXEqm_ z^PFVhD=s{S?Tr7hSog?vNh6EqMpPwl2!A4>5otarz4H--Kgbt!Ges1N;2m^zcrFxV z$O?c)0Ipr!lqf}G-xJHzgjo@owRhulzrnliCr^Z8j{wE>+v1xyx+%fuPq`9Be#M72 z`4R#=$0vWrW1C(CfcqZ1#qu+O|0Eh!I|?jxVg+l$^tpMOsPeyJQbai069gHA5B3KPp~O? z*9mIL4*t#o_RvSasZ6Lk#?Nt|I!#{Jkb(f+SMf&>0}{ID24&d;wdq{Df7gnpmp&yL z=$J_@c)~3K1L!9WEInVsU#TVTx=hdlpzWK#?70G~iiC~bOjI=K-;bZL9Yw6mWPM+K z0<3Jn{NuBY(Hn&ev*!x#EakheZDVhP2SKWBH=h@3p@=tW`oI0`A9EnYss4_UYIY}{+N-F|eeN~kpTmIx#6#ny2s{PRn>rRuo%I&)s z0$8*cCK&6|q#4X)Oz8h5&R+avZMGS+o%St%0kVDXxusY-kgOLsX&G3sQZnAuyyAc0 z?6)yyuK5+e5K1jWCluX+`4CZtsDwA8NIdCr7eD7e;9LO)%%W`9d{qHv(!tO=i_RLI18-)&}}UbwNM~;lnh@4 zO6G5*5Lcq zB020p1qp3R>ww>)^*g%gq(v2EAD1N;@$Sd|5YNZeE%l{JLwI5DScQl&Y`QV61L;-O zbvLtj_d#2zVh@+xYrstXgdE+{Z0GiJ`S|pAhW%Hts@$7<@ykTvGN@yTfglRvN(S_4 zS#riGnp#R;bO`+rlbGs}9!JVprU~@B_lQfb0|DTWI^7234=O4^waX#^mK`$@tbo}# z9gsbjg)o6SipZ(bL7%H67QQ(oHjcdS-POHJM*TrBA2h5>9nyCBNRc7nz#3h3!~Uky zsm{E_Uh`XiG386|i5(|qnhgQj4Q!Oxwf4hwA2J`l+49H&aGmt>S!6RYIpR6B-(pEB zBhDVe>eH``9%NSH3d=?|L>f=D;r1XeZ@yzMBIEI%w{6F)ekdO%V%@%4rs?3TwqLVg zBhnd(_%b8$ELKCuDCG>hew->c&xzPZt6>5iwp8j&`a^d)d2vc6{9s3fo%xTPc`7C+ zpJu8oJ-4&kIor6IFg9xUsK)!jFk1C$Vn16zU)i;p83`6oVI5oxwRX2#RTD&bs83V*#3-jj!cZc0kXo!cC9JcptWcV%-ARd91u!y3GB4k& z=kAD;uvv#%tI$)Bf5X68_aTQHFk`2Y*j z83K9o5i(%{+(+koJY?U*-vY+&GuxP7M)R!$N5Fo~%~|gq{MhjMhTxuLwIX{sMNEh% z9{ICG?z&2(X-YYFp`L;;CWEgi;BV{UH^kdOh5OOt2zz+ z)N}tVY3{8%^v!!8{}9|UG<7#v;~P}nwugtT-E(>vOjHOy6xkH9Ow+m-SVX2jwZDn)&U_#1FI{7;K7Hu- zWsyMbPT3imW?IN%WGmLsneWGg07)Uo9Q-=%T?{X5ljM92L6ZE`L>t7l; zgY1u|gt4{UJDiL%fZ^dwRilkowA2KBlO6RelZXCIXK?+@84*t8TS_4yif-4p3KzkQdHY^LUZR3n(WWp{YZtl00RSoC1h z6LdxaZG>;pp)pM=$CW0Be6dfPNzZa@@VzlB(+1iBJnu8O&&coHEnfBM2_$CP9F_Ad zMC`C``zLJv#s*KkE?#zXhU#FyU|s(v6kiR0$MS&T)EdxTpOocJ=nivk$g-e2_@z-M zE?>N7kQF=X@eVp!>|AS1iA~l`T!W#F7aBE?NYMQ|vL?jDdHaKUe&%3byWc>b%+SiE z5!%%v&B4R&%^csnE~J;5y7?~jVJ3MP(0nG=?+1ZjxAxrfz|IHizpsivekq=V3AQ{R zU+E3y1(lfQg`+`{qgRr?#>Ojm&`J&E3vc6v25bZ*e zXV(RBTK~{cP$P%E%{AB2<}VLZ0>dX;IzEMxl0E61>78n&Y0#gGTg$6LEzNCM(tla7J_EEQ4umUd_U& zWy9b~UV>=#sfGEy*&;r*KWt)$dWl3?d zJJTUiLT%FI=aaz&zk%wXe`5)k#RY}L!~VR}estExrD5#0WE@T~q7~B+W0vf;pS1f- zdjF9iNXvi+Q*K}JI&(IC_upl57GoG)ePXA<$$1_3BP;%**Q@0K16niDY!sl1LQtNY z_EgU_d7JAs_1pb}VtJO`bsl5DJpcxu!o{ey9!H*>dqx{18*6>(N&RNjVMB_W-y-22q4gqs82N2>Y0r^WX%H(FO>Ok1>>E;_ zpoW;IOuc$o%PlD}Y5HY2?nHyXu6Mh^cSCxip8-`P+JDFF_4L~A8N0K$K64%jcU9^W zkbTGF+X`XL^br4nRediy(hEURIU&~qLoebzm#h80hH7>UX%~*=sBCYl#cC-vt9*Ae zNv(D73<&Gj2F{z}VoCP0xO!PxVj#NtZdgay$F4x1`o%nah6Ycc%xL$bqd`{pXzYZ z$VnVDm5VS1AF7BYu%aj$}7Ii(O<1kr;c@3nG?yM%HI_FoLBDIVY!ffkv|Yi7rpZJP=Yge znWYuEoVwygpW#5y&$i9~ZE)PFEaJYigr+))G0JBqd3UFxp|?p))8`;p3wGw5;S$AAk=JTu@e1P}u$4OGyLi%@Jv_C) z67xO|FVgpO4o^)$&8&ex^2}jhH(|lJSxdOikUlNq(g}|;M?Apn>(Y_aanjB5xU2;! zjN+WO$UD`&;tB^H!jD?54}KcX;kPK7lQWWDU`=?YNV{&wdq(uZN?r0c>Mp~IKPF2T zv>-R~7rxG0Z?9)X_MESG>pDX}a~cw#8wy?C_TEMfn)>pg7dZ}!IlunlV)orpYFX~w z9PY|(IX9aJpp*z&6;`mE{Ct!a<5Az_o8M(3%vgVVt#uP^gPf~K+qGEdanu|D!x9K7 z@!457Xl1;fQB)jhV$w$#6Q6VKb|1vRxVf&N!I*ddub<9$@iG(N4gU&U$bTVZx#R;uMX0u|w{H)_F_(b`=`N+V@QhY%vdmNhTuT#eFNpVWT0hYV+2w~a8xAeaPbSf)R- z4*|KejYK5p(h?=d_}-CRXEhLepx#Md#X95{9VS1+Qe$1{#C-1dTl>a(2%RA+KWUkd zG5$HLgde~?G@%46uF4d*@E(W6R2!E|qVM?Rnmth-Vn)VZiz_$(w>o zac@D!>z4a4v-z4_<;yiah0GW#Lu(yO@q01-;rO}(M@+??;+)=Mcw(|=NfXbl2K>X| zWO7OWf&IaiK^l|~2a!L2U+&d=n?i46{P>=iYWO-amt5^71ntPuvH|)up6Be9SAo_)Z1PUJF zleDsYO%m1|_@G*8y?oS9Wi~44-96-NwdSMrX>bno(Yn3`h^^<*eGTLH*pi;qw{z2L zV}CXq1uIJRSx39iIAEdZ4E*T&nH8(;x$SjHNP>_Bg(memYdV?%(dorTXB8R*EA60O zLMpMs>YY&Fmd%sARixZ`esG`(u~!m24p#e&Os^g+PX}w93WH0PpzURZafiD{i%^h&l()y{_Qx-{9#`xypNoy|hf8V)T+G(%6V&_f45cp80VGUfG!63;4vU`bTx9{dl zwkV2}CiYBCIy7d4qpZp{$U>BSmI^2Paq3JV#1h0>~=ss?M2UdqDlgvhIAc-h2T zugZCz5S3jWzD=mUCWwl}CbO{&@{9Us20R+u6_Lpx0-aKT5RjINA3yO|LK95>ZeePX zRd#2Kej85@H4KSN)>mU;S65~-_SCJG(g{R%MI`gYL#DC%AH_bnCih?|KDyGCjQD>b z;0v<`byN1qR-uKxw(^DRS*XMm7RqzstxxjIFUisI@UOO1R5*cSk3}QC3MPLQ^Nbl* zDhW%OejKNrA%zm3Pe#lg)v}VWXIyF0WiZl6voupY9bh_0EpSeQ&ONm`iEVWkxw+~=`tiX+SdEP*ld71+2WlVo9SbzWltFmCM^H; z2nBPAFCJiAxysae)J=3{wuj&Bicglm>uK|+NvEMP;0pB}~01_m$IS-aB~0 zGYShUCv5c|l9Fk_DF$-+;lZm#f1BL{cT( z%l42C_`wLoNYGwYRWaKw*g_f~AYPtiOBdq-Kc8_ZeoieyO(1oqK3FdWU4XU?^h#u>Jb|Pz`o}!nhU;_pw$}g;cTT)m*J{uk8HMd&nI}$guM(@%=>Z0AxiPLE%*rEQ) ztJPb?4l!>Scn6mlkPMP!O|Z>HKC8&ZHr6S43FKpj+=0|-HKFFAQwxulR-q{#pbG>S^u+d_M0m7Df;FhiZ1A~R>pyHeX_ z(K5NjCo0}id~_>~$oth@g%_CUosyDrRv%=_9p$dv`utLkjb!gcbOiGu6n;y9f2p2~ z&Z-lBkPM_uYwyfM+L`;=-dBpS3GVON67wXkJE)w}X{+}BbnXsa-1?B~VYFDFl zXO%EclTSYHYgVcA7=_MXJH==7_m2Bj_thnxmJYoIFwy%M?|JKn`~tTDntF)+zJa|H z&R=ZpoLOAThodO?Su>D`)yu0QQV=51pL}6ZHh3PcMtHB{5veb*7rp}aLQ<%Ma4KWY zD^HI6`0-&mRpt5kC)xb4*mDBgtO2DG#!#Y4b$2-myk16a($2aAF1NX%%5==MqOs<8 z1bkXYy5N2;9m!3RW$H+v(r?C*N57xlhyMzm|2E4qOgQ+qA9YxzD;PD>>Zg182sJ5B)pM%SE!a$~wl9D3Gxw!jIR!Or##+%gg@}%W^{q%hGsSk2F6dz%g zE#yCr!Ayrw`)wXpw`gA>q8vg}wj_BA?z7R5B@kH z7oqxLNs>5W>+Ky=G$uIiAY!CYq$&SsJgpU?MGH3Y$m>3wqZs_!LDj7sy^T)t?ZfQ{ zM&ZYopUY_B;->pLaJ(3jFh4)TqIk1EdT5_F@Ks9lffCb>)7zd(OnJT0e}G1JQ)kR~ zawXS2!cdU$O?CEC&@|!x6YS5UQlQWuSdsRn@-xkgrx`!Gh-Zg5P_~zp9*M78{L!#d z2IJ!j?mI{qMfVAQg#C;gg>m5F{os3i)(8d2ULY}?b*ajBPY`WLMHA$E=MtoGpjc}% z!#Fk;ZJE&VEf)Y_a>2rj4%ASzS;%*GN3@@S7x9kUbcl4jxmq4WN47?x-C>AoEq)5m zSXLTS=fVn1vUX5`V5Ox{y{0Vx^m`;Hkpt1CeJ%zXB`ew3FRg~8laM7O>0a^Rj{am zEIZAce;doCuuZYdUl66uXi;oaxpAP!-mG~o!AMA(vp~*dIk9R$6aXiuE%*W?B!mmG zk`v7|P_dP6o(u0dV-j5J06TTSdEYNCemKwg;D5>gDv*W6&n{Qf$8;l!3VPvtM z)K=ETPoiVum$!86T7XIagADgJePHzU=3gyec}_uI=SBxPNF#2nlsJd*ch`-&1J zEbmX>M!EX*AI?jahfQ6TA}T7H(nSkm=bC8VaR)|6U^FARvxou}p3zs@Cs`#?P369H z&51z}PJb?*5j{vLBsZCpEl%YAlXlkec2{Bj@?K<(1zz;GL(&ubZesZ|2c!3WbU zmuC-lKjbj{8n6q+u6Quk92oa83LtK$%-(D7idxXwg>7QWYwGGIbsx)OgXcE(eo9If zdad2|Q+EQO4m5D{QAbuax$zx_E7tct_R%$G0>rfDN8iqOlKw4DcDM2aC)qCqYes6H z(Y!#f?fV`-p96OW|4n1*?@uFR$`Z=h#@_5@|1wCG#S>)`#OCE5Si%8k19V@31AVmj zahc6ke!6Wc7y^ENIG=M*j;Zos?39FzT~x`!PP!up2hR zaJ&ZgQ2Y80LWxNK@&cT7=mG~EAKrQsO@8RxNtAH;IX4+F#R1w9z!YGeogZ(s7Y2`> zy)CB#oiv0F1Wz#5@4cA6WhONjdC?y~ThqPY2BMp3~YPgO<53v9pJpzz?LS7NoY z78M2ypmhU2IoI<3-zJjHcP#%!tP=GwfmzxR1|)}J*tXjY1F~@GBhgAp7Fo)Y{mH7v z%$(Wo`LT+6GeVrLH(o1rk6?IK4k-IMUUEI*Pf;{^yVGUjFSE&%@ox_ z?+4mc?z&ePhJ3Go_Dsvf#}9afoF=wJU5uwKZV?|U_IqxL+p=miq&KZ{>?>BB#BKvU z`=Oqmq5Myi>~GF(}Mk>=3dNarEHee`{2p7~~Gty#j>6?V7|QyD7hVZIC=CvQgv?XbSF+Dhl#emc;J8tb{JLuJ&aNXX~e16HoQ9V+-=Ok zAq|ery)W0lWAM#?e+w}u)RR%h)yoTEVT-9fTFW8NN}UkVd{l2YD3obh{>E~(SrG2A z4rPKA4SNT>3}>QC2J5>sg$5r@_#b%Ib|kaIGm zv)p~VnmbP!ayIGMJLGbUz`V2O_r(P0c3OrtMIjuM|B*WJ-z^(a2pa^pk$V0~qUjPX z>^UL{kEvIFQtZ_>p3bhajX0Zj5~Ni8g_&47bZjdN&+Cy)7VHqE>sy3=yPHC@VpGxc z33Ow401!s}2sUSfk53xbL8d!hlK&!H*)=OwkfP_1rzJpz&tjtGp;m^gY)e_UpAxOC zbG6=3)Ok|Q&Ri_d#QfMJtrU0GlrZyXq*iABWmT~wNgI$Xu-10bD0rxU6?f4ZIRgm8~e`%I@LzGbwVnJf3r1S9g=n3E(Q8r9{^~5G_aPvAjWNa3d{pTd%BV+w+j_ z7NI)G{JN|u1gZZA6aL59f$gdLOcQj@!B4T|9i>e=@u+`T3oQZm4a|q+{pen-z`siq z)mOoqi~T-?VHYFlNkWHiPu|a76ZCz$r8|{B+A`_q#q>YGRjJ0QobNvvHh!|!ldKor z#3!z?AAWBB!TYARh(4dCUY6#^2LxV~7?l*2K z+BgAYPaRlxkbN5v0abg4ZNFl)cOH%??|#RV6M?63TVw-yd(KWGw9~7puQz`>$)iG> zF81Peb^(Oxt9AzjPJdnQttU?I_(xvSFo0?a`Mz?eb^z>wYLh81xP3mpv8H+Esr<#W z;6H8A6~4MPquUO+cA9j9e7!)sgU3Xpk1yitO_R!#2cYeR=~IC^l8w->w=h57EJAr^ z>bC`LA7}a6mMbFMvU)05r$K6my9v75^De>xsKi9Yo&ecN zhw1|WT3l*852Fz1kfp?A-%QNT>VylJ;Hn5Y>+_s$#oD24#fRo)#-fzjY%QK~ZTyj* z!c6-ITp+y#NPVVn-F40H%x2EZ8fz^?XAx8FZ_+x7(#?r>{~i70BPwO%?Vq#RdOG(i zqCW_mgD_LV3+qk+#-;VFI-0}UpC~d|`xE+)_Gcl%x~j48I%@(#5yAvt|8Wa|sj7Pl z*V!ocJ2X`nzy<y<>12mdjj%s^7LN#+k1YuDe{7vNL6JJB?Q0 zxujHX@+fasujTEhd>LM2dAM&|^+#-(B%(cXTYhQldHd|5Yvn1X!HVRlx_Q)8(i5Kz zDyQVKsn@9rMhvO-Ly4*vO6^fXX0}$>Cgqo6{OGL~^sZGBge6BjmY<~g-+(;|AOLa( z73(_guhV!bU(O=s?XTPyH-d}Lwuu;Khs<7XvvZkM93CZKV9ya0)1f=wUfDZlhu@rS z4cj#qMm=hyEX|d69@x5qZsp%Rd3%W;0SgoPqh9xf@$Vqf9c;4m7bu=h369co#;6zH zVbKY|Q{{MlZgHs)gKR#sK=btuSV;TRrBhI-NQk1>woKG0-ZE!PBHG0wC>rl;w@MxE7`8l5#x(l zy2ylVAckQInp?fJl3q}3rj@lKOqeWx=GeLTcgzC9@oD4rlP_mXXf6os2!L3i}e5fv7;cB(F?G(&OG z_&9^3LaFzL-Ti52nfKf51w27iWBrhwt}*kA>ByXNVEguOg4w>c(Z}B$&Us+u)n{ks z43QHWf-?Imaq!o<%N9;t;XSo=$T6?m6d?hRzJIKLYEWMekF7$yMW5n4J)M&u>!lfN zaRWphwEJMCIvR#&lVBnb>5xf~uTZOX266my>OZN@*r1XKlA&LBF;uWCyl!-|hs?}s z7FVI-y^z=dllg;`=%zh%gn@U8NiGf z6#HebigcfOuHJs)K=cedL3X#$kYpw?H`OzA`2bbEYy=TWz^z?;bLC&08uqV%26op9 zOrX*L47umnHj<%w%3heyCVYLWEB&x~o2)25A0O;DLd*;ev>uxL#6l)hos0->S49Sw zumIlg^(N_36s3?vd2!!DaIlg!1wM%uV6E`nPX;+*s%n}8uv`G156D~&pB$nVV}rSu zhfeAY_{P2^o9yl8?0Zfc}XA{GxTYEe^BBfFC2!3VAzF+N0RLR?4 zdgE%{7Rw8L-T$uajB|;kwi$wy1(`Q6Gl~<*gTJA4mH!H*(?OKsrhOJW$s4g&1WG$h zI_GbpkDb7VWpyv^7tOr5#3jO~Je2Ko_u*;;K|vwA^A!GoEt`kQ8QQJNZhvP9d= zMIeEBUbT1t#IvnnjZPJkaN5gO;~-AQz55m<6JU-*CxThS$^=k!LNe1J>`oY?nQ2@H zI+0zU@BXI%BXbdP&ArWI*Ux9wD7y1iSKr5V;@FBaQ48y%Wb#w!w{5COGD@~KN1;ty z{ZE?=ZkfHzqYt(vRTcr_nQ1IZxnCE|@yK!DlcY{R;x&J34<87AUj~D{&q4DwOd1YE z-naE>8?O#OE2K(Z=@#fGi$q2rLv6-H6242JFL(CbD>?qztU&L^vzOMF9bFPbd#1e2 z^|%F3ug3i1LH9o-rn-LdQ2-j*+o|Qy>jH~VY2CJSJ`c1(AKsScQ09;=)D7BAQZoQdaOXBJq}Sisupu2rP$FFF2HZ_6;7gqGt6VIsy)`vTMGy+GI81>3 z_Dy;8f#t->&(^teY;yWDV3)Q#q<#UGc#xH zm%904Quk`k<2i$Yn*Ea3ndM@9$~82{9m}tPIckC{L6zh4RX{W<9=#hNd7t><1|f~C zHDUAGuN5^?xfSGlg9d?&&((9n4Db9wn>a4j59*vGu~Ic>_K@#mJJeYfDe4DPkkj}Q z%2dLl1RRmvDn$lePsdJ2DF`>VfW>0Q_lIp6(^Sb-#)2;XnXXXiQ-vNM?qkTi#f@R} z?XenXPQMKo;QVRPdP^E6ji3q}_dC~lKPUCJuRbAS&mgP4cu@XlTT+%jrP7bbYEBw1 zN8aRyut)wB8tx>e;bS>$EOafBkQ$Yx_&}vFtQady0rQ?ngqCF$P%%Fn{TrYX4%42H zuiJoV*W#|LqO-Tz#!=iKknlO{uM8Zs7jmli&9&{c-4|PDeZsWu*Q$1BE;~F@Wyo3x zLDSfwid>M-ZM$T%`UNXcGlw*VXXiBQsf1z3G<~KVgOx1zu8#A-Su|xxUS4T$0k?ki zriP|lyN_DRDOVa2zFX|p0yl60^GHs<4sQP+j&PQF+4UG`^FF{8}Q<5-OD77atujSX!Yl*J?2!HNJg#^Hw z{E5SbW}u~hlA1~VUUCW6MTbvpDkSr4Pk)1n(Ngkl^LgNST=rt__>%dWSHOJ)F!vf5 zyx`&5%_~ruQV|Y_v7^G~2^bfrl6+fvgSOsKhg9IOi1gf5_f$+>mTaq_%*^3XHaThn zN!sJA&b@@_yT_EQJL9wZKArD$Q`5PZY}8|y5B};s)9AicaEZ9kDE1)lHr5;{o?86c z(n!U8c1pZrZ~O{2?e%C}>T+iMEq&vO|%nKY|thSGy1LQ>04-YvAWl{BUqir{lU=hNj+K41&a0BZE^kLh@j$FE9fcBRFjTO9m35Y^Ar-zcP@bX-ithNi~u)PsdRqJO8uy zM8-clZbQv!P0`+*B1{w_`K8tpJMU%LBzlXnvKmnV7?|BAxI;sH%5r1DNDC?p7$N>3 zCBLH0aoE*2<4bo+4QVTq}8dy4Cm?{f#^SDRWyBLVa%weXk!fWSQkt^W{*jo0ij4? zKoA{}ZJ>GPH)&N8MIW;nQIjMywnTT(FyLJldtnFQ*}y$^k0gK_&&zOB&dF}6jCT*78==F?GcbVsDYL#tiiob|IdqNg0`H+JI&qcu*WSguC;!d{{09;6F@x(d zix278q5e;1qgOpiWFf*)?&b!;26N(_@qe^l4#Ai!XQN-4d5q_2B^(L{**kRd8)}*? z;q8g1qAOhwQlH=)5!U{@pmt2whVQIN45|Ef8cjJ-O6FbVM48v!{HutEl~NF7Fd|TP z>!6VQCXu=8+-89rQt5GG_dT0I5=5tv&l~~ht+2&Z_ab_UI^z#{ z`NPWA(*NP!ojA5yO7nKpBK?qQ@{QF4ZAi|4i$VhGSFgaLY>?(U9w4mpoZbcaGGF@q zH3a@DK6&V*O92ZpW~2^L28U;*soTI}mBiZ*F|k2_&=2b3nCuQ7SW-(q!toTdUFSn3U-bA&Cw_Yc+{Ng-z;~Z6 z_P!A9cL1_r`V-B5lz+>KRy;^M1eh6#0fMC06L`e%J7*&PV%z!mtAQ%vI~^yZJ3|9; z$7^SCGeQL*{s@Quf039Eh{qnkw=vSY-P1T$;U9P;(h^>eGi&#sdsK!R1K5TBiqrss z1P&+UQT>)h6hUDh+V_K$ATtiOQJsyTQ#n~(C()TJNI29*AW#{&$v}L!+#i95bx5QD z&uAY-VWxf_mRvH`ZNzxG$M#n-+Kf z-5LKQYcLkuDy!{pei?~S-@A&hv_YOD$797327YI=y9V#qoz|!Y<*%hbVl)4H^A>V< zT}gm(cD!vTOgn?#$=U)|b3a6I>12>dculn%hPNgR8X2?Re22)7QfI%73yZLcUTePxpzvcgHg0p! zAm=hr6~B@9Gn}A)$qSj;!BtVub8^~vFe5spOUH!@_Xl{dQy;3ysu3+VJu9#sN_ZX< z{96My=*|rPmXMvvw!j(KjjXoR9kzG-M+tsHYvSnqXeY_UpCjcjwk4n;y>6PdsP%V8 zi`M?r$ETLgNyK_AJSgSVDNcM!V8T`KecO5IhjZF2<| zx2eV+O8Pvt@N+tX3Xi><*e*r=Uhnqii!M*Sc{$u>iOZ2H!Vz~i7ik!5NdT&S&@M@*$T=(8S0-txBzkRQ>SDZm7*E)&p*DZ@1?jYtXH!S z!hTlfLN=fX+v2uz4W4|qr^?b)+$^8N&M5~hzb0RtyX9GGlV^E==;gnKH)V_)x`_xcI`{qz4mNM6sCf0+s7a@z!QlXIX{;i^h4jZ! z&R)xjf>$Oc^%vmfgaO`vXasPYw29~~@qV*dBMs79iug=8Vz`Yx7Qq}0?@FZtdhRYg z8gbvi{+`6ntV@e|vMf6l)Nv`5i@2(;MLFVYlUh5s#-5VfG1 z=05wEeBmz^-o#1r$$Q>kchn=%JhdG4Of6QWy~HIWmRt+99@z6I-$ZSn?*t>}QF*cB zL!3gh%G#Ytof^!xBZyl~D1&R;O^_BW1j+vReLR-QZDu*R!xsb_gRu(0L+vk<%j#%3XgY~+vV-{Y z#K}>v(?El5%Q#M6lE7rGvF_9p;8Gz`Up(~RNiu4oe?=t0NHWT&dcVSqN5{TW(erYm z@**c15=-pukbhvHz#LM)kQkjevklpb+}XD%VG7qc&l=Nx+aehUu3VV(`fnV3HWEUL z7%xbY1+Ns~9k2||(e$s+Zb(-;G8)*G!%NzE zjNtl}3taoiUgEG{#APlcl^&3o4&$d8v}B~D?Hl`SA;w986Io1sP7fU|`h#bEa;%&W z9QtiFXzw)zAZOcud3U?L#3qA3#5=HEts*QLTmp0yOoPCF2@<-1ps+X51<7q*it+(n zp4iYw-vNNoLS0EYK3I*})H|4jlH;g<4G2I1Ks6(XS_4w-k3)s!3?00}atM+dJUiT1 zXQQBy0fRCOavVG%BO?R*pZq{E?r#Bp@P8}7#lOw|msq5nLQ|cxyDt{d;5$ltY7X`n zeeXL|;T6}5n$1Ya9tFIU0hhkL)3E>=l7owleuHGb{Xm72UgvfF^`o!k?_K2(kV?jG z3lCLx>RA4*1sL!;jS+wYfKGD1naO)cr`fWs;gNv}i$F zs3K62B0A62axu=&r3LbD5Sj4kT^OU_07XqBgxyDi($cYB?beaEG4w}ez6ngYoUxvx z9VO1P3+_pX?my}M&m_Y3{Gz^=Kz)Y3oSo^)NA#hR1hwpf#Q-%%4*GdSxR+F#m;a)X zLoJpX#2m99zu=(l6u(kR7bXhY<>^uKZ>IPPfq8AYN<)A`Oo}Gz@PWV$5y%7(U3M(o z4u9Wi?-k9J{}4LmjRBX`qz7^v`Da+RV)+w}}*0Lzu{$5kBH@ zp*{L#B!w(pLY3^=rxGGN#Isd=H7YWx5#{feyo^89M;FZ!*lDN!2R22>(uGEtlH$Y7 zl=NZwPk`uX=DTDB#-h{2U0hpq$M`bVHy<}xrWjVm%~Y{$46r}`e+i=YUgPD=$#*kQ z?Rs!`Ld~NDQK^4YM8?rQkdW}k_J^}r0Y2Qn6x^U?_P;1|vSjHgshJ7AymcEoP@hv3 zAHH<5T4+RzP^~S&Yjs+CELxfMn{&<2m^7cVBr9*V)G1Jjo_U6r<)O4J+5|}cGJ@6~ zy^T}@^sH;sV@`JzPitM1mh21e?AKzyb=dY6rPsF3K&Fp);a>`Qc{fal^@UNGI=9_} z66tswm%~%(gIgIVncX#yms|CG|MFB7yH&&m|BdtoaR%Vt0(`I;LEwS_=yaC^SOaib zFZ{uo@b!^vkr`;HL1)|3(^Fyc@)JRHTXi~7wab9(cG`&9WIDcuK3K_(Btx=)_-5F} z3ne;D=(;RgMJh0(K!IQMheqR!6r8e4c&~DX<=g8E(57~z{JpIQ+~VB^o`zcnlCc>tpD5V{AtIt6I<#YwC=B zp|(B zBPLIp8##eU!Q6kU$JF5Sx6sIDjnq#W9^H09eR3-Yc2}1PhhG{VA}xUAILVhJl1|yM zK{iqU4^%wQ&X)LwS}?wU^W07Sf2o)64oB`N-Sg_V;3EHr{KBpl(VU%$CTTOfjAZn#V5qJ zqi`8HuXfNc>e>fqSdwF{g10iZ*`R#qdteKOx{|Df)cl|d^Bn|s91ykyMyB6kA{k}i zCM=Pp$@)%9%mqFlh-37#bH$r-I=CckwaM=*VZz@aee0h zya5r%Y&(QsFpEx^gws6u$t?P+Gx+s?4zQ0BjjOnfZq)7n>-?JY9yY($zcZVTlkwQQ zVbZ=U+SWu#;rkSVktdUK`l&00c8^PT;@LXC>!mfmlSofcI^B}pRpk#Zk)KPD)A#n= z3<|}TWE-DrdnWYCJXK3{{BFJA_(D>}?Xqx~zKaq4vn-mOS#8@ZV@Zf#Gv9SX_<_6= zlw5#xKCT{}iyO_+yGS>^x1`_%VZCe5J=SqJ>q_#`n=e-|;yOqxdKgF=VCZlyuj5tr zg*)Wg7%;ixfph3DRr3zgJS_=?%CP8pCZzqR$AL+@86_7>$V=ZzFR;wG)d9T&qKL?^ z{Px!cGki_z#!)5cQ*qcv??e(-3nQNJ>;b^|EMAIcX!L_{CvZbffBI7G9>7Zm6bsN zhVh6&r3VnUBX68IIch)N4R-N>#U}8!Uao+wY77f4W#!r)@yEal@Qz}5@oPE55QlG1 zV^|L*FyPUBODE{2-Op2c()Enoy9J)FnzK5#^lM>Mml1Nv7cVG{SV3Dv{f0}nFL z(g1jeZh*DY20WqEq0}>{_aZzPB-~@gZ6tSV>hj^1=PxCi5<;%VpMgx-TmmUhyQTP*7e==ft?N$D_ z$Topdwh}duhH6(`>|I%6Hj-vIwYRo2O$5&k#?;@e(eitx-rsC4 zn>&EJlet`>>v7~H2SHnyKTd2w2o8fJ5D^)#Ay_Q}5)@2dMiq5=gVVA(z%Quz6cf1( z9^{O|!5Q195tR)YRUDqNQ=zjE8Ux1**kqt<1Kl(@?U%7UE)j3`9a?;D-LVc9TLMa& zuh8$-b??@LwbCwNSu|npr<9l1E%z>$1ua)EFMn4IEuHN`4$t>y53@My<2kqs#^G~* zHL}GiI&KO+lpg({Day)i{)Fq3Y;hmCQt?+fT0Q1iXOkpv^BV4t-ax&ad%9~S7|qPw3J=(bt8pCk@5 zz4`Kd`FvxL=yD{?9SH^l!>oXboK{ONi`S&CU?yE&MTws3IM30iqwd%uZoLrBKw_>Y z(gV?7O+zzC3$}B#Vbz=yspp6{AK-SR(B8p`k78d>fE(}5#1Jqgtgn>&KYZVK5FTvXbOw@KSH;W?>|NSCXO1Evsb+J^Kz`lIu;Zri$96+c zk=Xm=2B>eqbsw*R6qriz_sI}{0NKqF$|3B-TRz*?B0-GiJ5V9i3dxS3zI3_*-*(aU zqj-r@4B#N#1sPdNGoM!pAq6l_^|K;t~nJT5!P zJ^)$`c*=%m)?m*cc$f*n37JbqJze=lAOLXy_;?Rx?@wjhSh{TKxAGU$JI*UoZ!z7k z#!}mjQ{B!PR{Spv?W%`OfgTTdd;mrPP`Asm)VnA9)Au(+_pJvW_uzt-7mw9iF1wh$ zPx?u=!2!Y#?UP-Ct?AU;-^PaD+^lj&y&! zc>k~kItU(N{%eFFK4v_L=4z9LUi_ z#C$;_$Yo*+l<2Yz%N@>Yv6=T}*zB`L-oux{(Vn2_eI#q-zgfmQoL#)bhrK7qi+z$m zhPdfZOZ<1M{7b4WS9mv9kQP27=7?b=xJ&-?W*+h49qxM<-lmJw--)7x7Cz_Nf*&wm z;fNxj^*!&@Bc@%T>&bkfOl z!r3TIsRo;w=f_+w4#M(m?HBg0bsUD$1=3sc^S1XLF81b6=52p5($Td>(<=^-jKDX{ zhc-6yL`O%bH8v7g7!Qd$IdP$*qa(o!EfLxotx8YOx;A5CVx|=pJ?sf1lf9-HmTE0R0aZXlL;iU7Zul|UdZB2NA25Id?6XMt!P|#&UCgs7v$WHD-))| z5fBpUsLkS!O_N?cgLCST$bJrIM>`uRENJWwr-i4-E-gg&q#b&)X#+AFmPfNBO~d?> zl89Y5UZ=JZG#@^=8RNf=qQm9neNWi|R5Sy8&)$In+yqP@>w3JX9p$xx@|;jfjf! z@%BbAN$_uP7yKA{*-d2iVYb@hjosE`LLwsj-5-F!=A4SJiW}SZ%{FasWTB-+2_j(b zhBTkEf10bs&dSQ_9UPRVz^(m)IpM{oDPFtoE5vPHsQ!JVb8|i8vkk5?)6&6g4ORda zS+^LOn=ja+c%CV!D+wG(cS%AV>cEupm1eXrndCC?(pa9p%3f*F}b<9k56L51e$4ZOwqO27ZHlhM&xdX zQu)JPsuri^<)Mj%5({W+lW@O%>*r6v90%F$q738lo+wb&-yY4XXOBc>(hvg*m_n78 z=_9$9W0fDJiAJL{?0sbu70Qs#lbLnr3y~w@noi5qSWXvYd`isOebt_Rn{9_|G^ih(od!oB^31b>8YfooWUCm$;(X8VMOKC*LgFdmh{^-r{{Hx+h?W1vCi{zQIf8726WUg?g1i=R0sj7Wi*<*> zX*^=@HTo6}8PTtb($e0UNU^cAe}oikUxMBw?ANbfK>83Njw&cL#%41>a*O<~^0ldG zcpnkk(=s*8`(x&Ngo250eNnWl7`m4Tq%OZ%=Iq9#>FDXPO{2DP<3HfQr<--(;)ZT%+x+-ODZC{@sEQT?=ZA(fZ;YBPM5Q=5XXgOwcgu4;jtOWb6 zYd3s>ee17XdY8v>Ex|ox9>k!O5_ICEjtLW63kFPZ$fJuogX_!^GUYBTq5sFoWzCwW+De+m1p}DE~EfZgjxVChkU=@UMQF zytU1sZf3X${*a+}&pj6wbUw}3Vo9{zyeD3Jq~{FBvPSzZzde9Os?Z^+z@f8NmkxT6IH7CT!nM zHiz`VkvLUhjFpg(@aoMQy5sEg2nagP)f%*0LWlyv@U=9KXhVH{sqLmr60ZxsIi$D? zsQb%Yp`YbNxo=BdQ(CUX4*r^-Gceuz+fg_wTV+t=p>R;HyT zlU~_V4GoRM1Mm=QU7@Vt*TGR;|5%=(8TWWGrcO~yM-0azn#(Wot}ri8z}T1;%nJdu zDUJw|u?zwtB7QP&9}S1G@9-BhM(j5X7d{jt^zUYJ$R`2Cb#XDw?fQJV)e8|E?cq!e z;h(zurFSmpavWKqna5Egd!oL3pm4}*Y6e9{;`}*8Kz+%Uil^vCCVQRqlfpykbIMIq zkAOL1JW>CifLk{jh)*k~EUj;7uvzQHCgyi1rj<#0Co78u&YY(&k2OR)o9D#XCf~#h zQE`L_P(1Bp)&Z}3c=)k~rl!s1;m6-|b+|=EMO#~2kAG2A7^N`sWM7~0(;akyVqkN1Y6@$tM1_QW z4(G*ey@#k}UiF(lq1{z>0jcS9O}sAWOTVWprkdT}-d&a46V@y^znrtIQNuNOJ#yqf zlJaKWUY?prTh^t9K{SA?m(cBuWMnVw8;7Qr6u zGL;AGyK)ZXTN+$)tv9RfL?wuRw98*_hQaz1NE!sq0qDST>e~JIN0-uOC+VzST%3s- z^&$EEmLsms7uh;;3{Ol9y1R3S?R!9x^_Z75(O5D@wfV6*1rtR?@-PrwI;nGWbNjZc z%gcL#^M^sR8WTMB@p-AfWWE#bERCQZ45Nc9P&Mtoe9`L(C7Eh)U=AhY552l_)^<`p z`q`Y=)B-hD?^tX1!>DyUN`GYq^0EY-oa%m|ad*W@u)YWwA?=mQ-SkBpbl@|V5c2O| zzqmd+>LCj{ueV<8MV4Jd;9D;b6ts@u2DAaFhRDJv^zA z!N`KVg!b4aAV9GQJ|rXrwtoo;34QUGfInMKUjBzIG&pEK%3Av#yAwU*BujU0xxc&4Eh!Omad~@pM0SrIMI-GE zK(HtN6cMQH%cXCqb6mGn+IQipo=4VEdHqD+<^=cx_){~>=&DV^+Km7G^sSJpe@^gPhDD7Z9Ad&m3ERh#(`r@4c;{*T;Ns?gWuoSXzl zj>MoKa#yH>wu5uQ5iyMC7&VqbJ-q=hbd=WONxkxLA#d2V$%)@s!!b8sQWR^| zQCz*($#LYR*R}lBFHPG=fCG`DM7bDeaq4nbEUh2DCDb499=GC4#d`SgA#9lR1nupIfmlY+=)rQ~0Bd$A z5zFKyvfZu60yY2vbIT2Dwbf!urI8Iz5^lip+sG1TBs0b0Hj!1I6@|r~RU5D~S0F8S z?4ZOPW-+^=5ph}yySv}9ljDq$myBu?jG=KR*k3q<(aAXdm@|sr3Q4N+)48c=Y->Mg z5X+y^_9jX@b)didZWy6>3^sq-k00>h$%47BCxzF=`u7wIU@n*S>9vJ3~3wfmbjn2)T*~b^El!3@ToF~ zptq<|d4kR`iO)?Vy6xa%msr-=8F5+I8%Zpn5Zto-MF}scEglupSK7Wk5^-~X;0D(YgM;{{lT<%$U1a)_V zD~&L8TM@B%JfFQbLgn|&Z0bgP>OR~yU|gjEA`YXD$zIC24DdDQYCGy4olTfktz^aN z2ue#o%g7DGC3M8Uvwp}f(qgteP=}Y9DGH1_uPV8i&Ay%|4HyGZCIGjU7>a~I;G`Oqw>4hIBz zQB2iGrk2{DyX91(prFVrD*8ZlJxE|Z=vw;++4U zrcALJO}u%9T}NKS2%%GI@>z54JD`x6l3QqW&BW_)YwI)M!PauUQmvmM|$AX}hEwfPwDhqCC5mZ*7S zqmEd{2vw?)KH9`bZ!H>1+s2IzBiF+=6vy47no=<7J|Q45K5gc9IcI>i!JtFKKtx2W zKa$yNW#3sVmvefv{i`E4X>H(ucDj<()8qiOG)dg{=q*>wf&Z<}1ND zbeNg6YCT~y$i~n3?_9PtFw6&x%RxjNKPTy94zswavmJlPp{f>ZwH0eOScA?QG;Z?I zvX=bsHSGmU`gN9MGrXCA_~O_H!>G`(JPo? z7Q&`+KZ<9v&K5iq;GRR&3-m!+Oa!3Chs^~L9hZk^XJ_>`d8VpWTEH6(2{0ea^fpUg z*I@Xp?gSP3@pxu-whS~8j!ilM-egcKeFrIq=&klW(77u9><=45Admpq>U25(4RAD{ zeUdZ{U8k9Hxgw1MHNpi6!|t8K%>a-$)xta%0% z(}$oDTuysAiwln6WTrooIPYW0Zk6IhvEh$z-zAhEH5Cp$?Be>F39=n56V z8{bGmijXkMxH=pH9o`#oECOh(RIdvid`dWw(HN*ERb@raST5Dd9==wd^;$Hz{p859 zObv{CUqRd2c+^dlTU^|I13i(KlM8>6k|VSln6%#l-AFVsF@f{U(hhl6%qKjVhX`MAQ2P(w_%OCh2#Am)ZM7znmYi~XpXrE! z8k5bARQyaZ&3#RfhgJ^&iU4t3{?bt9c%%oyw7@`uiq8v7q856{lap=gyt>+hp+l+r)-G4gYF#z%;c&i%m`({3#iUqv9y}fk=G)Ea2lfm8t zaM;uRM3X_F=X~RS^9EK?8*k5N`;U7KV&&iFb`yuF?~etAalW%Um$6ML+0Gn!16Da8 zP*>OEbVX%kLZ6qK{8I2ZW;f0h4`}RPYvPD}#Ys5LGIAE5d!vY519?lM!Ewz-Df!_~ zX*PgshGxEO=&O6>2C4kWNQnasimzZW28epogJH?8m!*%}B}&sn_|Rzl{QR~K4#;9! zuyK5qA9djde&m-aQ(^;k{eWcr_= zI*b4iHz9%WW=FLJr#+3~chqBnY)N69+IdT13T&;q&kq1W`@(22!4JHjjP!H_Fb9I_ z8`xROwFbI$9#SkL&;kg@V^i#E&SSpttRPSp}!YCBSs7ZlFURqI6H*Q@pHwCE8*`J7F+sKPJt& z44{6B{DK0T9ukMN`g)B|NR$z%;3&0O=|p2=V_SkTm!YJ*`3)(go|Zoq_{zJ*C?Z+` zvl`^(1rH1bJcq4_?WdAoQh0ITdGQR$-aiTu3U`YTM`DV@aBm@l_0qOOyg3cIo>xg1 z86C^RT$QoRv;e5gEiCNJS1u?u9KinxZ~ZmV1;4 zDKn*MX>ecw88J}`-_7ILrNmY-z?lY=Ol|E`(8>!5385k(=rzhWY^<$)*qtoOEhrFj zaBxsaNMcAouLO_db$B<0E@JP`Xesh_L^KMNtLy8L;bBie8{+?v6W{E>^n63LbSs9W zO!oxGZOe|^1|&nS0^+h8G=jm`2Q9jrLIay8Kbo6KVW$AB{m{KX)8$48BNGlE`^?v3 zdWXJR){{)ud?x&*U6hNmEDio##_hK)X^2nvPXJ*1 z1H5Z6h2RqqA37jl3xP6fZ*T9o5pQe_0HO#E?C$8|2>h2NJ^e8+Z8nn~^o~}#U<>m3 z5Zw(J6s*Bshn?|&ZU+TB=qZ_St4(BVui|tgdxZGXgsQIJ;z18sKh>Y60alhGOkw;7 z)7@E>d*&(uQaf~;@%aneJe>Jh0_-xu z-O=)O%G)6mMz)ovS%g*)?mJ;G4E{{FcZqfsIWTAqESj5}P5z$sfw+USBBQ&YJaxYR8a5+Gy>YioGsRV{VtP|l2Te}sT>4F zDF-}zyMG!;SpN=N=izw-a71q7zI$=u{9{Dgkxi3 zmQ(McsrK$)S+QyUQYl}4PkHJ0grYrUMt~S)aDWTQotwV1=p&Q5oyt4%q)=HrE>>@t6OPfXjZGgU8PSm$J`{7%f7J%kdTh4zQ0#X97 zM+m5pX{ATq2olz_5Fo_Mw#niL;CLYhh{onns_bZZ5hr_39N|km02sg8G;hkIWdCx`1t@ zRGbs9QzGWWhs(o1giNfg2+Z?#vUy}44WEG51Z!RjREh)v_B$+&?k5Nkef<7`fd^cJ zyHdLXpox4$##gA>VVb=DTmdk5VmTjoj~wN+w1TA*IbdJ-JF6l3dr3+A8AUJ(u7CbG zQ)x=a#MBK#tbm9f1{i}r3k2*~(nTqOa{Hm&|+P&&oYjEamFPPpWj{#H~ z%p@5N4ZsC4l8`^MdGdMo6Ngj^kNRA{y0$iBH%`%Dro2eY^{x98JiO%&EdI`s3^6d{ zWW0>}y+tNInTvt!0~)Z_05bQl?BZ|d4oc!j* z3CUvp{PO&j0#uQV1LFLn3~%g@>?+LnQe)!mGOUc7CcEJ;?i?*6Ydr;s#Nvt5Hvwnc z3Bw{?E-YbHmm8(Gd^#ZsMH`zraoJGtgFMt=vlJOu>|t~}*uS8)0%YIr&!O5Vo%kh0{|7~@Jz*L3^@L;D(n2~qDZid{ zEn6>zzTF>=z}V)-$>!12)C5mf|1!y^reM%WNJ1j;xrw;r`cd#CIxEm2xR1O#d6QbM|=ML_DlpZlEuo-@Y1 z{}_AhG4|MstZ&V4&gXqjL3z(Pa_fAZNAVkX47a*^2SlZUq0ei`V`2d+> z;G3r&8X7XO+5VkxTTMwo8VehG3yi+n7>o}*w~P-TG~&Ax*NpP-A59dIoebDst4rrceqIX5>q1n9r7{22(2tgP~K%y^xIkPz#zg5d(_{r zgYZo7epy)HgXEF>s;L#{hRo9y3WOBDXa8Au-}m}TSV=q!AtiSMa0nsr1UV)j!*K+E z8pfQj@K}(iHacpUEaUm`w`fsNtzG+J2k5zQ{$x~CTt=S2&#k>RkXI6ul1i*ccpaTt zHlAc1-#A`y5JUbh?A?0f>+xx6BypWF*#d+()z#JTodHG!I|Z4~V|#l(U@RkDnBUa$ zg^xt!=CUD@PM&63%l3Sy@4}=$^Mh)p2idCDQhPON6lyN7q}L9fIL z`TuqmrePk7T0663-qzi78M8)*TmGEAa$=a*`US-tqe&wu8PIIv3Y6hip3Jezc&XCJ zxDuK)!&d>9JmqI6?v0R)NBr>-o-{ZTJoH>~wO=&mojD_vA3b`6!|1Wk41DlrMbWE^ zBr-?*+uPgW%J+ADo7L@4&8wfaifp`jb6?vG9BRwe$So{g;0rfp=8Z5X| zVmZKO^Rt#_etupq>U@<|;1?CnZRR-_34zBQ6jFb_oBGS6msVGszP!0WO6+$oLzb2- z*>%R;8a$ZbzCf5kQ26`I z5nQeVwEeELYs^tr<2_-`CSD_}L#SRKKzPYwj<_JAkjqKWcXobVm9x#Xq=iqzZ#~S7 zFx{{K6CH}xX8n}g9K^WG?*df4G)zcHSmH1(H#jt8yz%pMmCJHuyi6@zhtrn~7Y9{2 zOydG5zECuBtsGzX6%)%uh5Y5TVN~LU!Fr*epaS|`k$kpfSA3_Uz(!7ChZ-kWBP9@k zHHp#jik^UJ3(fcM+i1Zjl$s;(?6nb7>2=0}=>3=5) zUhRwwo3bB>q39dHiRTkfsqPj%3<2sCRS#pyIRlL;k8Ck71>-hUUC-jJCr|P$ZuNi5 zsYkvdac9NaxP$#^=g@j8H->VC8@q#dIQaOeM4#YZ+Hir7;VQ)|GX8Hok4<4iUP*f<-+McO0%&DkDDyHO31u34O12&igKZ;mysI zOR91g!aBcneWfQ>YH4Jq`Ux^ElSS2LcS_CKOO5AH8aQ~>PV3|1Qaj(Se=mJZ2;0b0 zxzq_eC4OoOL&D!Cl@OAOUPlH*@Y4vXlj9L4_=aoJdgnfPO0*6mwu{6T5XW?&BYZ`a zGVdz~=@}=?(q0$!Wk`()#uy9pahjV-vEs(CDRY3@CFD3x34!j{thW zTMhgM#+75uFI0D8_awWF)dwySo!^>zLqc$BCf{%e+UV-zTen>A|0X^D0a6GB#S{YM z5F0E4ksHm_hZg01w8wPBL+Qo68Bh0Hm)6$Q6W^GkwmFqOev<_lUpKZhR8O-!>Po#q z=ehUX)6eK+J5I%vJEn(MaO{)o7W(JQjT`V@mE@9^Py{tQz^iuz{w2J0BXaQ{7nb-s z(glDJDgpnE)Zwo(Rb$~9PtYyO8)OXxH2;9pL6NMb%1zh+Y$xL*~7LieG8eH)9!az=9jz7g(zeB&8H9k zjajd`>a-KLIdfJhh6>f3T(e5ex21GHD;|z>cuW@c+!DMHlOTWa#m!EL%#Hp&6 zBkjBMmw3EUKpO~ECr8axsx~hpzYCYCWEB*s;P=kaC;FGlqnYXP^RC}jX$z1?d(&$j z6wl21`SYMr?N2TZC5@(9GvK_q*@blXQldGl5)1$&J%3)wpAdPapp!|R@WZ)M;Fyfp zRm=^>7l~p5R&GM5R%yC`QLj+n3kc;v*V>{xDL!xIm=PamR@SX@Nm9Y(I>L}g6lsJR zgOz5H2i=kz{-rp2DN*l^%pFbQmDkwQ+{tdo@ghr{gQ7p8#>p+;sJchg>N#0)z*msR z5ZHJ}^M)^S0+pF}sag?geAs#Fz?!#~{9A~2CsE|r9ZqVZOjroGF8Gi+XF z3^*6@JnG+SqloFb!jRS>w>JFaN5GvtjcJ(YCA#itFgc=AI^+)~pHIHhuXei1rk=>E zd>zfA@)GuCk~eSOWZLiNkPX_K1wI3zKNDu#xu!1_eqzSzvn=pX0a#l63B6#E+G%~B z{m#L7-X0c+>fg%)zC8*nXR`$4mIrn~vPhP4)1it@t*op>UP;DRzayr9K4c5C;0bP> z{a$WKK3r(<23&su(pk@P-*9W72PC+ypPZF__>f_}c1J^?-u0!nWc!-R>g!xfYHI2R zWMUW_dp9`BKuAs6pFrOozm`;1RpOXs%|5ZTd&ml`oC?2$_Cng@#t#2Not(Jt9n8Go z%QM-DYFsxtQufW0URJAPlAhG{DeHE-sK}LVnD448$*NzTEF|e#QM5HxwAdEvzrvVc z!ixLW;7F}gd{jY3_h@UJwJ&~Ajxb>sSX;#81my6o2M)5YCaY*)SKI2)%rAH z2jrjei?${mLr~ZdL<9$3jG?HL0VhMu6EKHX&4x;XAv0Adb>B6jHCfGuOk4a(H$CEY z0h>dAvjACSRcvCeWtG!B0q=tvR=h>)bF_>N#U!bnaeP@oQ%R?3>fOz{fnRh-TbtCb zdYFTg?d*s!clH3;;f}0Wpi`0{>|AyY1Tw->JW-E#3CZ z7T^l&bI!VGE#cPo^ZGkPI!3018aP;+C)+oO?hjd!Fuj7VQ`)J{8(dIxidF z((k@#c+IS|rD7lF5%O>$s1g9~jdz)za|aT!jVQ*Kf(DcXzFMGT!yr!(C0iyS>){Vz zBc$ydAtnC6eKdtdavyEWYRecQ&dTbl?=rU8gw(LG{GpCfZ*q3e;K#~wr=ay%iT)ZQ zzb2T`;M&68a${R9!GZ-t=Ry1E1%yyWC<-iAwYn%m_+bCVr2u({rCw&pE@ZO`DG^w) zVD%t#uzGexBzn(VJ{<(uOT2N{3hcI%bN(zmWjE4yX_Gyk05=)n!HE}b?ZOYAduZb& zuz&%VejrbiYR4z(b7Y#NVbT84;+jgmLY6l2B*3rJ1PJsD^<;MYYHPqSFouA5uO`TZ znrq~ulfQW|s#HaE$c~WzzbK#S_BVCBClgol9SC?7`Nd{PvXB-JSd*WihH!T$Q_@9$-aowpW3TzId63phfc@zhkh1<4fF z6AfW5_FpjP!rhXR>-DD|Faa*Lk)y|5WpG}obN!u_ryWALC6F~Td->7q>lik&!GLsD zLEmpHA1wz;oB!Jj0EB9h`}{SaUBNa+d2r{ISq8`DDkcT*>r}FqL|ZlO8h!_yGP^Nq zAE7J^oG<_Jc~diRYh3{g!y9lBFXK5SXicwact3#DMP>c5?Xrz#GY$xr#$P`(8_#c@4(&l^mq@%!Of+tu*}R%xl5F@C8YDS$v$K7 z1Ypax8T+WNcb7oP+8ZuCAa1cFYBdqS+`Vf8v4iimZejstzB*T z6ZukL3uuMWIU+5MZ!~1sOk<|^ukudXu$^ky0%ZhifIKdOIzfS0Sy{o6RdQ&zmU!O5 z7^1kf@~+4PmLqVziYh89Ew5q;1~r6wEHwsbZ{x9~8x>g+>0&VW>?iJO?HgyXeD4{5 z*WsnJ^nk>9oA0v^kS0rilTR0?`qWXU_pR8Ve#VVA*@Q?=O*nsdXx2>Da3eS&?F z&E<*R#H5x;&ogxHC*+C_Zxvlt=1&jdu;d5zdun&YNJ(8ixw|tzG8w2rusIz(4|l;&j zn!7Y3s3TbeHW5u*Rm<5uvz^PETQCTMCd#c$eIlJFPICXL6;Bca50J{hiu98@k7Gh_ z{j|@{GPv1e#;6e;Yhx}NYb}swzg@>f8#05)k%&3P50`#>Kj##N93&L*`rXMvl}O$= zFGi{V8v!2f()zmM%a@>evinbsOiB+^sY0q@pj5rP~oRF6NE)Lw8+V22Z>ZuLE7WoQHR9c0IoU%kL9nTtGCK(w8u8pYYbiu z0Ek8pD2NV;wU;MnJ@|d6xt=nK^㿥KJG?W2Bx!0quF;-1#xCanKap>f!OoyFF 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 Cw;Y=Q literal 0 HcmV?d00001 diff --git a/src/entities/CategoryCard/CategoryCard.tsx b/src/entities/CategoryCard/CategoryCard.tsx index ed33804a..cbf2c2f4 100644 --- a/src/entities/CategoryCard/CategoryCard.tsx +++ b/src/entities/CategoryCard/CategoryCard.tsx @@ -2,7 +2,7 @@ import { FC } from 'react' import Paragraph from '@/shared/ui/Paragraph/Paragraph' -import card1 from '../../assets/images/categoryCards/img-categories-01-210x263.webp' +import card1 from '../../assets/images/categoryCards/placeholder-1200x800.png' import { TCategory } from '../../models/CategoryModel' import styles from './CategoryCard.module.scss' @@ -16,10 +16,7 @@ export const CategoryCard: FC = ({ category }) => {

    {category && ( <> - {/* {category.name} */}{' '} - {/* когда будут картинки в бэке, можно будет воспользоваться этой строчкой */} - {category.name}{' '} - {/* временная заглушка */} + {category.name} {category.name} )} diff --git a/src/entities/CategoryCard/categorySlice.tsx b/src/entities/CategoryCard/categorySlice.tsx index 33b6c2dd..90a0e143 100644 --- a/src/entities/CategoryCard/categorySlice.tsx +++ b/src/entities/CategoryCard/categorySlice.tsx @@ -1,12 +1,9 @@ import { createSlice } from '@reduxjs/toolkit' import { rejectedPayloadHandle } from '@/shared/api/rejectedPayloadHandle' -// import { apiErrorIdentify } from '@/shared/api/apiErrorIdentify'; -// import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types'; import { REDUCER_CATEGORY } from '@/shared/constants/constants' import { getCategoryCard } from './getCategoryCard' -// import { TCategory } from '../../models/CategoryModel'; import { ICategorySchema } from './types' const initialState: ICategorySchema = { @@ -39,11 +36,10 @@ export const categoriesProductsSlice = createSlice({ if (payload !== null && typeof payload === 'object') { state.error = rejectedPayloadHandle(payload) } else { - state.error = null // обработка для случая, когда payload === null + state.error = null } }) } }) -// Экспортируем редуктор среза export const { actions: categoriesActions, reducer: categoriesProductsReducer } = categoriesProductsSlice From b70afe0b3f764ec9409c9dc496f7ccb1e55c699f Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Thu, 2 May 2024 16:43:50 +0300 Subject: [PATCH 19/66] fix-4 enhancement_333_sidebar_menu --- src/features/SideBar/ui/SideBar.tsx | 18 +++++++++++++----- src/pages/FormReturnPage/FormReturnPage.tsx | 2 +- src/widgets/SideBarMenu/ui/SideBarMenu.tsx | 19 ++++++++++++++----- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/features/SideBar/ui/SideBar.tsx b/src/features/SideBar/ui/SideBar.tsx index 49a6ce60..2babfbdd 100644 --- a/src/features/SideBar/ui/SideBar.tsx +++ b/src/features/SideBar/ui/SideBar.tsx @@ -1,4 +1,4 @@ -import { KeyboardEvent, ReactElement, useState, type FC } from 'react' +import { KeyboardEvent, KeyboardEventHandler, ReactElement, useState, type FC } from 'react' import ArrowIcon from '@/assets/images/sideBarMenu/IconArrowDown.svg' import Paragraph from '@/shared/ui/Paragraph/Paragraph' @@ -9,6 +9,7 @@ export interface ISideBar { title?: string isVisible?: boolean onClick?: () => void + onKeyUp?: KeyboardEventHandler children?: ReactElement | JSX.Element | JSX.Element[] } @@ -16,18 +17,19 @@ export interface ISideBar { * Компонент SideBar - кнопка, раскрывающаяся в бургер меню * @param {string} title - название разворачивающейся кнопки; * @param {boolean} isVisible - атрибут дающий видимость иконке стрелочки; - * @param {function} onClick - название разворачивающейся кнопки; * + * @param {function} onClick - функция выхода из профиля handleLogOut; + * @param {function} onKeyUp - функция выхода из профиля handleLogOut при нажатии клавиши Enter; * @param {JSX.Element} children - контент; */ -const SideBar: FC = ({ title, isVisible, onClick, children }) => { +const SideBar: FC = ({ title, isVisible, onClick, onKeyUp, children }) => { const [isActive, setIsActive] = useState(false) const handleClick = () => { setIsActive(!isActive) } - const onKeyDown = (e: KeyboardEvent) => { + const handleKeyDown = (e: KeyboardEvent) => { if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() e.stopPropagation() @@ -36,7 +38,13 @@ const SideBar: FC = ({ title, isVisible, onClick, children }) => { } return ( -
  • +
  • {title} {isVisible && ( diff --git a/src/pages/FormReturnPage/FormReturnPage.tsx b/src/pages/FormReturnPage/FormReturnPage.tsx index ab6f4d4b..47911e27 100644 --- a/src/pages/FormReturnPage/FormReturnPage.tsx +++ b/src/pages/FormReturnPage/FormReturnPage.tsx @@ -16,7 +16,7 @@ const links = [ ] const FormReturnPage: FC = () => { - const [user, setUser] = useState('Elon Musk') // позже юзера будем получать из редакса + const [user, setUser] = useState('Моругина Мария') // позже юзера будем получать из редакса const handleLogOut = () => { setUser('') diff --git a/src/widgets/SideBarMenu/ui/SideBarMenu.tsx b/src/widgets/SideBarMenu/ui/SideBarMenu.tsx index 1dbcf895..c595cc2e 100644 --- a/src/widgets/SideBarMenu/ui/SideBarMenu.tsx +++ b/src/widgets/SideBarMenu/ui/SideBarMenu.tsx @@ -1,4 +1,5 @@ import { KeyboardEvent, FC } from 'react' +import { useNavigate } from 'react-router-dom' import SideBar from '@/features/SideBar' import { userData, noUserData } from '@/mockData/sideBarProfileData' @@ -19,13 +20,21 @@ export interface ISideBarMenu { */ const SideBarMenu: FC = ({ user, handleLogOut }) => { + const navigate = useNavigate() + const data = user ? userData : noUserData - const onKeyDown = (e: KeyboardEvent) => { + const handleKeyDown = (e: KeyboardEvent, index: string) => { if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() + navigate(index) + } + } - console.log('Link') + const handleKeyUp = (e: KeyboardEvent) => { + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault() + handleLogOut() } } @@ -46,8 +55,8 @@ const SideBarMenu: FC = ({ user, handleLogOut }) => {
  • handleKeyDown(e, el.route)} + to={el.route} className={styles.sideBar__sublink}> {el.subtitle} @@ -57,7 +66,7 @@ const SideBarMenu: FC = ({ user, handleLogOut }) => { ))} - {user && } + {user && } ) } From d01891b81102133a8852a9cac7deaf8ec1a20e29 Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Thu, 2 May 2024 16:49:06 +0300 Subject: [PATCH 20/66] fix-4 enhancement_333_sidebar_menu --- src/widgets/SideBarMenu/ui/SideBarMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets/SideBarMenu/ui/SideBarMenu.tsx b/src/widgets/SideBarMenu/ui/SideBarMenu.tsx index c595cc2e..8a612761 100644 --- a/src/widgets/SideBarMenu/ui/SideBarMenu.tsx +++ b/src/widgets/SideBarMenu/ui/SideBarMenu.tsx @@ -15,8 +15,8 @@ export interface ISideBarMenu { /** * Компонент SideBarMenu раскрывающийся в бургер меню - * @param {user} string - название разворачивающейся кнопки; - * @param {handleLogOut} function - название разворачивающейся кнопки; * + * @param {string} user - данные пользователя; + * @param {function} handleLogOut - функция выхода из профиля handleLogOut; */ const SideBarMenu: FC = ({ user, handleLogOut }) => { From 4a305111aebbe2a8c76cb96bec507ec1a2749d4a Mon Sep 17 00:00:00 2001 From: Kseniya Date: Thu, 2 May 2024 18:42:14 +0300 Subject: [PATCH 21/66] enhancement/types, categorySlice, categorySelector --- src/entities/CategoryCard/categorySlice.tsx | 45 --------------------- src/entities/CategoryCard/selectors.tsx | 6 --- src/entities/CategoryCard/types.tsx | 11 ----- 3 files changed, 62 deletions(-) delete mode 100644 src/entities/CategoryCard/categorySlice.tsx delete mode 100644 src/entities/CategoryCard/selectors.tsx diff --git a/src/entities/CategoryCard/categorySlice.tsx b/src/entities/CategoryCard/categorySlice.tsx deleted file mode 100644 index 90a0e143..00000000 --- a/src/entities/CategoryCard/categorySlice.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' - -import { rejectedPayloadHandle } from '@/shared/api/rejectedPayloadHandle' -import { REDUCER_CATEGORY } from '@/shared/constants/constants' - -import { getCategoryCard } from './getCategoryCard' -import { ICategorySchema } from './types' - -const initialState: ICategorySchema = { - isLoading: false, - category: { - name: '', - count: 0, - next: '', - previous: '', - results: [] - } -} - -export const categoriesProductsSlice = createSlice({ - name: REDUCER_CATEGORY, - initialState, - reducers: {}, - extraReducers: builder => { - builder - .addCase(getCategoryCard.pending, state => { - state.isLoading = true - state.error = null - }) - .addCase(getCategoryCard.fulfilled, (state, { payload }) => { - state.isLoading = false - state.category = payload - }) - .addCase(getCategoryCard.rejected, (state, { payload }) => { - state.isLoading = false - if (payload !== null && typeof payload === 'object') { - state.error = rejectedPayloadHandle(payload) - } else { - state.error = null - } - }) - } -}) - -export const { actions: categoriesActions, reducer: categoriesProductsReducer } = categoriesProductsSlice diff --git a/src/entities/CategoryCard/selectors.tsx b/src/entities/CategoryCard/selectors.tsx deleted file mode 100644 index a4ea5c52..00000000 --- a/src/entities/CategoryCard/selectors.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { RootState } from './types' -import { Category } from './types' - -export const selectCategory = (state: RootState, categoryId: string): Category | undefined => { - return state.categories.find(category => category.id === categoryId) -} diff --git a/src/entities/CategoryCard/types.tsx b/src/entities/CategoryCard/types.tsx index 87d32bf8..be8f54d0 100644 --- a/src/entities/CategoryCard/types.tsx +++ b/src/entities/CategoryCard/types.tsx @@ -12,14 +12,3 @@ export interface CategoryInfo { previous: string results: Category[] } - -// Тип данных для состояния -export interface RootState { - categories: Category[] // массив категорий -} - -export interface ICategorySchema { - isLoading: boolean - category: CategoryInfo - error?: string | string[] | null | undefined -} From 553c3f2ce58e019cd6c6b57043e8640156ab994c Mon Sep 17 00:00:00 2001 From: kirill-k88 Date: Thu, 2 May 2024 20:41:12 +0300 Subject: [PATCH 22/66] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=84=D0=BE=D1=80=D0=BC=D1=83=20=D0=BD=D0=B0=20Fromik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/SubscribeForm/SubscribeForm.tsx | 61 ++++++++++++++----- .../model/constants/constants.ts | 1 + .../SubscribeForm/model/types/types.ts | 3 + .../validationSchema/validationSchema.ts | 7 +++ .../SubscribeForm/subscribeForm.module.scss | 7 +++ src/widgets/Footer/Footer.tsx | 9 ++- src/widgets/Footer/footer.module.scss | 10 +-- 7 files changed, 74 insertions(+), 24 deletions(-) create mode 100644 src/features/SubscribeForm/model/constants/constants.ts create mode 100644 src/features/SubscribeForm/model/types/types.ts create mode 100644 src/features/SubscribeForm/model/validationSchema/validationSchema.ts diff --git a/src/features/SubscribeForm/SubscribeForm.tsx b/src/features/SubscribeForm/SubscribeForm.tsx index 4282c9ca..aa9d2dfc 100644 --- a/src/features/SubscribeForm/SubscribeForm.tsx +++ b/src/features/SubscribeForm/SubscribeForm.tsx @@ -1,15 +1,19 @@ import classNames from 'classnames' -import { type FC, FormEvent } from 'react' +import { Form, Formik } from 'formik' +import { useState, type FC } from 'react' import SubscribeIcon from '@/assets/images/subscriptionForm/icon-subsc.svg' +import { FormMsg } from '@/shared/ui/FormMsg/FormMsg' import { Input, InputSize, InputTheme } from '@/shared/ui/Input/Input' +import Label from '@/shared/ui/Label/Label' +import { validationSchema } from './model/validationSchema/validationSchema' import styles from './subscribeForm.module.scss' type TSubscribeForm = { type: 'footer' | 'subscribe' className?: string - onSubmit: (event: FormEvent) => void + onSubmit: () => void } // @TODO: Перевести форму на Formik + Yup @@ -20,6 +24,8 @@ type TSubscribeForm = { * @param {string} onSubmit - функция для обработки формы */ const SubscribeForm: FC = ({ type, onSubmit, className = '' }) => { + const [showApiErrorMsg, setShowApiErrorMsg] = useState(false) + const classNameContainer = classNames(styles.container, { [styles.container]: true, [styles.container_footer]: type === 'footer', @@ -36,20 +42,45 @@ const SubscribeForm: FC = ({ type, onSubmit, className = '' }) = [styles.form_subscribe]: type === 'subscribe' }) + const submitHandle = () => { + onSubmit() + } + + const onErrorMsgClose = () => { + setShowApiErrorMsg(false) + } + return ( -
    - {/* @TODO: Добавить компонент Label - https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/102 */} - -

    Мы не будем присылать вам спам. Только скидки и выгодные предложения

    -
    - - -
    -
    + + {({ isSubmitting, errors }) => ( +
    + +
    + + +
    + {showApiErrorMsg && ( + + )} + + )} +
    ) } diff --git a/src/features/SubscribeForm/model/constants/constants.ts b/src/features/SubscribeForm/model/constants/constants.ts new file mode 100644 index 00000000..883f1217 --- /dev/null +++ b/src/features/SubscribeForm/model/constants/constants.ts @@ -0,0 +1 @@ +export const EMAIL_VALIDATION_ERROR = 'E-mail адрес введён неверно!' diff --git a/src/features/SubscribeForm/model/types/types.ts b/src/features/SubscribeForm/model/types/types.ts new file mode 100644 index 00000000..fce635ed --- /dev/null +++ b/src/features/SubscribeForm/model/types/types.ts @@ -0,0 +1,3 @@ +export interface ISubscribeFormValues { + email: string +} diff --git a/src/features/SubscribeForm/model/validationSchema/validationSchema.ts b/src/features/SubscribeForm/model/validationSchema/validationSchema.ts new file mode 100644 index 00000000..45ea026b --- /dev/null +++ b/src/features/SubscribeForm/model/validationSchema/validationSchema.ts @@ -0,0 +1,7 @@ +import * as Yup from 'yup' + +import { EMAIL_VALIDATION_ERROR } from '../constants/constants' + +export const validationSchema = Yup.object().shape({ + email: Yup.string().required(EMAIL_VALIDATION_ERROR).email(EMAIL_VALIDATION_ERROR) +}) diff --git a/src/features/SubscribeForm/subscribeForm.module.scss b/src/features/SubscribeForm/subscribeForm.module.scss index a6bcd5da..aa531dde 100644 --- a/src/features/SubscribeForm/subscribeForm.module.scss +++ b/src/features/SubscribeForm/subscribeForm.module.scss @@ -115,3 +115,10 @@ line-height: 16px; } } + +.subscribeform__msg { + width: 300px; + position: fixed; + top: 50px; + left: 50px; +} diff --git a/src/widgets/Footer/Footer.tsx b/src/widgets/Footer/Footer.tsx index cc08be75..ff7757bb 100644 --- a/src/widgets/Footer/Footer.tsx +++ b/src/widgets/Footer/Footer.tsx @@ -1,12 +1,12 @@ import { useState, useEffect } from 'react' import Skeleton from 'react-loading-skeleton' import 'react-loading-skeleton/dist/skeleton.css' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' -import { AppDispatch } from '@/app/providers/StoreProvider/config/store' import Payments from '@/entities/Payments/Payments' import CallBack from '@/features/CallBack' import SubscribeForm from '@/features/SubscribeForm/SubscribeForm' +import { useAppDispatch } from '@/shared/libs/hooks/store' import { Button } from '@/shared/ui/Button/Button' import Link from '@/shared/ui/Link/Link' import Logo from '@/shared/ui/logo/Logo' @@ -19,7 +19,7 @@ import { getCoreBaseFooterSelector } from './model/selectors/selectors' import { getCoreBaseFooter } from './model/services/getCoreBaseFooter' function Footer() { - const dispatch = useDispatch() + const dispatch = useAppDispatch() const coreBaseData = useSelector(getCoreBaseFooterSelector) const [isModalOpen, setIsModalOpen] = useState(false) const [isModalClosing, setIsModalClosing] = useState(false) @@ -76,7 +76,6 @@ function Footer() {
  • @@ -92,7 +91,7 @@ function Footer() { Created by{' '} - maxboom.ru + {coreBaseData.footer.disclaimer} diff --git a/src/widgets/Footer/footer.module.scss b/src/widgets/Footer/footer.module.scss index 8393eca5..9ffec10f 100644 --- a/src/widgets/Footer/footer.module.scss +++ b/src/widgets/Footer/footer.module.scss @@ -30,10 +30,12 @@ margin: 0; padding: 0; height: 25px; - font-size: 15px; + font-size: 14.5px; + font-style: normal; font-weight: 400; line-height: 25px; - opacity: 0.5; + color: var.$white; + opacity: 0.8; margin-top: 24px; } @@ -88,7 +90,6 @@ font-size: 13px; font-weight: 400; line-height: 15.6px; - margin-bottom: 11.5px; } &__link { @@ -118,7 +119,8 @@ font-style: normal; font-weight: 500; line-height: 18.2px; - opacity: 0.4; + color: var.$white; + opacity: 0.8; } &__bottom { From 61827b6ab909c9d912c69adc1279b583ba1eff54 Mon Sep 17 00:00:00 2001 From: Yulia Avramenko Date: Fri, 3 May 2024 14:58:15 +0300 Subject: [PATCH 23/66] #316-api-renew-product-amount --- .../ui/ProductEntity/ProductEntity.tsx | 4 +-- .../model/services/putRenewProductAmount.ts | 30 ++++++++++++++++++ .../model/slice/productAmountSlice.ts | 20 ++++++++++-- src/features/CartEdit/model/types.ts | 7 +++++ .../CartEdit/ui/CartEdit/CartEdit.tsx | 31 ++++++++++++++++--- src/shared/api/types.ts | 3 +- 6 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 src/features/CartEdit/model/services/putRenewProductAmount.ts diff --git a/src/entities/ProductEntity/ui/ProductEntity/ProductEntity.tsx b/src/entities/ProductEntity/ui/ProductEntity/ProductEntity.tsx index 59cdbb8d..b02dc0ac 100644 --- a/src/entities/ProductEntity/ui/ProductEntity/ProductEntity.tsx +++ b/src/entities/ProductEntity/ui/ProductEntity/ProductEntity.tsx @@ -18,9 +18,9 @@ export const ProductEntity: FC = product => { return (
    - {product.images.length > 0 && ( + {(product.images.length > 0 && ( {'product'} - )} + )) ||
    }
    {product.id} diff --git a/src/features/CartEdit/model/services/putRenewProductAmount.ts b/src/features/CartEdit/model/services/putRenewProductAmount.ts new file mode 100644 index 00000000..89bf6645 --- /dev/null +++ b/src/features/CartEdit/model/services/putRenewProductAmount.ts @@ -0,0 +1,30 @@ +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 { IProductCartList } from '@/shared/model/types/ProductCartListModel' + +import { IRenewProductAmountRequest } from '../types' + +export const putRenewProductAmount = createAsyncThunk< + IProductCartList, + IRenewProductAmountRequest, + ThunkConfig +>('cart-renew-product-amount', async (request, thunkAPI) => { + const { rejectWithValue, extra } = thunkAPI + try { + const { data } = await extra.api.put( + `api/${ApiRoutes.RENEW_PRODUCT_AMOUNT}${request.cart}/`, + { + product: request.product, + cart: request.cart, + amount: (request.amount === 0 && 1) || request.amount + }, + { withCredentials: true } + ) + return data + } catch (error) { + return rejectWithValue(apiErrorIdentify(error, ApiErrorTypes.DATA_EMPTY_ERROR)) + } +}) diff --git a/src/features/CartEdit/model/slice/productAmountSlice.ts b/src/features/CartEdit/model/slice/productAmountSlice.ts index 62ff426b..898fac85 100644 --- a/src/features/CartEdit/model/slice/productAmountSlice.ts +++ b/src/features/CartEdit/model/slice/productAmountSlice.ts @@ -4,6 +4,7 @@ import { rejectedPayloadHandle } from '@/shared/api/rejectedPayloadHandle' import { putDecreaseProductAmount } from '../services/putDecreaseProductAmount' import { putIncreaseProductAmount } from '../services/putIncreaseProductAmount' +import { putRenewProductAmount } from '../services/putRenewProductAmount' import { IProductAmountStateSchema } from '../types' const initialState: IProductAmountStateSchema = { @@ -25,7 +26,8 @@ const initialState: IProductAmountStateSchema = { full_price: 0, full_weight: 0 }, - isDecreaseSuccessful: false + isDecreaseSuccessful: false, + isRenewProductAmountSuccessful: false } export const productAmountSlice = createSlice({ @@ -40,7 +42,6 @@ export const productAmountSlice = createSlice({ builder .addCase(putIncreaseProductAmount.pending, state => { state.isIncreaseSuccessful = false - state.isDecreaseSuccessful = false }) .addCase(putIncreaseProductAmount.fulfilled, (state, { payload }) => { state.isIncreaseSuccessful = true @@ -62,6 +63,21 @@ export const productAmountSlice = createSlice({ state.isDecreaseSuccessful = false state.error = rejectedPayloadHandle(payload) }) + + .addCase(putRenewProductAmount.pending, state => { + state.isRenewProductAmountSuccessful = false + }) + .addCase(putRenewProductAmount.fulfilled, (state, { payload }) => { + state.isRenewProductAmountSuccessful = true + state.productList = { + ...state.productList, + amount: payload.amount + } + }) + .addCase(putRenewProductAmount.rejected, (state, { payload }) => { + state.isRenewProductAmountSuccessful = false + state.error = rejectedPayloadHandle(payload) + }) } }) diff --git a/src/features/CartEdit/model/types.ts b/src/features/CartEdit/model/types.ts index 55789200..0ae61009 100644 --- a/src/features/CartEdit/model/types.ts +++ b/src/features/CartEdit/model/types.ts @@ -3,6 +3,13 @@ import { IProductCartList } from '@/shared/model/types/ProductCartListModel' export interface IProductAmountStateSchema { isIncreaseSuccessful: boolean isDecreaseSuccessful: boolean + isRenewProductAmountSuccessful: boolean productList: IProductCartList error?: string | string[] } + +export interface IRenewProductAmountRequest { + product: number + cart: number + amount: number +} diff --git a/src/features/CartEdit/ui/CartEdit/CartEdit.tsx b/src/features/CartEdit/ui/CartEdit/CartEdit.tsx index df0f7263..c278bd74 100644 --- a/src/features/CartEdit/ui/CartEdit/CartEdit.tsx +++ b/src/features/CartEdit/ui/CartEdit/CartEdit.tsx @@ -13,6 +13,7 @@ import Subheading from '@/shared/ui/Subheading/Subheading' import { getProductListSelector } from '../../model/selectors' import { putDecreaseProductAmount } from '../../model/services/putDecreaseProductAmount' import { putIncreaseProductAmount } from '../../model/services/putIncreaseProductAmount' +import { putRenewProductAmount } from '../../model/services/putRenewProductAmount' import { productAmountActions } from '../../model/slice/productAmountSlice' import styles from './CartEdit.module.scss' @@ -32,10 +33,13 @@ export type TCartEditProps = { // eslint-disable-next-line @typescript-eslint/no-unused-vars export const CartEdit: React.FC = ({ cartId, productList }: TCartEditProps) => { const [needToOpenContextMenuButtonDots, setNeedToOpen] = useState(false) + const EMPTY = '' const dispatch = useAppDispatch() const productListState: IProductCartList = useSelector(getProductListSelector) + const [value, setValue] = useState(EMPTY) + function deleteProductHandler() { setNeedToOpen(false) // removeProduct(product.id) переделать на вызов action https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/319 @@ -50,17 +54,36 @@ export const CartEdit: React.FC = ({ cartId, productList }: TCar function decreaseAmountHandler() { dispatch(putDecreaseProductAmount(productListState.product.id)) - // tbd https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/318 } - function setAmountHandler() { - //tbd https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/316 + function setAmountHandler(e: React.ChangeEvent) { + const newValue = Number(e.target.value) + + if (Number.isInteger(newValue) && newValue > 0) { + dispatch( + putRenewProductAmount({ + product: productList.product.id, + cart: cartId, + amount: newValue + }) + ) + } else { + setValue(EMPTY) + } } useEffect(() => { dispatch(productAmountActions.setProductList(productList)) }, [productList]) + useEffect(() => { + if (productListState.amount === 0) { + setValue(EMPTY) + } else { + setValue(String(productListState.amount)) + } + }, [productListState.amount]) + return ( <>
    @@ -88,7 +111,7 @@ export const CartEdit: React.FC = ({ cartId, productList }: TCar Date: Fri, 3 May 2024 16:22:35 +0300 Subject: [PATCH 24/66] enhancement_338_sidebar_menu_modal --- src/assets/icons/iconCategory.svg | 3 + .../images/sideBarMenu/iconCategory.svg | 3 + src/entities/SideBarButton/index.tsx | 2 + .../ui/SideBarButton.module.scss | 20 +++++ .../ui/SideBarButton.stories.tsx | 17 ++++ .../SideBarButton/ui/SideBarButton.tsx | 31 +++++++ .../SideBarLink/SideBarLink.module.scss | 22 +++++ .../SideBarLink/SideBarLink.tsx | 32 ++++++++ .../SideBarSublinks.module.scss | 41 ++++++++++ .../SideBarSublinks/SideBarSublinks.tsx | 76 +++++++++++++++++ src/features/SideBarMenuModal/index.tsx | 2 + .../SideBarMenuModal/model/data/data.ts | 54 ++++++++++++ .../ui/SideBarMenuModal.module.scss | 38 +++++++++ .../ui/SideBarMenuModal.stories.tsx | 34 ++++++++ .../SideBarMenuModal/ui/SideBarMenuModal.tsx | 82 +++++++++++++++++++ src/pages/FormReturnPage/FormReturnPage.tsx | 79 ++++++++++++++---- 16 files changed, 520 insertions(+), 16 deletions(-) create mode 100644 src/assets/icons/iconCategory.svg create mode 100644 src/assets/images/sideBarMenu/iconCategory.svg create mode 100644 src/entities/SideBarButton/index.tsx create mode 100644 src/entities/SideBarButton/ui/SideBarButton.module.scss create mode 100644 src/entities/SideBarButton/ui/SideBarButton.stories.tsx create mode 100644 src/entities/SideBarButton/ui/SideBarButton.tsx create mode 100644 src/features/SideBarMenuModal/SideBarLink/SideBarLink.module.scss create mode 100644 src/features/SideBarMenuModal/SideBarLink/SideBarLink.tsx create mode 100644 src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.module.scss create mode 100644 src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.tsx create mode 100644 src/features/SideBarMenuModal/index.tsx create mode 100644 src/features/SideBarMenuModal/model/data/data.ts create mode 100644 src/features/SideBarMenuModal/ui/SideBarMenuModal.module.scss create mode 100644 src/features/SideBarMenuModal/ui/SideBarMenuModal.stories.tsx create mode 100644 src/features/SideBarMenuModal/ui/SideBarMenuModal.tsx diff --git a/src/assets/icons/iconCategory.svg b/src/assets/icons/iconCategory.svg new file mode 100644 index 00000000..9557b4b7 --- /dev/null +++ b/src/assets/icons/iconCategory.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/sideBarMenu/iconCategory.svg b/src/assets/images/sideBarMenu/iconCategory.svg new file mode 100644 index 00000000..e49cbebf --- /dev/null +++ b/src/assets/images/sideBarMenu/iconCategory.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/entities/SideBarButton/index.tsx b/src/entities/SideBarButton/index.tsx new file mode 100644 index 00000000..a55ea03a --- /dev/null +++ b/src/entities/SideBarButton/index.tsx @@ -0,0 +1,2 @@ +import SideBarButton from './ui/SideBarButton' +export default SideBarButton diff --git a/src/entities/SideBarButton/ui/SideBarButton.module.scss b/src/entities/SideBarButton/ui/SideBarButton.module.scss new file mode 100644 index 00000000..8f690a3a --- /dev/null +++ b/src/entities/SideBarButton/ui/SideBarButton.module.scss @@ -0,0 +1,20 @@ +@use '@/shared/styles/utils/variables' as var; + +.sideBarButton { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + width: fit-content; + background: var.$white; + font-size: 14px; + color: var.$body-color; + fill: var.$body-color; + padding: 5px 15px; + transition: 0.25s; +} + +.sideBarButton:hover { + color: var.$theme-secondary-color; + fill: var.$theme-secondary-color; +} diff --git a/src/entities/SideBarButton/ui/SideBarButton.stories.tsx b/src/entities/SideBarButton/ui/SideBarButton.stories.tsx new file mode 100644 index 00000000..08499064 --- /dev/null +++ b/src/entities/SideBarButton/ui/SideBarButton.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import SideBarButton from './SideBarButton' + +const meta = { + title: 'entities/SideBarButton', + component: SideBarButton, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/src/entities/SideBarButton/ui/SideBarButton.tsx b/src/entities/SideBarButton/ui/SideBarButton.tsx new file mode 100644 index 00000000..d2010c11 --- /dev/null +++ b/src/entities/SideBarButton/ui/SideBarButton.tsx @@ -0,0 +1,31 @@ +import { FC } from 'react' + +import CategoryIcon from '@/assets/images/sideBarMenu/iconCategory.svg' +import { Button, ButtonDesign, ButtonSize } from '@/shared/ui/Button/Button' + +import styles from './SideBarButton.module.scss' + +interface ISideBarButton { + onClick: () => void +} + +/** + * Компонент кнопки "Меню" - для адаптива Side Bar Menu + * @param {function} onClick - функция клика для открытия модального окна SideBarMenuModal + */ + +const SideBarButton: FC = ({ onClick }) => { + return ( + + ) +} + +export default SideBarButton diff --git a/src/features/SideBarMenuModal/SideBarLink/SideBarLink.module.scss b/src/features/SideBarMenuModal/SideBarLink/SideBarLink.module.scss new file mode 100644 index 00000000..5a60bd29 --- /dev/null +++ b/src/features/SideBarMenuModal/SideBarLink/SideBarLink.module.scss @@ -0,0 +1,22 @@ +@use '@/shared/styles/utils/variables' as var; + +.sideBarLink { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + background: var.$body-bg; + min-height: 45px; + border-radius: 5px; + padding: 10px 15px; + cursor: pointer; + + &__paragraph { + font-size: 15px; + color: var.$body-color; + } + + &__arrow { + transform: rotate(270deg); + } +} diff --git a/src/features/SideBarMenuModal/SideBarLink/SideBarLink.tsx b/src/features/SideBarMenuModal/SideBarLink/SideBarLink.tsx new file mode 100644 index 00000000..1ec32ccc --- /dev/null +++ b/src/features/SideBarMenuModal/SideBarLink/SideBarLink.tsx @@ -0,0 +1,32 @@ +import { KeyboardEventHandler, FC } from 'react' + +import ArrowIcon from '@/assets/images/sideBarMenu/IconArrowDown.svg' +import Paragraph from '@/shared/ui/Paragraph/Paragraph' + +import styles from './SideBarLink.module.scss' + +export interface ISideBarLink { + isVisible?: boolean + onKeyUp?: KeyboardEventHandler + onClick?: () => void + title?: string +} + +/** + * Компонент модального окна SideBarMenuModal, отвечающий за развертывание названий обьектов массива + * @param {boolean} isVisible - булево значение скрывающее стрелку; + * @param {function} onKeyUp - функция обнуляющая пользователя по нажатии клавиши Enter; + * @param {function} onClick - функция клика по роуту; + * @param {string} title - название роута; + */ + +const SideBarLink: FC = ({ isVisible, onKeyUp, onClick, title }) => { + return ( +
    + {title} + {isVisible && } +
    + ) +} + +export default SideBarLink diff --git a/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.module.scss b/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.module.scss new file mode 100644 index 00000000..84c422fe --- /dev/null +++ b/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.module.scss @@ -0,0 +1,41 @@ +@use '@/shared/styles/utils/variables' as var; + +.sideBarSublinks { + display: flex; + flex-direction: column; + align-items: center; + background: var.$white; + width: 100%; + border-radius: 5px; + + &__header { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + margin-bottom: 18px; + cursor: pointer; + } + + &__headerArrow { + transform: rotate(90deg); + } + + &__routes { + display: flex; + flex-direction: column; + gap: 5px; + width: 100%; + } + + &__route { + display: flex; + align-items: center; + background: var.$body-bg; + min-height: 45px; + border-radius: 5px; + font-size: 15px; + color: var.$body-color; + padding: 10px 10px 10px 15px; + } +} diff --git a/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.tsx b/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.tsx new file mode 100644 index 00000000..0e76d1d9 --- /dev/null +++ b/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.tsx @@ -0,0 +1,76 @@ +import { KeyboardEvent, FC } from 'react' +import { useNavigate } from 'react-router-dom' + +import ArrowIcon from '@/assets/images/sideBarMenu/IconArrowDown.svg' +import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' +import Link from '@/shared/ui/Link/Link' + +import styles from './SideBarSublinks.module.scss' + +export interface IData { + routes?: IRoute[] + subtitle?: string + route?: string +} + +export interface IRoute { + subtitle?: string + route?: string +} + +export interface ISideBarSublinks { + isActive?: boolean + choice?: number + index?: number + item?: IData + title?: string +} + +/** + * Компонент модального окна SideBarMenuModal, отвечающий за развертывание роутов и их названий + * @param {boolean} isActive - булево значение; + * @param {number} choice - изменяемое состояние индекса; + * @param {number} index - индекс выбранной кнопки; + * @param {object} item - обьект массива; + * @param {string} title - заголовок обьекта массива; + */ + +const SideBarSublinks: FC = ({ isActive, choice, index, item, title }) => { + const navigate = useNavigate() + + const handleKeyDown = (e: KeyboardEvent, index: string) => { + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault() + navigate(index) + } + } + + return ( + <> + {choice === index && ( +
    +
    + + {title} +
    +
      + {isActive && + choice === index && + item?.routes?.map((el: IData, i: number) => ( +
    • + handleKeyDown(e, el.route || '#')} + to={el.route || '#'} + className={styles.sideBarSublinks__route}> + {el.subtitle} + +
    • + ))} +
    +
    + )} + + ) +} + +export default SideBarSublinks diff --git a/src/features/SideBarMenuModal/index.tsx b/src/features/SideBarMenuModal/index.tsx new file mode 100644 index 00000000..c6f87fd2 --- /dev/null +++ b/src/features/SideBarMenuModal/index.tsx @@ -0,0 +1,2 @@ +import SideBarMenuModal from './ui/SideBarMenuModal' +export default SideBarMenuModal diff --git a/src/features/SideBarMenuModal/model/data/data.ts b/src/features/SideBarMenuModal/model/data/data.ts new file mode 100644 index 00000000..ba392aa5 --- /dev/null +++ b/src/features/SideBarMenuModal/model/data/data.ts @@ -0,0 +1,54 @@ +import { Routes } from '@/shared/config/routerConfig/routes' + +// import { IData } from '../types/types' + +const user = [ + { + title: 'Мои данные', + routes: [ + { subtitle: 'Личный Кабинет', route: Routes.HOME }, // '/my-account' - данного роута пока нет + { subtitle: 'Изменить контактную информацию', route: Routes.HOME }, // '/edit-account' - данного роута пока нет + { subtitle: 'Изменить свой пароль', route: Routes.HOME }, // '/change-password' - данного роута пока нет + { subtitle: 'Изменить мои адреса', route: Routes.HOME }, // '/address-book' - данного роута пока нет + { subtitle: 'Посмотреть закладки', route: Routes.HOME } // '/wishlist' - данного роута пока нет + ] + } +] + +const noUser = [ + { + title: 'Мои данные', + routes: [ + { subtitle: 'Вход', route: Routes.LOGIN }, + { subtitle: 'Регистрация', route: Routes.HOME }, // '/create-account' - данного роута пока нет + { subtitle: 'Забыли пароль?', route: Routes.HOME }, // '/forgot-password' - данного роута пока нет + { subtitle: 'Личный Кабинет', route: Routes.HOME } // '/my-account' - данного роута пока нет + ] + } +] + +const forAll = [ + { + title: 'Мои заказы', + routes: [ + { subtitle: 'История заказов', route: Routes.HOME }, // '/order-history' - данного роута пока нет + { subtitle: 'Файлы для скачивания', route: Routes.HOME }, // '/downloads' - данного роута пока нет + { subtitle: 'Бонусные баллы: 0', route: Routes.HOME }, // '/reward-points' - данного роута пока нет + { subtitle: 'Запросы на возврат', route: Routes.HOME }, // '/returns' - данного роута пока нет + { subtitle: 'История транзакций', route: Routes.HOME }, // '/transactions' - данного роута пока нет + { subtitle: 'Периодические платежи', route: Routes.HOME } // '/index.php?route=account/recurring' - данного роута пока нет + ] + }, + { + title: 'Мой партнерский аккаунт', + routes: [{ subtitle: 'Регистрация партнерского аккаунта', route: Routes.HOME }] + }, // '/index.php?route=account/affiliate/add' - данного роута пока нет + { + title: 'Подписка', + routes: [{ subtitle: 'Подписаться или отказаться от рассылки новостей', route: Routes.HOME }] + } // '/newsletter' - данного роута пока нет +] + +export const userData = user.concat(forAll) + +export const noUserData = noUser.concat(forAll) diff --git a/src/features/SideBarMenuModal/ui/SideBarMenuModal.module.scss b/src/features/SideBarMenuModal/ui/SideBarMenuModal.module.scss new file mode 100644 index 00000000..81db39a7 --- /dev/null +++ b/src/features/SideBarMenuModal/ui/SideBarMenuModal.module.scss @@ -0,0 +1,38 @@ +@use '@/shared/styles/utils/variables' as var; + +.sideBarMenuModal { + position: absolute; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: flex-end; + gap: 5px; + width: 100%; + padding: 25px; + + &__container { + display: flex; + flex-direction: column; + gap: 18px; + background: var.$white; + border-radius: 10px; + padding: 30px; + } + + &__list { + display: flex; + flex-direction: column; + gap: 5px; + list-style: none; + } + + &__button { + display: flex; + align-items: center; + background: var.$white; + border-radius: 10px; + font-size: 15px; + color: var.$body-color; + padding: 10px 20px; + } +} diff --git a/src/features/SideBarMenuModal/ui/SideBarMenuModal.stories.tsx b/src/features/SideBarMenuModal/ui/SideBarMenuModal.stories.tsx new file mode 100644 index 00000000..7a8a7d3f --- /dev/null +++ b/src/features/SideBarMenuModal/ui/SideBarMenuModal.stories.tsx @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { useState } from 'react' + +import SideBarMenuModal from './SideBarMenuModal' + +const meta = { + title: 'features/SideBarMenuModal', + component: SideBarMenuModal, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = () => { + const [user, setUser] = useState('Elon Musk') + + const handleLogOut = () => { + setUser('') + } + + return ( +
    + +
    + ) +} + +Default.args = { + user: 'Elon Musk' +} diff --git a/src/features/SideBarMenuModal/ui/SideBarMenuModal.tsx b/src/features/SideBarMenuModal/ui/SideBarMenuModal.tsx new file mode 100644 index 00000000..3c43d20d --- /dev/null +++ b/src/features/SideBarMenuModal/ui/SideBarMenuModal.tsx @@ -0,0 +1,82 @@ +import { KeyboardEventHandler, KeyboardEvent, FC, useState } from 'react' + +import { userData, noUserData } from '@/mockData/sideBarProfileData' +import { Button } from '@/shared/ui/Button/Button' +import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' + +import SideBarLink from '../SideBarLink/SideBarLink' +import SideBarSublinks from '../SideBarSublinks/SideBarSublinks' + +import styles from './SideBarMenuModal.module.scss' + +export interface ISideBarMenuModal { + handleClose?: () => void + onKeyUp?: KeyboardEventHandler + handleLogOut?: () => void + user?: string +} + +/** + * Модальное окно SideBarMenuModal + * @param {function} handleClose - функция установки булевого значения, для обозначения состояние процесса закрытия модального окна; + * @param {function} onKeyUp - функция обнуляющая пользователя по нажатии клавиши Enter; + * @param {function} handleLogOut - функция обнуляющая пользователя по клику мышки; + * @param {string} user - приходящий с сервера пользователь; + */ + +const SideBarMenuModal: FC = ({ handleClose, onKeyUp, handleLogOut, user }) => { + const [isActive, setIsActive] = useState(false) + const [choice, setChoice] = useState(0) + + const data = user ? userData : noUserData + + const handleClick = (index: number) => { + setChoice(index) + setIsActive(!isActive) + } + + const handleKeyDown = (e: KeyboardEvent, index: number) => { + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault() + setChoice(index) + setIsActive(!isActive) + } + } + + return ( +
    +
    + {user && !isActive && {user}} + +
      + {data && + data.map((item, index) => ( +
    • handleKeyDown(e, index)} onClick={() => handleClick(index)}> + {!isActive ? ( + + ) : ( + + )} +
    • + ))} + + {user && !isActive && ( + + )} +
    +
    + + +
    + ) +} + +export default SideBarMenuModal diff --git a/src/pages/FormReturnPage/FormReturnPage.tsx b/src/pages/FormReturnPage/FormReturnPage.tsx index 47911e27..3b8ccb32 100644 --- a/src/pages/FormReturnPage/FormReturnPage.tsx +++ b/src/pages/FormReturnPage/FormReturnPage.tsx @@ -1,14 +1,20 @@ -import { FC, useState } from 'react' +import { KeyboardEvent, FC, Suspense, lazy, useState } from 'react' import WrapperForMainContent from '@/components/WrapperForMainContent/WrapperForMainContent' +import SideBarButton from '@/entities/SideBarButton' import { Routes } from '@/shared/config/routerConfig/routes' +import { useResize } from '@/shared/libs/hooks/useResize' 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 FormReturn from '@/widgets/FormReturn' import SideBarMenu from '@/widgets/SideBarMenu' import styles from './FormReturnPage.module.scss' +const SideBarMenuModal = lazy(() => import('@/features/SideBarMenuModal')) + const links = [ { heading: 'Главная', href: Routes.HOME }, { heading: 'Личный Кабинет', href: Routes.LOGIN }, @@ -16,27 +22,68 @@ const links = [ ] const FormReturnPage: FC = () => { - const [user, setUser] = useState('Моругина Мария') // позже юзера будем получать из редакса + const [isModalOpen, setIsModalOpen] = useState(false) + const [isModalClosing, setIsModalClosing] = useState(false) + const [user, setUser] = useState('Elon Musk') // позже юзера будем получать из редакса + + const { isScreenMd } = useResize() + + const changeModalState = () => { + setIsModalOpen(!isModalOpen) + } + + const handleClick = () => { + setIsModalOpen(true) + } const handleLogOut = () => { setUser('') } + const handleKeyUp = (e: KeyboardEvent) => { + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault() + handleLogOut() + } + } + return ( - -
    -
    - - Возврат товара - - -
    -
    - - -
    -
    -
    + <> + {isModalOpen && ( + + }> + + + + )} + +
    +
    + + Возврат товара + + +
    +
    + {isScreenMd ? ( + + ) : ( + + )} + +
    +
    +
    + ) } From 6afab169dfc56e3e68bae23ec8a56233bd1adc3b Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Fri, 3 May 2024 17:17:50 +0300 Subject: [PATCH 25/66] enhancement_338_sidebar_menu_modal --- src/assets/images/sideBarMenu/IconArrowDown.svg | 4 ++-- .../SideBarButton/ui/SideBarButton.module.scss | 4 ++-- src/features/SideBar/ui/SideBar.module.scss | 5 +++-- .../SideBarLink/SideBarLink.module.scss | 14 +++++++++----- .../SideBarMenuModal/SideBarLink/SideBarLink.tsx | 3 +-- .../SideBarSublinks/SideBarSublinks.module.scss | 6 ++++++ .../ui/SideBarMenuModal.module.scss | 7 +++++++ 7 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/assets/images/sideBarMenu/IconArrowDown.svg b/src/assets/images/sideBarMenu/IconArrowDown.svg index 65d87109..3025b2c5 100644 --- a/src/assets/images/sideBarMenu/IconArrowDown.svg +++ b/src/assets/images/sideBarMenu/IconArrowDown.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/entities/SideBarButton/ui/SideBarButton.module.scss b/src/entities/SideBarButton/ui/SideBarButton.module.scss index 8f690a3a..b743ba5d 100644 --- a/src/entities/SideBarButton/ui/SideBarButton.module.scss +++ b/src/entities/SideBarButton/ui/SideBarButton.module.scss @@ -15,6 +15,6 @@ } .sideBarButton:hover { - color: var.$theme-secondary-color; - fill: var.$theme-secondary-color; + color: var.$header-color; + fill: var.$header-color; } diff --git a/src/features/SideBar/ui/SideBar.module.scss b/src/features/SideBar/ui/SideBar.module.scss index da85054d..c4e3791e 100644 --- a/src/features/SideBar/ui/SideBar.module.scss +++ b/src/features/SideBar/ui/SideBar.module.scss @@ -16,15 +16,16 @@ border-radius: 6px; padding: 10px 10px 10px 15px; cursor: pointer; + transition: 0.25s; } &__headerText { - color: var.$body-color; transition: 0.25s; } &__headerArrow { - transition: transform 0.25s; + fill: var.$body-color; + transition: 0.25s; } &__headerArrow_active { diff --git a/src/features/SideBarMenuModal/SideBarLink/SideBarLink.module.scss b/src/features/SideBarMenuModal/SideBarLink/SideBarLink.module.scss index 5a60bd29..2313a5b5 100644 --- a/src/features/SideBarMenuModal/SideBarLink/SideBarLink.module.scss +++ b/src/features/SideBarMenuModal/SideBarLink/SideBarLink.module.scss @@ -8,15 +8,19 @@ background: var.$body-bg; min-height: 45px; border-radius: 5px; + font-size: 15px; + color: var.$body-color; + fill: var.$body-color; padding: 10px 15px; cursor: pointer; - - &__paragraph { - font-size: 15px; - color: var.$body-color; - } + transition: 0.25s; &__arrow { transform: rotate(270deg); } } + +.sideBarLink:hover { + color: var.$header-color; + fill: var.$header-color; +} diff --git a/src/features/SideBarMenuModal/SideBarLink/SideBarLink.tsx b/src/features/SideBarMenuModal/SideBarLink/SideBarLink.tsx index 1ec32ccc..784830b3 100644 --- a/src/features/SideBarMenuModal/SideBarLink/SideBarLink.tsx +++ b/src/features/SideBarMenuModal/SideBarLink/SideBarLink.tsx @@ -1,7 +1,6 @@ import { KeyboardEventHandler, FC } from 'react' import ArrowIcon from '@/assets/images/sideBarMenu/IconArrowDown.svg' -import Paragraph from '@/shared/ui/Paragraph/Paragraph' import styles from './SideBarLink.module.scss' @@ -23,7 +22,7 @@ export interface ISideBarLink { const SideBarLink: FC = ({ isVisible, onKeyUp, onClick, title }) => { return (
    - {title} + {title} {isVisible && }
    ) diff --git a/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.module.scss b/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.module.scss index 84c422fe..bf5ba39b 100644 --- a/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.module.scss +++ b/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.module.scss @@ -37,5 +37,11 @@ font-size: 15px; color: var.$body-color; padding: 10px 10px 10px 15px; + cursor: pointer; + transition: 0.25s; + } + + &__route:hover { + color: var.$header-color; } } diff --git a/src/features/SideBarMenuModal/ui/SideBarMenuModal.module.scss b/src/features/SideBarMenuModal/ui/SideBarMenuModal.module.scss index 81db39a7..f16bd10a 100644 --- a/src/features/SideBarMenuModal/ui/SideBarMenuModal.module.scss +++ b/src/features/SideBarMenuModal/ui/SideBarMenuModal.module.scss @@ -32,7 +32,14 @@ background: var.$white; border-radius: 10px; font-size: 15px; + font-weight: 700; + letter-spacing: 0.5px; color: var.$body-color; padding: 10px 20px; + transition: 0.25s; + } + + &__button:hover { + color: var.$header-color; } } From 7b51d391f6434b8f976e23f92dcddfc0b250e4fc Mon Sep 17 00:00:00 2001 From: Yulia Avramenko Date: Sat, 4 May 2024 12:07:56 +0300 Subject: [PATCH 26/66] #319-remove-product-from-cart --- src/features/CartEdit/model/selectors.ts | 8 ++- .../model/services/putRemoveProduct.ts | 26 +++++++++ .../model/slice/productAmountSlice.ts | 33 ++++++++---- src/features/CartEdit/model/types.ts | 1 + .../CartEdit/ui/CartEdit/CartEdit.stories.tsx | 5 +- .../CartEdit/ui/CartEdit/CartEdit.tsx | 54 +++++++++++-------- src/pages/CartPage/CartPage.tsx | 13 ++++- src/shared/api/types.ts | 3 +- 8 files changed, 106 insertions(+), 37 deletions(-) create mode 100644 src/features/CartEdit/model/services/putRemoveProduct.ts diff --git a/src/features/CartEdit/model/selectors.ts b/src/features/CartEdit/model/selectors.ts index 2963212e..6705e10c 100644 --- a/src/features/CartEdit/model/selectors.ts +++ b/src/features/CartEdit/model/selectors.ts @@ -1,7 +1,11 @@ import { StateSchema } from '@/app/providers/StoreProvider' -export const putIncreaseProductAmountSelector = (state: StateSchema) => { - return state.productAmount.isIncreaseSuccessful +export const isSuccessfulRequest = (state: StateSchema) => { + return ( + state.productAmount.isIncreaseSuccessful || + state.productAmount.isDecreaseSuccessful || + state.productAmount.isRemoveSuccessful + ) } export const getProductListSelector = (state: StateSchema) => { diff --git a/src/features/CartEdit/model/services/putRemoveProduct.ts b/src/features/CartEdit/model/services/putRemoveProduct.ts new file mode 100644 index 00000000..3957db0f --- /dev/null +++ b/src/features/CartEdit/model/services/putRemoveProduct.ts @@ -0,0 +1,26 @@ +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' + +export const putRemoveProduct = createAsyncThunk>( + 'cart-remove-product', + async (productId, thunkAPI) => { + const { rejectWithValue, extra } = thunkAPI + try { + const { data } = await extra.api.put( + `api/${ApiRoutes.REMOVE_PRODUCT}`, + { + product: productId + }, + { + withCredentials: true + } + ) + return data + } catch (error) { + return rejectWithValue(apiErrorIdentify(error, ApiErrorTypes.DATA_EMPTY_ERROR)) + } + } +) diff --git a/src/features/CartEdit/model/slice/productAmountSlice.ts b/src/features/CartEdit/model/slice/productAmountSlice.ts index 62ff426b..39634d24 100644 --- a/src/features/CartEdit/model/slice/productAmountSlice.ts +++ b/src/features/CartEdit/model/slice/productAmountSlice.ts @@ -4,10 +4,10 @@ import { rejectedPayloadHandle } from '@/shared/api/rejectedPayloadHandle' import { putDecreaseProductAmount } from '../services/putDecreaseProductAmount' import { putIncreaseProductAmount } from '../services/putIncreaseProductAmount' +import { putRemoveProduct } from '../services/putRemoveProduct' import { IProductAmountStateSchema } from '../types' const initialState: IProductAmountStateSchema = { - isIncreaseSuccessful: false, productList: { amount: 0, product: { @@ -25,7 +25,15 @@ const initialState: IProductAmountStateSchema = { full_price: 0, full_weight: 0 }, - isDecreaseSuccessful: false + isIncreaseSuccessful: false, + isDecreaseSuccessful: false, + isRemoveSuccessful: false +} + +function resetStatuses(state: IProductAmountStateSchema) { + state.isIncreaseSuccessful = false + state.isDecreaseSuccessful = false + state.isRemoveSuccessful = false } export const productAmountSlice = createSlice({ @@ -39,12 +47,10 @@ export const productAmountSlice = createSlice({ extraReducers: builder => { builder .addCase(putIncreaseProductAmount.pending, state => { - state.isIncreaseSuccessful = false - state.isDecreaseSuccessful = false + resetStatuses(state) }) - .addCase(putIncreaseProductAmount.fulfilled, (state, { payload }) => { + .addCase(putIncreaseProductAmount.fulfilled, state => { state.isIncreaseSuccessful = true - state.productList = payload }) .addCase(putIncreaseProductAmount.rejected, (state, { payload }) => { state.isIncreaseSuccessful = false @@ -52,16 +58,25 @@ export const productAmountSlice = createSlice({ }) .addCase(putDecreaseProductAmount.pending, state => { - state.isDecreaseSuccessful = false + resetStatuses(state) }) - .addCase(putDecreaseProductAmount.fulfilled, (state, { payload }) => { + .addCase(putDecreaseProductAmount.fulfilled, state => { state.isDecreaseSuccessful = true - state.productList = payload }) .addCase(putDecreaseProductAmount.rejected, (state, { payload }) => { state.isDecreaseSuccessful = false state.error = rejectedPayloadHandle(payload) }) + + .addCase(putRemoveProduct.pending, state => { + resetStatuses(state) + }) + .addCase(putRemoveProduct.fulfilled, state => { + state.isRemoveSuccessful = true + }) + .addCase(putRemoveProduct.rejected, state => { + state.isRemoveSuccessful = false + }) } }) diff --git a/src/features/CartEdit/model/types.ts b/src/features/CartEdit/model/types.ts index 55789200..b01589b5 100644 --- a/src/features/CartEdit/model/types.ts +++ b/src/features/CartEdit/model/types.ts @@ -3,6 +3,7 @@ import { IProductCartList } from '@/shared/model/types/ProductCartListModel' export interface IProductAmountStateSchema { isIncreaseSuccessful: boolean isDecreaseSuccessful: boolean + isRemoveSuccessful: boolean productList: IProductCartList error?: string | string[] } diff --git a/src/features/CartEdit/ui/CartEdit/CartEdit.stories.tsx b/src/features/CartEdit/ui/CartEdit/CartEdit.stories.tsx index d1ea00a6..e53d62d7 100644 --- a/src/features/CartEdit/ui/CartEdit/CartEdit.stories.tsx +++ b/src/features/CartEdit/ui/CartEdit/CartEdit.stories.tsx @@ -28,7 +28,7 @@ type Story = StoryObj export const Default: Story = { args: { cartId: 85, - productList: { + productWithInfo: { amount: 1, product: { id: 1, @@ -48,6 +48,7 @@ export const Default: Story = { }, full_price: 0, full_weight: 0 - } + }, + updateCart: () => {} } } diff --git a/src/features/CartEdit/ui/CartEdit/CartEdit.tsx b/src/features/CartEdit/ui/CartEdit/CartEdit.tsx index df0f7263..772d13ca 100644 --- a/src/features/CartEdit/ui/CartEdit/CartEdit.tsx +++ b/src/features/CartEdit/ui/CartEdit/CartEdit.tsx @@ -10,72 +10,82 @@ import ButtonDots from '@/shared/ui/ButtonDots/ButtonDots' import Paragraph from '@/shared/ui/Paragraph/Paragraph' import Subheading from '@/shared/ui/Subheading/Subheading' -import { getProductListSelector } from '../../model/selectors' +import { isSuccessfulRequest } from '../../model/selectors' import { putDecreaseProductAmount } from '../../model/services/putDecreaseProductAmount' import { putIncreaseProductAmount } from '../../model/services/putIncreaseProductAmount' -import { productAmountActions } from '../../model/slice/productAmountSlice' +import { putRemoveProduct } from '../../model/services/putRemoveProduct' import styles from './CartEdit.module.scss' export type TCartEditProps = { cartId: number - productList: IProductCartList + productWithInfo: IProductCartList + updateCart: () => void } /** * Компонент используется для отображения добавленных в корзину продуктов, изменения кол-ва продуктов в корзине, * для удаления продуктов из корзины, для добавления продуктов в закладки * @param {number} cartId - id корзины - * @param {IProductCartList} productList - это продукт для определения состояния + * @param {IProductCartList} productList - это корзина с количеством товара, общей стоимостью и весом + * @param {function} updateCart - это функция для обновления корзины */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const CartEdit: React.FC = ({ cartId, productList }: TCartEditProps) => { +export const CartEdit: React.FC = ({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + cartId, + productWithInfo, + updateCart +}: TCartEditProps) => { + const MIN_AMOUNT = 1 + const MAX_AMOUNT = 99 const [needToOpenContextMenuButtonDots, setNeedToOpen] = useState(false) const dispatch = useAppDispatch() - const productListState: IProductCartList = useSelector(getProductListSelector) + const isSuccessful: boolean = useSelector(isSuccessfulRequest) function deleteProductHandler() { setNeedToOpen(false) - // removeProduct(product.id) переделать на вызов action https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/319 + dispatch(putRemoveProduct(productWithInfo.product.id)) } + useEffect(() => { + updateCart() + }, [isSuccessful]) + function addToFavoritesHandler() { setNeedToOpen(false) } function increaseAmountHandler() { - dispatch(putIncreaseProductAmount(productListState.product.id)) + if (productWithInfo.amount < MAX_AMOUNT) { + dispatch(putIncreaseProductAmount(productWithInfo.product.id)) + } } function decreaseAmountHandler() { - dispatch(putDecreaseProductAmount(productListState.product.id)) - // tbd https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/318 + if (productWithInfo.amount > MIN_AMOUNT) { + dispatch(putDecreaseProductAmount(productWithInfo.product.id)) + } } function setAmountHandler() { //tbd https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/316 } - useEffect(() => { - dispatch(productAmountActions.setProductList(productList)) - }, [productList]) - return ( <>
    - +
    {' '} - {productListState.amount * Number(productListState.product.price)}{' '} - {productListState.product.brand} + {productWithInfo.amount * Number(productWithInfo.product.price)} {productWithInfo.product.brand} {/* currency, not brand, c Number непонятно пока*/} {' '} - {productListState.product.price} {productListState.product.brand}/шт + {productWithInfo.product.price} {productWithInfo.product.brand}/шт {/* currency, not brand */}
    @@ -88,9 +98,9 @@ export const CartEdit: React.FC = ({ cartId, productList }: TCar diff --git a/src/pages/CartPage/CartPage.tsx b/src/pages/CartPage/CartPage.tsx index cfb03561..680e5b63 100644 --- a/src/pages/CartPage/CartPage.tsx +++ b/src/pages/CartPage/CartPage.tsx @@ -28,6 +28,10 @@ const CartPage = () => { dispatch(getCartList()) }, []) + function updateCart() { + dispatch(getCartList()) + } + return (
    @@ -48,7 +52,14 @@ const CartPage = () => {
    {cart.products.map(item => { - return + return ( + + ) })}
    diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index bc674244..831fd5bc 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -14,7 +14,8 @@ export enum ApiRoutes { PRODUCT = 'catalogue', CART_LIST = 'cart', INCREASE_PRODUCT_AMOUNT = 'cart/add/', - DECREASE_PRODUCT_AMOUNT = 'cart/subtract/' + DECREASE_PRODUCT_AMOUNT = 'cart/subtract/', + REMOVE_PRODUCT = 'cart/delete/' } export enum ApiErrorTypes { From 8696021055ec1fd3289925e3c548704c1dbd17a2 Mon Sep 17 00:00:00 2001 From: kirill-k88 Date: Sat, 4 May 2024 21:51:57 +0300 Subject: [PATCH 27/66] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B0=D0=BB=20=D0=BD=D0=B0=20Fromik=20c=20=D0=B2=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/SubscribeForm/SubscribeForm.tsx | 37 ++++++++++--------- .../model/constants/constants.ts | 3 +- .../validationSchema/validationSchema.ts | 4 +- .../SubscribeForm/subscribeForm.module.scss | 20 ++++++++-- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/features/SubscribeForm/SubscribeForm.tsx b/src/features/SubscribeForm/SubscribeForm.tsx index aa9d2dfc..69a057ec 100644 --- a/src/features/SubscribeForm/SubscribeForm.tsx +++ b/src/features/SubscribeForm/SubscribeForm.tsx @@ -1,10 +1,9 @@ import classNames from 'classnames' -import { Form, Formik } from 'formik' -import { useState, type FC } from 'react' +import { Field, Form, Formik } from 'formik' +import { type FC } from 'react' import SubscribeIcon from '@/assets/images/subscriptionForm/icon-subsc.svg' import { FormMsg } from '@/shared/ui/FormMsg/FormMsg' -import { Input, InputSize, InputTheme } from '@/shared/ui/Input/Input' import Label from '@/shared/ui/Label/Label' import { validationSchema } from './model/validationSchema/validationSchema' @@ -16,16 +15,12 @@ type TSubscribeForm = { onSubmit: () => void } -// @TODO: Перевести форму на Formik + Yup -// https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/91 /** * @param {string} type - определяет внешний вид для компонентов footer и для subscribe - * @param {string} className - нужно будет, если захотят переиспользовать компонент + * @param {string} className - для переопределения стилей
    * @param {string} onSubmit - функция для обработки формы */ const SubscribeForm: FC = ({ type, onSubmit, className = '' }) => { - const [showApiErrorMsg, setShowApiErrorMsg] = useState(false) - const classNameContainer = classNames(styles.container, { [styles.container]: true, [styles.container_footer]: type === 'footer', @@ -41,15 +36,17 @@ const SubscribeForm: FC = ({ type, onSubmit, className = '' }) = [styles.form_footer]: type === 'footer', [styles.form_subscribe]: type === 'subscribe' }) + const classNameField = classNames({ + [styles.field]: true, + [styles.field_footer]: type === 'footer', + [styles.field_subscribe]: type === 'subscribe' + }) const submitHandle = () => { + //@TODO: Доделать после появления эндпоинта на BE onSubmit() } - const onErrorMsgClose = () => { - setShowApiErrorMsg(false) - } - return ( = ({ type, onSubmit, className = '' }) = }} validationSchema={validationSchema} onSubmit={submitHandle}> - {({ isSubmitting, errors }) => ( + {({ isSubmitting, errors, resetForm, touched }) => ( -
    - +
    = ({
    - {score} + {score.toFixed(1)} Рейтинг нашего магазина
      - - - + + +
    diff --git a/src/widgets/FeedbackList/FeedbackList.module.scss b/src/widgets/FeedbackList/FeedbackList.module.scss index 9e850b37..d0e094f3 100644 --- a/src/widgets/FeedbackList/FeedbackList.module.scss +++ b/src/widgets/FeedbackList/FeedbackList.module.scss @@ -6,13 +6,6 @@ gap: 30px; width: 100%; margin: 0 auto; + padding-bottom: 30px; background-color: color.$body-bg; - - &__list { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 10px; - } } diff --git a/src/widgets/FeedbackList/FeedbackList.stories.tsx b/src/widgets/FeedbackList/FeedbackList.stories.tsx index 0d18ab65..bb146b8d 100644 --- a/src/widgets/FeedbackList/FeedbackList.stories.tsx +++ b/src/widgets/FeedbackList/FeedbackList.stories.tsx @@ -70,6 +70,8 @@ export const Default: Story = { replay: null } ], - pk: 0 + targetId: 0, + isLoading: false, + nextPage: null } } diff --git a/src/widgets/FeedbackList/FeedbackList.tsx b/src/widgets/FeedbackList/FeedbackList.tsx index a5e48542..6582d698 100644 --- a/src/widgets/FeedbackList/FeedbackList.tsx +++ b/src/widgets/FeedbackList/FeedbackList.tsx @@ -1,40 +1,59 @@ import { useEffect, useRef, type FC } from 'react' +import { Virtuoso, VirtuosoHandle } from 'react-virtuoso' -import FeedbackCard from '@/entities/FeedbackCard/FeedbackCard' +import { FeedbackCard } from '@/entities/FeedbackCard/FeedbackCard' import styles from './FeedbackList.module.scss' import type { IFeedback } from './model/types/types' interface IFeedbackListProps { - pk: number + targetId: number feedbacks: IFeedback[] + isLoading: boolean + nextPage: number | null + fetchNextPage: () => void } /** * Виджет дял отображения отзывов * @param {IFeedback[]} feedbacks- массив отзывов о магазине - * @param {number} pk - идентификатор отзыва, на который нужно спозиционировать страницу + * @param {number} ptargetId - идентификатор отзыва, на который нужно спозиционировать страницу */ -export const FeedbackList: FC = ({ feedbacks, pk }) => { - const targetRef = useRef(null) +export const FeedbackList: FC = ({ + feedbacks, + targetId, + isLoading, + nextPage, + fetchNextPage +}) => { + const virtuosoRef = useRef(null) useEffect(() => { - if (targetRef.current) { - targetRef.current.scrollIntoView({ behavior: 'smooth' }) + if (virtuosoRef.current) { + virtuosoRef.current.scrollToIndex({ index: targetId, behavior: 'smooth' }) } - }, [pk]) + }, [targetId]) return (
    -
    - {feedbacks.map(f => { - return f.pk === pk ? ( - - ) : ( - - ) - })} -
    + isLoading &&

    Загрузка ...

    + }} + itemContent={(_, item) => { + return + }} + endReached={() => { + if (nextPage) { + fetchNextPage() + } + }} + />
    ) } diff --git a/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx index f78e38ed..42ae63d8 100644 --- a/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx +++ b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx @@ -5,7 +5,7 @@ import { StateSchema } from '@/app/providers/StoreProvider' import IconLink from '@/assets/icons/IconLink' import IconHand from '@/assets/images/img-hand.png.png' import CardReview from '@/entities/CardReview/ui/CardReview/CardReview' -import { getAverageMark, getFeedbacks } from '@/features/Reviews/model/slice/feedbacksSlice' +import { getAverageMark, getFirstFeedbacks } from '@/features/Reviews/model/slice/feedbacksSlice' import { IFeedback } from '@/features/Reviews/model/types/types' import { useAppDispatch } from '@/shared/libs/hooks/store' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' @@ -33,7 +33,7 @@ const ReviewsBlock: FC = props => { const reviews = useSelector((store: StateSchema) => store.feedbacks) useEffect(() => { - dispatch(getFeedbacks(1)) + dispatch(getFirstFeedbacks()) dispatch(getAverageMark()) }, []) @@ -59,8 +59,9 @@ const ReviewsBlock: FC = props => { date="" score={reviews.averageMark.average_score__avg} name="" + index={0} /> - {reviews.feedbacks.map((item: IFeedback) => ( + {reviews.feedbacks.map((item: IFeedback, index) => ( = props => { date={item.pub_date} score={item.average_score} name={item.author_name} + index={index} /> ))} From 41f5607fddba546e6511aaa4b47c9d6dd6ef0fb9 Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Fri, 10 May 2024 17:48:54 +0300 Subject: [PATCH 46/66] enhancement_347_header_mobile_menu_modal --- src/assets/icons/IconCart.svg | 4 +- src/assets/icons/IconHeaderMenuArrow.svg | 3 + src/assets/icons/IconHeart.svg | 4 + .../icons/IconPerson.svg} | 2 +- .../icons/IconPersonAuth.svg} | 2 +- src/assets/icons/IconPhone.svg | 4 + .../icons/IconScales.svg} | 2 +- src/assets/icons/iconHeaderMenuClose.svg | 4 + src/assets/icons/iconMenu.svg | 8 +- src/assets/icons/iconSearch.svg | 4 +- src/entities/ContactCard/ContactCard.tsx | 19 ++- .../ContactCard/contactCard.module.scss | 37 +++-- src/entities/HeaderAccount/HeaderAccount.tsx | 123 +++++++++++---- .../HeaderAccount/headerAccount.module.scss | 34 +++-- .../HeaderMenuModalCatalog.module.scss | 27 ++++ .../HeaderMenuModalCatalog.tsx | 56 +++++++ .../HeaderMenuModalHeading.module.scss | 33 ++++ .../HeaderMenuModalHeading.tsx | 26 ++++ .../HeaderMenuModalLink.module.scss | 27 ++++ .../HeaderMenuModalLink.tsx | 30 ++++ .../HeaderMenuModalSublinks.module.scss | 25 +++ .../HeaderMenuModalSublinks.tsx | 59 +++++++ src/features/HeaderMenuModal/index.tsx | 2 + .../HeaderMenuModal/model/types/types.ts | 16 ++ .../ui/HeaderMenuModal.module.scss | 112 ++++++++++++++ .../HeaderMenuModal/ui/HeaderMenuModal.tsx | 144 ++++++++++++++++++ src/mockData/headerMenuData.ts | 39 +++++ .../FormReturnPage/FormReturnPage.module.scss | 6 +- src/pages/FormReturnPage/FormReturnPage.tsx | 4 +- src/shared/icons/cart.svg | 3 - src/shared/icons/heart.svg | 15 -- src/widgets/Header/Header.tsx | 41 ++++- src/widgets/Header/header.module.scss | 28 +++- 33 files changed, 845 insertions(+), 98 deletions(-) create mode 100644 src/assets/icons/IconHeaderMenuArrow.svg create mode 100644 src/assets/icons/IconHeart.svg rename src/{shared/icons/person.svg => assets/icons/IconPerson.svg} (96%) rename src/{shared/icons/person_auth.svg => assets/icons/IconPersonAuth.svg} (98%) create mode 100644 src/assets/icons/IconPhone.svg rename src/{shared/icons/scales.svg => assets/icons/IconScales.svg} (98%) create mode 100644 src/assets/icons/iconHeaderMenuClose.svg create mode 100644 src/features/HeaderMenuModal/HeaderMenuModalCatalog/HeaderMenuModalCatalog.module.scss create mode 100644 src/features/HeaderMenuModal/HeaderMenuModalCatalog/HeaderMenuModalCatalog.tsx create mode 100644 src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.module.scss create mode 100644 src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.tsx create mode 100644 src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.module.scss create mode 100644 src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.tsx create mode 100644 src/features/HeaderMenuModal/HeaderMenuModalSublinks/HeaderMenuModalSublinks.module.scss create mode 100644 src/features/HeaderMenuModal/HeaderMenuModalSublinks/HeaderMenuModalSublinks.tsx create mode 100644 src/features/HeaderMenuModal/index.tsx create mode 100644 src/features/HeaderMenuModal/model/types/types.ts create mode 100644 src/features/HeaderMenuModal/ui/HeaderMenuModal.module.scss create mode 100644 src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx create mode 100644 src/mockData/headerMenuData.ts delete mode 100644 src/shared/icons/cart.svg delete mode 100644 src/shared/icons/heart.svg diff --git a/src/assets/icons/IconCart.svg b/src/assets/icons/IconCart.svg index 19ba73fa..094fee80 100644 --- a/src/assets/icons/IconCart.svg +++ b/src/assets/icons/IconCart.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/assets/icons/IconHeaderMenuArrow.svg b/src/assets/icons/IconHeaderMenuArrow.svg new file mode 100644 index 00000000..83967014 --- /dev/null +++ b/src/assets/icons/IconHeaderMenuArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/IconHeart.svg b/src/assets/icons/IconHeart.svg new file mode 100644 index 00000000..c253ef84 --- /dev/null +++ b/src/assets/icons/IconHeart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/shared/icons/person.svg b/src/assets/icons/IconPerson.svg similarity index 96% rename from src/shared/icons/person.svg rename to src/assets/icons/IconPerson.svg index d9236bfc..cbd0d566 100644 --- a/src/shared/icons/person.svg +++ b/src/assets/icons/IconPerson.svg @@ -1,3 +1,3 @@ - + diff --git a/src/shared/icons/person_auth.svg b/src/assets/icons/IconPersonAuth.svg similarity index 98% rename from src/shared/icons/person_auth.svg rename to src/assets/icons/IconPersonAuth.svg index 0f045e0c..2bd9baeb 100644 --- a/src/shared/icons/person_auth.svg +++ b/src/assets/icons/IconPersonAuth.svg @@ -1,3 +1,3 @@ - + diff --git a/src/assets/icons/IconPhone.svg b/src/assets/icons/IconPhone.svg new file mode 100644 index 00000000..5af1cbab --- /dev/null +++ b/src/assets/icons/IconPhone.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/shared/icons/scales.svg b/src/assets/icons/IconScales.svg similarity index 98% rename from src/shared/icons/scales.svg rename to src/assets/icons/IconScales.svg index d770e784..19675179 100644 --- a/src/shared/icons/scales.svg +++ b/src/assets/icons/IconScales.svg @@ -1,3 +1,3 @@ - + diff --git a/src/assets/icons/iconHeaderMenuClose.svg b/src/assets/icons/iconHeaderMenuClose.svg new file mode 100644 index 00000000..7e327aaa --- /dev/null +++ b/src/assets/icons/iconHeaderMenuClose.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/assets/icons/iconMenu.svg b/src/assets/icons/iconMenu.svg index c1fc5fb6..5685a635 100644 --- a/src/assets/icons/iconMenu.svg +++ b/src/assets/icons/iconMenu.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/src/assets/icons/iconSearch.svg b/src/assets/icons/iconSearch.svg index ac0c10a9..21c1538d 100644 --- a/src/assets/icons/iconSearch.svg +++ b/src/assets/icons/iconSearch.svg @@ -1,4 +1,4 @@ - - + + diff --git a/src/entities/ContactCard/ContactCard.tsx b/src/entities/ContactCard/ContactCard.tsx index 1f048598..6ee5b9e4 100644 --- a/src/entities/ContactCard/ContactCard.tsx +++ b/src/entities/ContactCard/ContactCard.tsx @@ -7,6 +7,7 @@ import Paragraph from '@/shared/ui/Paragraph/Paragraph' import styles from './contactCard.module.scss' export type PropsContactCard = { + isMenuModalOpen?: boolean messenger: TMessenger Icon: string } @@ -15,17 +16,25 @@ export type PropsContactCard = { * Компонент карточки контакта. Заполнение карточки происходит с применением метода map * по массиву мессенджеров с использованием из него ссылки (link) на мессенджер, его названием (title) и иконку(Icon). * Причем иконка(Icon) в массиве мессенджеров импортируется как svg компонент + * @param {boolean} isMenuModalOpen - состояние открытия модального окна; * @param {TMessenger} messenger - массив для наполнения карточки контакта; * @param {string} Icon - компонент-ссылка на svg-компонент из массива messenger (messenger.icon); */ -const ContactCard: FC = ({ messenger, Icon }) => { +const ContactCard: FC = ({ isMenuModalOpen, messenger, Icon }) => { return ( -
  • - -
    +
  • + +
    - {messenger.title} + {!isMenuModalOpen && {messenger.title}}
  • ) diff --git a/src/entities/ContactCard/contactCard.module.scss b/src/entities/ContactCard/contactCard.module.scss index 62e8a2ea..54d1e4cd 100644 --- a/src/entities/ContactCard/contactCard.module.scss +++ b/src/entities/ContactCard/contactCard.module.scss @@ -4,19 +4,30 @@ background-color: var.$white; border-radius: 5px; margin: 0 1.5px 3px; -} -.link { - height: 160px; - width: 163px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} + &__link { + height: 160px; + width: 163px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + &__icon { + width: 60px; + height: 60px; + margin-bottom: 12px; + } -.icon { - width: 60px; - height: 60px; - margin-bottom: 12px; + &__iconHeaderMenuModal { + width: 40px; + height: 40px; + } } + + + + + + diff --git a/src/entities/HeaderAccount/HeaderAccount.tsx b/src/entities/HeaderAccount/HeaderAccount.tsx index d09938cb..ca67f516 100644 --- a/src/entities/HeaderAccount/HeaderAccount.tsx +++ b/src/entities/HeaderAccount/HeaderAccount.tsx @@ -1,16 +1,16 @@ import { FC, lazy, useState, Suspense, useEffect } from 'react' import { useSelector } from 'react-redux' -import SearchIcon from '@/assets/icons/iconSearch.svg' +import CartIcon from '@/assets/icons/IconCart.svg' +import HeartIcon from '@/assets/icons/IconHeart.svg' +import PersonIcon from '@/assets/icons/IconPerson.svg' +import PersonAuthIcon from '@/assets/icons/IconPersonAuth.svg' +import ScalesIcon from '@/assets/icons/IconScales.svg' +import SearchIcon from '@/assets/icons/IconSearch.svg' import { getUserAuthStatus } from '@/features/login/model/selectors/getUserAuthStatus' import { logout } from '@/features/login/model/services/logout/logout' import { loginActions } from '@/features/login/model/slice/loginSlice' import { Routes } from '@/shared/config/routerConfig/routes' -import CartIcon from '@/shared/icons/cart.svg' -import HeartIcon from '@/shared/icons/heart.svg' -import PersonIcon from '@/shared/icons/person.svg' -import PersonAuthIcon from '@/shared/icons/person_auth.svg' -import ScalesIcon from '@/shared/icons/scales.svg' import { useAppDispatch } from '@/shared/libs/hooks/store' import { useResize } from '@/shared/libs/hooks/useResize' import { Button } from '@/shared/ui/Button/Button' @@ -22,6 +22,8 @@ import Spinner from '@/shared/ui/Spinner/Spinner' import styles from './headerAccount.module.scss' export type HeaderAccountProps = { + isMenuModalOpen?: boolean + handleClose?: () => void counter: number total: string } @@ -29,10 +31,12 @@ export type HeaderAccountProps = { const LazyLoginForm = lazy(() => import('@/features/login/index')) /** + * Компонент хедера, показывающий блок аккаунта + * @param {boolean} isMenuModalOpen - состояние открытия модального окна * @param {string} counter - счетчик количества товаров в корзине * @param {string} total - полная стоимость */ -const HeaderAccount: FC = ({ counter, total }) => { +const HeaderAccount: FC = ({ isMenuModalOpen, handleClose, counter, total }) => { const [isModalOpen, setIsModalOpen] = useState(false) const [isModalClosing, setIsModalClosing] = useState(false) @@ -73,47 +77,110 @@ const HeaderAccount: FC = ({ counter, total }) => { )} -
      - {!isScreenLg && } +
        + {isScreenLg || isMenuModalOpen ? null : }
      • {/* Временная реализация TODO заменить на дропдаун на ховер в контекстном меню добавить пункт-кнопку для разлогина пока висит на иконке */}
      • - {isScreenLg && ( + {isScreenLg || isMenuModalOpen ? (
      • - - + +
      • - )} + ) : null} - {isScreenLg && ( + {isScreenLg || isMenuModalOpen ? (
      • - - + +
      • - )} + ) : null}
      • - - {isScreenLg && ( -
        -
        - Корзина - {counter} + className={ + isScreenLg || isMenuModalOpen + ? `${styles.headerAccount__cart}` + : `${styles.headerAccount__cartMobile}` + }> + {isScreenLg || isMenuModalOpen ? ( + <> + +
        +
        + Корзина + {counter} +
        + + {total} +
        - {total} -
        + + ) : ( + )}
      • diff --git a/src/entities/HeaderAccount/headerAccount.module.scss b/src/entities/HeaderAccount/headerAccount.module.scss index cc479828..38751182 100644 --- a/src/entities/HeaderAccount/headerAccount.module.scss +++ b/src/entities/HeaderAccount/headerAccount.module.scss @@ -1,4 +1,4 @@ -@use '../../shared/styles/utils/variables' as var; +@use '@/shared/styles/utils/variables' as var; .headerAccount { grid-area: account; @@ -12,14 +12,6 @@ gap: 24px; } - &__search { - cursor: pointer; - - &:active { - transform: scale(0.9); - } - } - &__cart { display: flex; justify-content: center; @@ -31,7 +23,7 @@ transition: border 0.25s; &:hover { - border: 1px solid var.$white; + border: 1px solid var.$link-border; } } @@ -47,6 +39,24 @@ } } + &__icon { + fill: var.$white; + cursor: pointer; + transition: 0.25s; + + &:hover { + fill: var.$header-color; + } + + &:active { + transform: scale(0.9); + } + + &_active { + fill: var.$footer-form-color; + } + } + &__cartContainer { display: flex; flex-direction: column; @@ -66,6 +76,10 @@ font-size: 13px; font-weight: 600; color: var.$white; + + &_dark { + color: var.$body-color; + } } &__cartTotalText { diff --git a/src/features/HeaderMenuModal/HeaderMenuModalCatalog/HeaderMenuModalCatalog.module.scss b/src/features/HeaderMenuModal/HeaderMenuModalCatalog/HeaderMenuModalCatalog.module.scss new file mode 100644 index 00000000..c6d7a317 --- /dev/null +++ b/src/features/HeaderMenuModal/HeaderMenuModalCatalog/HeaderMenuModalCatalog.module.scss @@ -0,0 +1,27 @@ +@use '@/shared/styles/utils/variables' as var; + +.headerMenuModalCatalog { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + + &__route { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + min-height: 57px; + border-bottom: 1px solid var.$bg-subscribe; + font-size: 15px; + color: var.$body-color; + margin: 0; + padding: 10px 30px; + cursor: pointer; + transition: 0.25s; + + &:active { + color: var.$header-color; + } + } +} diff --git a/src/features/HeaderMenuModal/HeaderMenuModalCatalog/HeaderMenuModalCatalog.tsx b/src/features/HeaderMenuModal/HeaderMenuModalCatalog/HeaderMenuModalCatalog.tsx new file mode 100644 index 00000000..3c1df450 --- /dev/null +++ b/src/features/HeaderMenuModal/HeaderMenuModalCatalog/HeaderMenuModalCatalog.tsx @@ -0,0 +1,56 @@ +import { FC } from 'react' + +import { Routes } from '@/shared/config/routerConfig/routes' +import Link from '@/shared/ui/Link/Link' + +import HeaderMenuModalHeading from '../HeaderMenuModalHeading/HeaderMenuModalHeading' +import { ICategory } from '../model/types/types' + +import styles from './HeaderMenuModalCatalog.module.scss' + +export interface IHeaderMenuModalCatalog { + categories?: ICategory[] + handleCategory?: () => void + isCatalog?: boolean + handleClose?: () => void +} + +/** + * Компонент для отрисовки категорий в HeaderMenuModal; + * @param {boolean} isCatalog - стейт позволяющий показывать данный компонент; + * @param {array} categories - массив категорий полученный из редакса; + * @param {function} handleCategory - функция переключения активности; + * @param {function} handleClose - функция закрытия модальго окна; + */ + +const HeaderMenuModalCatalog: FC = ({ + isCatalog, + categories, + handleCategory, + handleClose +}) => { + return ( + <> + {isCatalog && ( +
        + + +
          + {categories?.map(item => ( +
        • + + {item.name} + +
        • + ))} +
        +
        + )} + + ) +} + +export default HeaderMenuModalCatalog diff --git a/src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.module.scss b/src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.module.scss new file mode 100644 index 00000000..000ddbfb --- /dev/null +++ b/src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.module.scss @@ -0,0 +1,33 @@ +@use '@/shared/styles/utils/variables' as var; + +.headerMenuModalHeading { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 14px; + width: 100%; + height: 75px; + background: var.$theme-primary-color; + font-size: 17px; + color: var.$white; + fill: var.$white; + border-bottom: 1px solid var.$bg-subscribe; + margin: 0; + padding: 20px 30px 20px 20px; + cursor: pointer; + transition: 0.25s; + + &:active { + color: var.$header-color; + } + + &__arrow { + fill: var.$white; + transform: rotate(90deg); + transition: 0.25s; + + &:active { + fill: var.$header-color; + } + } +} diff --git a/src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.tsx b/src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.tsx new file mode 100644 index 00000000..278f120c --- /dev/null +++ b/src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.tsx @@ -0,0 +1,26 @@ +import { FC } from 'react' + +import ArrowIcon from '@/assets/icons/IconHeaderMenuArrow.svg' +import { Button } from '@/shared/ui/Button/Button' + +import styles from './HeaderMenuModalHeading.module.scss' + +interface IHeaderMenuModalHeading { + handleCategory?: () => void +} + +/** + * Компонент для отрисовки верхнего болка "Меню" со стрелкой "Назад" в HeaderMenuModal + * @param {function} handleCategory - функция переключения активности + */ + +const HeaderMenuModalHeading: FC = ({ handleCategory }) => { + return ( + + ) +} + +export default HeaderMenuModalHeading diff --git a/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.module.scss b/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.module.scss new file mode 100644 index 00000000..029a77ac --- /dev/null +++ b/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.module.scss @@ -0,0 +1,27 @@ +@use '@/shared/styles/utils/variables' as var; + +.headerMenuModalLink { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: 57px; + border-bottom: 1px solid var.$bg-subscribe; + padding: 10px 30px; + font-size: 15px; + color: var.$body-color; + cursor: pointer; + transition: 0.25s; + + &:hover { + color: var.$header-color; + } + + &:active { + color: var.$header-color; + } + + &__arrow { + transform: rotate(270deg); + } +} diff --git a/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.tsx b/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.tsx new file mode 100644 index 00000000..db7258d9 --- /dev/null +++ b/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react' + +import ArrowIcon from '@/assets/images/sideBarMenu/IconArrowDown.svg' +import { Button } from '@/shared/ui/Button/Button' + +import styles from './HeaderMenuModalLink.module.scss' + +interface IHeaderMenuModalLink { + title?: string + isVisible?: boolean + onClick?: () => void +} + +/** + * Компонент модального окна HeaderMenuModal, отвечающий за развертывание кнопок с именами обьектов массива; + * @param {string} title - название линка; + * @param {boolean} isVisible - значение позволяющее показывать иконку стрелки; + * @param {function} onClick - - функция переключения активности; + */ + +const HeaderMenuModalLink: FC = ({ title, isVisible, onClick }) => { + return ( + + ) +} + +export default HeaderMenuModalLink diff --git a/src/features/HeaderMenuModal/HeaderMenuModalSublinks/HeaderMenuModalSublinks.module.scss b/src/features/HeaderMenuModal/HeaderMenuModalSublinks/HeaderMenuModalSublinks.module.scss new file mode 100644 index 00000000..879c8cd1 --- /dev/null +++ b/src/features/HeaderMenuModal/HeaderMenuModalSublinks/HeaderMenuModalSublinks.module.scss @@ -0,0 +1,25 @@ +@use '@/shared/styles/utils/variables' as var; + +.headerMenuModalSublinks { + display: flex; + flex-direction: column; + width: 100%; + + &__route { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + min-height: 57px; + border-bottom: 1px solid var.$bg-subscribe; + padding: 10px 30px; + font-size: 15px; + color: var.$body-color; + cursor: pointer; + transition: 0.25s; + + &:active { + color: var.$header-color; + } + } +} diff --git a/src/features/HeaderMenuModal/HeaderMenuModalSublinks/HeaderMenuModalSublinks.tsx b/src/features/HeaderMenuModal/HeaderMenuModalSublinks/HeaderMenuModalSublinks.tsx new file mode 100644 index 00000000..601076e1 --- /dev/null +++ b/src/features/HeaderMenuModal/HeaderMenuModalSublinks/HeaderMenuModalSublinks.tsx @@ -0,0 +1,59 @@ +import { FC } from 'react' + +import Link from '@/shared/ui/Link/Link' + +import HeaderMenuModalHeading from '../HeaderMenuModalHeading/HeaderMenuModalHeading' +import { IData } from '../model/types/types' + +import styles from './HeaderMenuModalSublinks.module.scss' + +export interface IHeaderMenuModalSublinks { + isActive?: boolean + choice?: number + index?: number + item?: IData + handleClose?: () => void +} + +/** + * Компонент модального окна HeaderMenuModal, отвечающий за развертывание роутов и их названий + * @param {boolean} isActive - булево значение; + * @param {number} choice - изменяемое состояние индекса; + * @param {number} index - индекс выбранной кнопки; + * @param {object} item - обьект массива; + * @param {function} handleClose - функция закрытия модального окна; + */ + +const HeaderMenuModalSublinks: FC = ({ + isActive, + choice, + index, + item, + handleClose +}) => { + return ( + <> + {choice === index && ( +
        + +
          + {isActive && + choice === index && + item?.routes?.map((el, i) => ( +
        • + + {el.subtitle} + +
        • + ))} +
        +
        + )} + + ) +} + +export default HeaderMenuModalSublinks diff --git a/src/features/HeaderMenuModal/index.tsx b/src/features/HeaderMenuModal/index.tsx new file mode 100644 index 00000000..850f4bcf --- /dev/null +++ b/src/features/HeaderMenuModal/index.tsx @@ -0,0 +1,2 @@ +import HeaderMenuModal from './ui/HeaderMenuModal' +export default HeaderMenuModal diff --git a/src/features/HeaderMenuModal/model/types/types.ts b/src/features/HeaderMenuModal/model/types/types.ts new file mode 100644 index 00000000..561986c1 --- /dev/null +++ b/src/features/HeaderMenuModal/model/types/types.ts @@ -0,0 +1,16 @@ +export interface IData { + routes?: IRoute[] + subtitle?: string + route?: string | null +} + +export interface IRoute { + subtitle?: string + route?: string | null +} + +export interface ICategory { + id?: number + name?: string + slug?: string +} diff --git a/src/features/HeaderMenuModal/ui/HeaderMenuModal.module.scss b/src/features/HeaderMenuModal/ui/HeaderMenuModal.module.scss new file mode 100644 index 00000000..2f1f5ec7 --- /dev/null +++ b/src/features/HeaderMenuModal/ui/HeaderMenuModal.module.scss @@ -0,0 +1,112 @@ +@use '@/shared/styles/utils/variables' as var; + +.headerMenuModal { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background: var.$white; + overflow: auto; + + &__heading { + position: relative; + display: flex; + align-items: center; + width: 100%; + height: 75px; + background: var.$theme-primary-color; + color: var.$white; + fill: var.$white; + border-bottom: 1px solid var.$bg-subscribe; + padding: 20px 30px; + } + + &__closeButton { + margin: 0; + padding: 0; + } + + &__closeIcon { + fill: var.$white; + cursor: pointer; + transition: 0.25s; + + &:hover { + fill: var.$header-color; + } + + &:active { + transform: scale(0.8); + } + } + + &__title { + position: absolute; + left: 45%; + color: var.$white; + letter-spacing: 1px; + } + + &__account { + display: flex; + justify-content: center; + align-items: center; + padding: 20px 30px; + } + + &__catalog { + display: flex; + align-items: center; + width: 100%; + height: 57px; + font-size: 15px; + color: var.$body-color; + border-bottom: 1px solid var.$bg-subscribe; + padding: 10px 30px; + cursor: pointer; + transition: 0.25s; + } + + &__container { + width: 100%; + margin-top: 57px; + padding: 0 30px; + } + + &__support { + display: flex; + align-items: center; + height: 57px; + background: var.$promo-color; + border-radius: 10px; + font-size: 15px; + font-weight: 700; + color: var.$white; + padding: 10px 30px; + cursor: pointer; + opacity: 1; + transition: 0.25s; + + &:hover { + opacity: 0.9; + } + + &:active { + opacity: 0.9; + } + } + + &__workHours { + padding: 0 30px; + font-size: 13px; + color: var.$footer-form-color; + } + + &__contactList { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + padding-top: 30px; + } +} diff --git a/src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx b/src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx new file mode 100644 index 00000000..54ca39b7 --- /dev/null +++ b/src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx @@ -0,0 +1,144 @@ +import { FC, useState } from 'react' +import Skeleton from 'react-loading-skeleton' +import 'react-loading-skeleton/dist/skeleton.css' +import { useNavigate } from 'react-router' + +import CloseIcon from '@/assets/icons/iconHeaderMenuClose.svg' +import PhoneIcon from '@/assets/icons/IconPhone.svg' +import ContactCard from '@/entities/ContactCard/ContactCard' +import HeaderAccount from '@/entities/HeaderAccount/HeaderAccount' +import { headerMenuData } from '@/mockData/headerMenuData' +import { headerAccountData } from '@/shared/mockData/headerAccountData' +import { messengerArray } from '@/shared/model/types/messengerArray' +import { Button } from '@/shared/ui/Button/Button' +import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' +import Link from '@/shared/ui/Link/Link' +import Paragraph from '@/shared/ui/Paragraph/Paragraph' + +import HeaderMenuModalCatalog from '../HeaderMenuModalCatalog/HeaderMenuModalCatalog' +import HeaderMenuModalLink from '../HeaderMenuModalLink/HeaderMenuModalLink' +import HeaderMenuModalSublinks from '../HeaderMenuModalSublinks/HeaderMenuModalSublinks' +import { ICategory } from '../model/types/types' + +import styles from './HeaderMenuModal.module.scss' + +interface IHeaderMenuModal { + categories?: ICategory[] + phoneNumber?: string + isMenuModalOpen?: boolean + handleClose?: () => void +} + +/** + * Модальное окно HeaderMenuModal + * @param {array} categories - массив категорий полученный из редакса; + * @param {string} phoneNumber - телефон полученный из редакса; + * @param {boolean} isMenuModalOpen - состояние открытия модального окна; + * @param {function} handleClose - функция закрытия модального окна; + */ + +const HeaderMenuModal: FC = ({ categories, phoneNumber, isMenuModalOpen, handleClose }) => { + const [isActive, setIsActive] = useState(false) + const [isCatalog, setIsCatalog] = useState(false) + const [choice, setChoice] = useState(0) + const navigate = useNavigate() + + const handleClick = (index: number) => { + setChoice(index) + setIsActive(!isActive) + } + + const handleLink = (route: string) => { + navigate(route) + handleClose + } + + const handleCategory = () => { + setIsActive(!isActive) + setIsCatalog(!isCatalog) + } + + return ( +
        + {!isActive && !isCatalog && ( + <> +
        + + + + Меню + +
        + +
        + +
        + + )} + + {!isCatalog ? ( + !isActive && + ) : ( + + )} + + {!isCatalog && ( +
          + {headerMenuData && + headerMenuData.map((item, index) => ( +
        • (item.link === null ? handleClick(index) : handleLink(item.link))}> + {!isActive ? ( + + ) : ( + + )} +
        • + ))} +
        + )} + + {!isActive && !isCatalog && ( +
        + + Поддержка:  + +   + {phoneNumber || } + + Будни, с 10.00 до 20.00 +
          + {messengerArray.map(item => ( + + ))} +
        +
        + )} +
        + ) +} + +export default HeaderMenuModal diff --git a/src/mockData/headerMenuData.ts b/src/mockData/headerMenuData.ts new file mode 100644 index 00000000..dafcda69 --- /dev/null +++ b/src/mockData/headerMenuData.ts @@ -0,0 +1,39 @@ +import { Routes } from '@/shared/config/routerConfig/routes' + +export const headerMenuData = [ + { + title: 'О нас', + link: null, + routes: [ + { subtitle: 'О нас', route: Routes.ABOUT }, + { subtitle: 'Политика безопасности', route: Routes.HOME }, + { subtitle: 'Обзоры', route: Routes.HOME }, + { subtitle: 'Условия соглашения', route: Routes.HOME } + ] + }, + { + title: 'Блог', + link: Routes.BLOG + }, + { + title: 'Новости', + link: Routes.NEWS + }, + { + title: 'Отзывы о магазине', + link: Routes.REVIEWS + }, + { + title: 'Контакты', + link: Routes.CONTACTS + }, + { + title: 'Помощь', + link: null, + routes: [ + { subtitle: 'Информация о доставке', route: Routes.DELIVERY }, + { subtitle: 'Возвраты', route: Routes.ADD_RETURN }, + { subtitle: 'Подарочные сертификаты', route: Routes.VOUCHERS } + ] + } +] diff --git a/src/pages/FormReturnPage/FormReturnPage.module.scss b/src/pages/FormReturnPage/FormReturnPage.module.scss index e0ea4a2a..32ea7f6b 100644 --- a/src/pages/FormReturnPage/FormReturnPage.module.scss +++ b/src/pages/FormReturnPage/FormReturnPage.module.scss @@ -1,4 +1,4 @@ -@use '../../shared/styles/utils/mixins' as media; +@use '@/shared/styles/utils/mixins' as media; .formReturn { display: flex; @@ -23,7 +23,7 @@ align-items: flex-start; gap: 20px; - @include media.respond-to('middle') { + @include media.respond-to('large') { flex-direction: column; gap: 10px; } @@ -46,5 +46,3 @@ } } } - - diff --git a/src/pages/FormReturnPage/FormReturnPage.tsx b/src/pages/FormReturnPage/FormReturnPage.tsx index 3b8ccb32..e85c384e 100644 --- a/src/pages/FormReturnPage/FormReturnPage.tsx +++ b/src/pages/FormReturnPage/FormReturnPage.tsx @@ -26,7 +26,7 @@ const FormReturnPage: FC = () => { const [isModalClosing, setIsModalClosing] = useState(false) const [user, setUser] = useState('Elon Musk') // позже юзера будем получать из редакса - const { isScreenMd } = useResize() + const { isScreenLg } = useResize() const changeModalState = () => { setIsModalOpen(!isModalOpen) @@ -74,7 +74,7 @@ const FormReturnPage: FC = () => {
    - {isScreenMd ? ( + {isScreenLg ? ( ) : ( diff --git a/src/shared/icons/cart.svg b/src/shared/icons/cart.svg deleted file mode 100644 index 5f2a943c..00000000 --- a/src/shared/icons/cart.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/shared/icons/heart.svg b/src/shared/icons/heart.svg deleted file mode 100644 index 9b3bd37c..00000000 --- a/src/shared/icons/heart.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index f3b6cb66..f21be474 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames' -import { useEffect, useMemo, useState } from 'react' +import { lazy, Suspense, useEffect, useMemo, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { AppDispatch } from '@/app/providers/StoreProvider/config/store' @@ -19,6 +19,7 @@ import IconCategories from '@/shared/icons/IconCategories.svg' import { useResize } from '@/shared/libs/hooks/useResize' import { linkItems } from '@/shared/mockData/catalogListData' import { headerAccountData } from '@/shared/mockData/headerAccountData' +import { Button } from '@/shared/ui/Button/Button' import CatalogLink from '@/shared/ui/CatalogLink/CatalogLink' import CatalogLinkSkeleton from '@/shared/ui/CatalogLink/ui/skeleton/CatalogLinkSkeleton' import ContextMenuElement from '@/shared/ui/ContextMenuElement/ContextMenuElement' @@ -27,6 +28,7 @@ import Logo from '@/shared/ui/logo/Logo' import LogoSkeleton from '@/shared/ui/logo/model/skeleton/LogoSkeleton' import Modal from '@/shared/ui/Modal/Modal' import Paragraph from '@/shared/ui/Paragraph/Paragraph' +import Spinner from '@/shared/ui/Spinner/Spinner' import CatalogNodeItem from '@/widgets/CatalogNodeItem/CatalogNodeItem' import NavigationLink from '@/widgets/NavigationLink/NavigationLink' @@ -37,6 +39,12 @@ import { getCoreBaseHeader } from './model/services/getCoreBaseHeader' import ListItemButton from './ui/ListItemButton' import ListItemLink from './ui/ListItemLink' +const HeaderMenuModal = lazy(() => import('@/features/HeaderMenuModal')) + +/** + * Компонент шапки сайта + */ + function Header() { const dispatch = useDispatch() const categories = useSelector(selectCategories) @@ -44,6 +52,7 @@ function Header() { const displayedCategories = useSelector(selectDisplayedCategories) const [isModalOpen, setIsModalOpen] = useState(false) const [isModalClosing, setIsModalClosing] = useState(false) + const [isMenuModalOpen, setIsMenuModalOpen] = useState(false) const phoneNumber = coreBaseData.header.support.phone_number const logo = coreBaseData.header.main_logo.image const isCategoriesLoading = useSelector(getLoading) @@ -54,6 +63,10 @@ function Header() { setIsModalOpen(!isModalOpen) } + const changeMenuModalState = () => { + setIsMenuModalOpen(!isMenuModalOpen) + } + const aboutUsNode = useMemo( () => (
      @@ -129,7 +142,25 @@ function Header() { )} -
      + + {isMenuModalOpen && ( + + }> + + + + )} + +
      {isScreenLg && (
    ) } diff --git a/src/widgets/ReviewsBlock/ui/ReviewsBlock/reviewsBlock.module.scss b/src/widgets/ReviewsBlock/ui/ReviewsBlock/reviewsBlock.module.scss index f144f0ec..0a6db1ac 100644 --- a/src/widgets/ReviewsBlock/ui/ReviewsBlock/reviewsBlock.module.scss +++ b/src/widgets/ReviewsBlock/ui/ReviewsBlock/reviewsBlock.module.scss @@ -3,6 +3,7 @@ .wrapper { width: 100%; margin: 0 auto; + user-select: none; h2 { font-size: #{'min(max(18px, 1.6vw), 20px)'}; @@ -34,7 +35,7 @@ vertical-align: middle; } - ul { + .list { display: flex; gap: 20px; max-width: 100%; From 99530b252942a932bdfa90b7077af79a00950359 Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Sat, 11 May 2024 15:20:16 +0300 Subject: [PATCH 50/66] fix-1 enhancement_347_header_mobile_menu_modal --- src/assets/icons/IconCatalog.svg | 7 +++++ src/entities/HeaderAccount/HeaderAccount.tsx | 19 +++++++++----- .../HeaderAccount/headerAccount.module.scss | 17 ++++++++++++ .../HeaderMenuModalHeading.tsx | 2 +- .../HeaderMenuModalLink.module.scss | 12 +++++++++ .../HeaderMenuModalLink.tsx | 16 ++++++++++-- .../HeaderMenuModal/ui/HeaderMenuModal.tsx | 26 ++++++++++--------- src/shared/mockData/headerAccountData.ts | 4 +-- 8 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 src/assets/icons/IconCatalog.svg diff --git a/src/assets/icons/IconCatalog.svg b/src/assets/icons/IconCatalog.svg new file mode 100644 index 00000000..638295e9 --- /dev/null +++ b/src/assets/icons/IconCatalog.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/entities/HeaderAccount/HeaderAccount.tsx b/src/entities/HeaderAccount/HeaderAccount.tsx index d56a61f9..f68f3835 100644 --- a/src/entities/HeaderAccount/HeaderAccount.tsx +++ b/src/entities/HeaderAccount/HeaderAccount.tsx @@ -149,6 +149,9 @@ const HeaderAccount: FC = ({ isMenuModalOpen, handleClose, c ? `${styles.headerAccount__cart}` : `${styles.headerAccount__cartMobile}` }> + {!isScreenLg && !isMenuModalOpen && counter && ( +
    {counter}
    + )} {isScreenLg || isMenuModalOpen ? ( <> = ({ isMenuModalOpen, handleClose, c
    ) : ( - +
    + +
    )} diff --git a/src/entities/HeaderAccount/headerAccount.module.scss b/src/entities/HeaderAccount/headerAccount.module.scss index 38751182..bda58e96 100644 --- a/src/entities/HeaderAccount/headerAccount.module.scss +++ b/src/entities/HeaderAccount/headerAccount.module.scss @@ -28,6 +28,7 @@ } &__cartMobile { + position: relative; display: flex; justify-content: center; align-items: center; @@ -71,6 +72,22 @@ color: var.$header-color; } + &__cartCounterMobile { + position: absolute; + top: -12px; + right: -12px; + display: flex; + justify-content: center; + align-items: center; + width: 20px; + height: 20px; + border-radius: 50%; + background: var.$promo-color; + font-size: 13px; + font-weight: 700; + color: var.$white; + } + &__cartTotal { line-height: 13px; font-size: 13px; diff --git a/src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.tsx b/src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.tsx index 278f120c..e9ab8db9 100644 --- a/src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.tsx +++ b/src/features/HeaderMenuModal/HeaderMenuModalHeading/HeaderMenuModalHeading.tsx @@ -16,7 +16,7 @@ interface IHeaderMenuModalHeading { const HeaderMenuModalHeading: FC = ({ handleCategory }) => { return ( - diff --git a/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.module.scss b/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.module.scss index 029a77ac..6be2dc20 100644 --- a/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.module.scss +++ b/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.module.scss @@ -21,6 +21,18 @@ color: var.$header-color; } + &__title { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + width: fit-content; + } + + &__iconCatalog { + fill: var.$body-color; + } + &__arrow { transform: rotate(270deg); } diff --git a/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.tsx b/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.tsx index db7258d9..38ea1422 100644 --- a/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.tsx +++ b/src/features/HeaderMenuModal/HeaderMenuModalLink/HeaderMenuModalLink.tsx @@ -1,12 +1,16 @@ import { FC } from 'react' +import CatalogIcon from '@/assets/icons/IconCatalog.svg' import ArrowIcon from '@/assets/images/sideBarMenu/IconArrowDown.svg' import { Button } from '@/shared/ui/Button/Button' +import { ICategory } from '../model/types/types' + import styles from './HeaderMenuModalLink.module.scss' interface IHeaderMenuModalLink { title?: string + categories?: ICategory[] isVisible?: boolean onClick?: () => void } @@ -14,14 +18,22 @@ interface IHeaderMenuModalLink { /** * Компонент модального окна HeaderMenuModal, отвечающий за развертывание кнопок с именами обьектов массива; * @param {string} title - название линка; + * @param {array} categories - массив категорий полученный из редакса; * @param {boolean} isVisible - значение позволяющее показывать иконку стрелки; * @param {function} onClick - - функция переключения активности; */ -const HeaderMenuModalLink: FC = ({ title, isVisible, onClick }) => { +const HeaderMenuModalLink: FC = ({ title, categories, isVisible, onClick }) => { return ( ) diff --git a/src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx b/src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx index 54ca39b7..efbe876f 100644 --- a/src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx +++ b/src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx @@ -1,7 +1,6 @@ import { FC, useState } from 'react' import Skeleton from 'react-loading-skeleton' import 'react-loading-skeleton/dist/skeleton.css' -import { useNavigate } from 'react-router' import CloseIcon from '@/assets/icons/iconHeaderMenuClose.svg' import PhoneIcon from '@/assets/icons/IconPhone.svg' @@ -41,18 +40,12 @@ const HeaderMenuModal: FC = ({ categories, phoneNumber, isMenu const [isActive, setIsActive] = useState(false) const [isCatalog, setIsCatalog] = useState(false) const [choice, setChoice] = useState(0) - const navigate = useNavigate() const handleClick = (index: number) => { setChoice(index) setIsActive(!isActive) } - const handleLink = (route: string) => { - navigate(route) - handleClose - } - const handleCategory = () => { setIsActive(!isActive) setIsCatalog(!isCatalog) @@ -83,7 +76,14 @@ const HeaderMenuModal: FC = ({ categories, phoneNumber, isMenu )} {!isCatalog ? ( - !isActive && + !isActive && ( + + ) ) : ( = ({ categories, phoneNumber, isMenu
      {headerMenuData && headerMenuData.map((item, index) => ( -
    • (item.link === null ? handleClick(index) : handleLink(item.link))}> +
    • {!isActive ? ( - + (item.link === null ? handleClick(index) : handleClose())} + to={item.link || '#'}> + + ) : ( Date: Sat, 11 May 2024 15:34:15 +0300 Subject: [PATCH 51/66] fix-2 enhancement_347_header_mobile_menu_modal --- src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx b/src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx index efbe876f..7968d717 100644 --- a/src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx +++ b/src/features/HeaderMenuModal/ui/HeaderMenuModal.tsx @@ -100,7 +100,7 @@ const HeaderMenuModal: FC = ({ categories, phoneNumber, isMenu
    • {!isActive ? ( (item.link === null ? handleClick(index) : handleClose())} + onClick={item.link === null ? () => handleClick(index) : handleClose} to={item.link || '#'}> From 5ffae1907097242c215f76bdffb65df6c4822261 Mon Sep 17 00:00:00 2001 From: Alexander Morugin Date: Sun, 12 May 2024 14:56:42 +0300 Subject: [PATCH 52/66] fix-3 enhancement_347_header_mobile_menu_modal --- src/entities/HeaderAccount/HeaderAccount.tsx | 6 +++++- .../CallBack/ui/CallBack/CallBack.module.scss | 16 ++++++--------- .../CallBack/ui/CallBack/CallBack.tsx | 4 ++-- .../login/ui/LoginForm/LoginForm.module.scss | 20 +++++++++++++++++++ src/features/login/ui/LoginForm/LoginForm.tsx | 18 ++++++++++++++++- src/pages/FormReturnPage/FormReturnPage.tsx | 6 +++++- src/widgets/Header/Header.tsx | 6 +++++- 7 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/entities/HeaderAccount/HeaderAccount.tsx b/src/entities/HeaderAccount/HeaderAccount.tsx index f68f3835..41e19ccf 100644 --- a/src/entities/HeaderAccount/HeaderAccount.tsx +++ b/src/entities/HeaderAccount/HeaderAccount.tsx @@ -54,6 +54,10 @@ const HeaderAccount: FC = ({ isMenuModalOpen, handleClose, c dispatch(loginActions.errorReset()) } + const closeModal = () => { + setIsModalClosing(true) + } + const onLogout = () => { dispatch(logout()) } @@ -73,7 +77,7 @@ const HeaderAccount: FC = ({ isMenuModalOpen, handleClose, c isModalClosing={isModalClosing} setIsModalClosing={setIsModalClosing}> }> - + )} diff --git a/src/features/CallBack/ui/CallBack/CallBack.module.scss b/src/features/CallBack/ui/CallBack/CallBack.module.scss index 0af86877..b4ffcacf 100644 --- a/src/features/CallBack/ui/CallBack/CallBack.module.scss +++ b/src/features/CallBack/ui/CallBack/CallBack.module.scss @@ -1,4 +1,4 @@ -@use '../../../../shared/styles/utils/variables' as var; +@use '@/shared/styles/utils/variables' as var; .form { position: relative; @@ -27,18 +27,14 @@ padding: 0; border: none; cursor: pointer; +} - path { - width: 100%; - height: 100%; - fill: var.$black; - transition: fill 300ms; - } +.close-icon { + fill: var.$body-color; + transition: 0.25s; &:hover { - path { - fill: var.$theme-primary-color; - } + fill: var.$header-color; } } diff --git a/src/features/CallBack/ui/CallBack/CallBack.tsx b/src/features/CallBack/ui/CallBack/CallBack.tsx index a795c34f..1c086091 100644 --- a/src/features/CallBack/ui/CallBack/CallBack.tsx +++ b/src/features/CallBack/ui/CallBack/CallBack.tsx @@ -1,7 +1,7 @@ import { ErrorMessage, Field, Form, Formik, FormikHelpers } from 'formik' import React, { useCallback } from 'react' -import IconClose from '@/assets/icons/IconClose.svg' +import IconClose from '@/assets/icons/iconHeaderMenuClose.svg' import { CallBackData } from '@/features/CallBack/models/types/types' import { validationSchema } from '@/features/CallBack/models/validation/validation' import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button/Button' @@ -51,7 +51,7 @@ export const CallBack: React.FC = ({ setIsModalClosing }) => { {({ isValid, dirty, isSubmitting }) => (
      Заказать обратный звонок