From 2bdb70fe2108175a2f7cdbefb4f45fa63556e18b Mon Sep 17 00:00:00 2001 From: Yulia Avramenko Date: Mon, 22 Jan 2024 15:29:55 +0300 Subject: [PATCH 01/19] #146-api-inserted-to-ReviewsBlock --- .../SroreProvider/config/StateSchema.ts | 2 + .../providers/SroreProvider/config/store.ts | 4 +- .../ui/CardReview/CardReview.stories.tsx | 12 +++--- .../CardReview/ui/CardReview/CardReview.tsx | 40 +++++++++++++------ src/pages/MainPage/MainPage.tsx | 4 +- src/shared/api/types.ts | 3 +- .../ReviewsBlock/model/selectors/selectors.ts | 5 +++ .../model/services/getStoreReviews.ts | 21 ++++++++++ .../ReviewsBlock/model/slice/reviewsSlice.ts | 33 +++++++++++++++ src/widgets/ReviewsBlock/model/types/types.ts | 29 ++++++++++++++ .../ui/ReviewsBlock/ReviewsBlock.stories.tsx | 3 +- .../ui/ReviewsBlock/ReviewsBlock.tsx | 27 ++++++++++--- 12 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 src/widgets/ReviewsBlock/model/selectors/selectors.ts create mode 100644 src/widgets/ReviewsBlock/model/services/getStoreReviews.ts create mode 100644 src/widgets/ReviewsBlock/model/slice/reviewsSlice.ts create mode 100644 src/widgets/ReviewsBlock/model/types/types.ts diff --git a/src/app/providers/SroreProvider/config/StateSchema.ts b/src/app/providers/SroreProvider/config/StateSchema.ts index 2adc174a..cd8c0fc1 100644 --- a/src/app/providers/SroreProvider/config/StateSchema.ts +++ b/src/app/providers/SroreProvider/config/StateSchema.ts @@ -1,8 +1,10 @@ import { LoginSchema } from '@/features/login/model/types/types' import { ApiInstance } from '@/shared/api/api' +import { StoreReviewsSchema } from '@/widgets/ReviewsBlock/model/types/types' export interface StateSchema { login: LoginSchema + storeReviews: StoreReviewsSchema } export interface ThunkExtraArg { diff --git a/src/app/providers/SroreProvider/config/store.ts b/src/app/providers/SroreProvider/config/store.ts index eb73b868..a3254d4a 100644 --- a/src/app/providers/SroreProvider/config/store.ts +++ b/src/app/providers/SroreProvider/config/store.ts @@ -2,9 +2,11 @@ import { configureStore, ReducersMapObject } from '@reduxjs/toolkit' import { loginReducer } from '@/features/login/model/slice/loginSlice' import { StateSchema, ThunkExtraArg } from './StateSchema' import { $api } from '@/shared/api/api' +import { storeReviewsReducer } from '@/widgets/ReviewsBlock/model/slice/reviewsSlice' const rootReducer: ReducersMapObject = { - login: loginReducer + login: loginReducer, + storeReviews: storeReviewsReducer } export function createReduxStore(initialState: StateSchema) { diff --git a/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx b/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx index c45fa99b..f65f18ee 100644 --- a/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx +++ b/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx @@ -15,12 +15,10 @@ type Story = StoryObj export const Default: Story = { args: { - review: { - id: 1, - name: 'Рейтинг нашего магазина', - score: '4.3', - text: 'Мы очень ним гордимся, это результат упорного труда в течении длительного времени и сейчас наша команда ежедневно работает над улучшением сервиса.', - date: '' - } + pk: 1, + name: 'Рейтинг нашего магазина', + score: 4.3, + text: 'Мы очень ним гордимся, это результат упорного труда в течении длительного времени и сейчас наша команда ежедневно работает над улучшением сервиса.', + date: '' } } diff --git a/src/entities/CardReview/ui/CardReview/CardReview.tsx b/src/entities/CardReview/ui/CardReview/CardReview.tsx index b77adfb1..2fd8311d 100644 --- a/src/entities/CardReview/ui/CardReview/CardReview.tsx +++ b/src/entities/CardReview/ui/CardReview/CardReview.tsx @@ -1,5 +1,4 @@ -import { FC, useMemo } from 'react' -import { TReview } from '@/models/ReviewModel' +import { FC, useEffect, useMemo, useState } from 'react' import IconStar from '@/assets/icons/IconStar' import Paragraph from '@/shared/ui/Paragraph/Paragraph' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' @@ -7,26 +6,41 @@ import Link from '@/shared/ui/Link/Link' import styles from './cardReview.module.scss' export type Props = { - review: TReview + // review: TReview + pk: number + text: string + date: string + score: number + name: string } -const CardReview: FC = props => { - const { review } = props +const CardReview: FC = ({ pk, text, date, score, name }) => { + // const { review } = props + const [parsedDate, setParsedDate] = useState() const initials = useMemo(() => { - return review.name.slice(0, 1) + return name.slice(0, 1) }, [0, 1]) const linkTextStyle = styles.link__text + useEffect(() => { + const _parsedDate = new Date(date) + const day = _parsedDate.getDate() + const month = _parsedDate.toLocaleString('ru', { month: 'long' }) + const year = _parsedDate.getFullYear() + + setParsedDate(`${day} ${month}, ${year}`) + }, [date]) + return (
- {review.id === 0 ? ( + {pk === 0 ? ( <> - {review.name} - {review.score} + {name} - {score} -

{review.text}

+

{text}

Вы можете{' '} @@ -44,16 +58,16 @@ const CardReview: FC = props => {

{initials}
- {review.name} + {name} - Оценил(а) магазин на {review.score} + Оценил(а) магазин на {score}
- {review.text} - {review.date} + {text} + {parsedDate}
Читать полный отзыв diff --git a/src/pages/MainPage/MainPage.tsx b/src/pages/MainPage/MainPage.tsx index b3d5a5ef..6666b801 100644 --- a/src/pages/MainPage/MainPage.tsx +++ b/src/pages/MainPage/MainPage.tsx @@ -5,7 +5,6 @@ import BrandsBlock from '@/widgets/BrandBlock/BrandBlock' import { storiesData } from '@/mockData/storiesData' import { blogData } from '@/mockData/blogData' import { newsData } from '@/mockData/newsData' -import { reviewsData } from '@/mockData/reviews.Data' import { TEXT_STORIES, TEXT_BLOG, @@ -20,7 +19,6 @@ import CategoryGrid from '@/widgets/CategoryGrid/CategoryGrid' import ReviewsBlock from '@/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock' import Advantages from '@/widgets/Advantages/ui/Advantages/Advantages' - const MainPage = () => { return ( <> @@ -30,7 +28,7 @@ const MainPage = () => { - + diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index 7fb56991..64ea9c98 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -1,6 +1,7 @@ export enum ApiRoutes { LOGIN = 'token/login', - LOGOUT = 'token/logout' + LOGOUT = 'token/logout', + STORE_REVIEWS = 'store-reviews' } export enum ApiErrorTypes { diff --git a/src/widgets/ReviewsBlock/model/selectors/selectors.ts b/src/widgets/ReviewsBlock/model/selectors/selectors.ts new file mode 100644 index 00000000..cc5acf49 --- /dev/null +++ b/src/widgets/ReviewsBlock/model/selectors/selectors.ts @@ -0,0 +1,5 @@ +import { StateSchema } from '@/app/providers/SroreProvider' + +export const getStoreReviewsSelector = (state: StateSchema) => { + return state.storeReviews.reviews +} diff --git a/src/widgets/ReviewsBlock/model/services/getStoreReviews.ts b/src/widgets/ReviewsBlock/model/services/getStoreReviews.ts new file mode 100644 index 00000000..0d60e05e --- /dev/null +++ b/src/widgets/ReviewsBlock/model/services/getStoreReviews.ts @@ -0,0 +1,21 @@ +import { createAsyncThunk } from '@reduxjs/toolkit' +import { ThunkConfig } from '@/app/providers/SroreProvider/config/StateSchema' +import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types' +import { apiErrorIdentify } from '@/shared/api/apiErrorIdentify' +import { StoreReviewData } from '../types/types' + +export const getStoreReviews = createAsyncThunk>( //void1- выходные данные, void2- входные данные , thunkConfig- тип store + 'store-reviews', // action type, первый аргумент + async (_, thunkAPI) => { + // второй аргумент- асинхронная функция , кот вызовет dispatch в компоненте + const { rejectWithValue, extra } = thunkAPI + try { + const { data } = await extra.api.get(ApiRoutes.STORE_REVIEWS) + console.log(data) + + return data.results + } catch (error) { + return rejectWithValue(apiErrorIdentify(error, ApiErrorTypes.DATA_EMPTY_ERROR)) + } + } +) diff --git a/src/widgets/ReviewsBlock/model/slice/reviewsSlice.ts b/src/widgets/ReviewsBlock/model/slice/reviewsSlice.ts new file mode 100644 index 00000000..1792dc3b --- /dev/null +++ b/src/widgets/ReviewsBlock/model/slice/reviewsSlice.ts @@ -0,0 +1,33 @@ +import { createSlice } from '@reduxjs/toolkit' +import { StoreReviewsSchema } from '../types/types' +import { getStoreReviews } from '../services/getStoreReviews' + +const initialState: StoreReviewsSchema = { + isLoading: false, + reviews: [] +} + +export const reviewsSlice = createSlice({ + name: 'storeReviews', + initialState, + reducers: { + // для обычных actions, не thunk + }, + extraReducers: builder => { + // для thunk actions + builder + .addCase(getStoreReviews.pending, state => { + state.isLoading = true + }) + .addCase(getStoreReviews.fulfilled, (state, { payload }) => { + state.isLoading = false + state.reviews = payload + //state.reviews = payload.test2 // StoreeviewsData с сервера переклыдвается в StoreReviewsSchema (наше Redux хранилище) + }) + .addCase(getStoreReviews.rejected, state => { + state.isLoading = false + }) + } +}) + +export const { actions: reviewsActions, reducer: storeReviewsReducer } = reviewsSlice diff --git a/src/widgets/ReviewsBlock/model/types/types.ts b/src/widgets/ReviewsBlock/model/types/types.ts new file mode 100644 index 00000000..ad286611 --- /dev/null +++ b/src/widgets/ReviewsBlock/model/types/types.ts @@ -0,0 +1,29 @@ +export interface GetStoreReviewsResponse { + count: number + previous: string + next: string + results: StoreReviewData[] +} + +export interface StoreReviewData { + pk: number + text: string + pub_date: string + author_name: string + author_email: string + average_score: number + delivery_speed_score: number + quality_score: number + price_score: number + replay: StoreReviewReplay +} +export interface StoreReviewReplay { + text: string + pub_date: string + name: string +} + +export interface StoreReviewsSchema { + isLoading: boolean + reviews: StoreReviewData[] +} diff --git a/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.stories.tsx b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.stories.tsx index 82c25356..87f2cb3c 100644 --- a/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.stories.tsx +++ b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.stories.tsx @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react' import ReviewsBlock from './ReviewsBlock' -import { reviewsData } from '@/mockData/reviews.Data' import { LINK_REVIEWS_ALL, TEXT_CUSTOMERS_ABOUT_US } from '@/shared/constants/constants' const meta = { @@ -18,7 +17,7 @@ type Story = StoryObj export const Default: Story = { args: { title: TEXT_CUSTOMERS_ABOUT_US, - reviews: reviewsData, + // reviews: reviewsData, linkText: LINK_REVIEWS_ALL } } diff --git a/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx index fa75e9ce..ab0a9972 100644 --- a/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx +++ b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx @@ -1,17 +1,19 @@ -import { type FC } from 'react' -import { TReview } from '@/models/ReviewModel' +import { useEffect, type FC } from 'react' import IconHand from '@/assets/images/img-hand.png.png' import IconLink from '@/assets/icons/IconLink' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' import Link from '@/shared/ui/Link/Link' import styles from './reviewsBlock.module.scss' import CardReview from '@/entities/CardReview/ui/CardReview/CardReview' +import { getStoreReviews } from '../../model/services/getStoreReviews' +import { getStoreReviewsSelector } from '../../model/selectors/selectors' +import { useSelector } from 'react-redux' +import { useAppDispatch } from '@/shared/libs/hooks/store' export type Props = { title: string linkText?: string linkPath?: string - reviews: TReview[] } /** @@ -19,12 +21,18 @@ export type Props = { * @param {string} title - загаловок контейнера * @param {string} linkText - загаловок ссылки * @param {string} linkPath - адрес ссылки - * @param {array} reviews - массив отзывов */ const ReviewsBlock: FC = props => { - const { title, linkText = '', linkPath = '', reviews } = props + const { title, linkText = '', linkPath = '' } = props const linkTextStyle = styles.link + const dispatch = useAppDispatch() + const reviews = useSelector(getStoreReviewsSelector) + + useEffect(() => { + dispatch(getStoreReviews()) + }, []) + return (
@@ -40,7 +48,14 @@ const ReviewsBlock: FC = props => {
    {reviews.map(item => ( - + ))}
From 43eb922dbaf59b04d2b3a70a6e728c522a182158 Mon Sep 17 00:00:00 2001 From: Artur Khelshtein Date: Tue, 23 Jan 2024 00:30:35 +0300 Subject: [PATCH 02/19] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=BE=D0=B1=D0=B5=D1=80=D1=82=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/Contacts/contacts.module.scss | 2 +- src/shared/ui/Modal/Modal.tsx | 34 +++++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/features/Contacts/contacts.module.scss b/src/features/Contacts/contacts.module.scss index 1c32fb54..e8ada6e4 100644 --- a/src/features/Contacts/contacts.module.scss +++ b/src/features/Contacts/contacts.module.scss @@ -4,7 +4,7 @@ position: fixed; right: 20px; bottom: 20px; - z-index: 999; + z-index: 2; } .contactsMenu { diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index 01176d0a..e595700b 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -2,9 +2,7 @@ import React, { HTMLAttributes, useCallback, useEffect, useRef, useState } from import classNames from 'classnames' import { createFocusTrap } from 'focus-trap' import { createPortal } from 'react-dom' -import IconClose from '@/assets/icons/IconClose.svg' import styles from './Modal.module.scss' -import { Button } from '@/shared/ui/Button/Button' interface IModalProps extends HTMLAttributes { isModalOpen: boolean @@ -26,7 +24,6 @@ interface IModalProps extends HTMLAttributes { export default function Modal({ isModalOpen, onClose, className, children }: IModalProps) { const modalRef = useRef(null) const [isModalClosing, setIsModalClosing] = useState(false) - const closeDelayTimeout = useRef(null) const handleClose = useCallback(() => { setIsModalClosing(true) @@ -65,26 +62,44 @@ export default function Modal({ isModalOpen, onClose, className, children }: IMo [handleClose, isModalOpen] ) + const handleClickWrapper = useCallback( + (event: MouseEvent) => { + if (modalRef.current === event.target && isModalOpen) { + const neighboringModals = document.querySelectorAll(`.${styles['modal-container']}`) + const lastNeighboringModal = neighboringModals[neighboringModals.length - 1] + const isLastModal = lastNeighboringModal && lastNeighboringModal.contains(modalRef.current) + if (isLastModal) { + handleClose() + } + } + }, + [handleClose, isModalOpen] + ) + // Для добавления слушателей событий при открытии модального окна и их удаления при его закрытии // Позволяет избежать возможных проблем с утечками памяти или продолжительной работы слушателей, когда они больше не нужны // useEffect обеспечивает активацию и деактивацию (через return) обработчиков событий useEffect(() => { + document.addEventListener('mousedown', handleClickWrapper) document.addEventListener('keydown', handleKeyDown) return () => { + document.removeEventListener('mousedown', handleClickWrapper) document.removeEventListener('keydown', handleKeyDown) } - }, [handleKeyDown]) + }, [handleKeyDown, handleClickWrapper]) // Таймер затираем на стадии размонтирования, т.к. реакт много раз рендерится и глобальная область заполняется useEffect(() => { + let closeTimeout: NodeJS.Timeout if (isModalClosing) { - closeDelayTimeout.current = window.setTimeout(() => { + closeTimeout = setTimeout(() => { closeModal() }, 300) - } else if (closeDelayTimeout.current) { - clearTimeout(closeDelayTimeout.current) - closeDelayTimeout.current = null + } + + return () => { + clearTimeout(closeTimeout) } }, [isModalClosing, closeModal]) @@ -105,9 +120,6 @@ export default function Modal({ isModalOpen, onClose, className, children }: IMo return createPortal(
- {children}
, From e27d192bbb37ecb7a320d29b0accedb9782d00b9 Mon Sep 17 00:00:00 2001 From: Artur Khelshtein Date: Tue, 23 Jan 2024 15:38:02 +0300 Subject: [PATCH 03/19] refactoring --- src/shared/ui/Modal/Modal.module.scss | 11 ----------- src/shared/ui/Modal/Modal.tsx | 4 ---- 2 files changed, 15 deletions(-) diff --git a/src/shared/ui/Modal/Modal.module.scss b/src/shared/ui/Modal/Modal.module.scss index a89b3a48..994fdc0a 100644 --- a/src/shared/ui/Modal/Modal.module.scss +++ b/src/shared/ui/Modal/Modal.module.scss @@ -29,17 +29,6 @@ &.modal-zoom-out { animation: zoom-out 300ms ease-in-out; } - - .cross-button { - cursor: pointer; - position: absolute; - top: 50px; - right: 56px; - - path { - fill: var.$white; - } - } } @keyframes zoom-in { diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index e595700b..09937b6b 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -11,8 +11,6 @@ interface IModalProps extends HTMLAttributes { } // Поменял импорт на дефолтный, чтобы можно было использовать React.lazy -// @TODO: Ограничить перемещение табом внутри одного поп-апа -// https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/106 /** * Functional component for a modal window. @@ -42,8 +40,6 @@ export default function Modal({ isModalOpen, onClose, className, children }: IMo className ) - // @TODO: Не работает клик по wrapper для закрытия модалки - // https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/118 const handleContentClick = useCallback((event: React.MouseEvent) => { event.stopPropagation() }, []) From b1bcf6f9260750418567a5a75abd446daf90b5d7 Mon Sep 17 00:00:00 2001 From: Artur Khelshtein Date: Tue, 23 Jan 2024 15:43:10 +0300 Subject: [PATCH 04/19] =?UTF-8?q?Revert=20"=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=20=D0=BE=D0=B1=D0=B5=D1=80=D1=82=D0=BA?= =?UTF-8?q?=D0=B5"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 6b1a91aa1bcef949e9a010ef2bc1e76a5df24067. --- src/features/Contacts/contacts.module.scss | 2 +- src/shared/ui/Modal/Modal.tsx | 34 +++++++--------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/features/Contacts/contacts.module.scss b/src/features/Contacts/contacts.module.scss index e8ada6e4..1c32fb54 100644 --- a/src/features/Contacts/contacts.module.scss +++ b/src/features/Contacts/contacts.module.scss @@ -4,7 +4,7 @@ position: fixed; right: 20px; bottom: 20px; - z-index: 2; + z-index: 999; } .contactsMenu { diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index 09937b6b..1404d730 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -2,7 +2,9 @@ import React, { HTMLAttributes, useCallback, useEffect, useRef, useState } from import classNames from 'classnames' import { createFocusTrap } from 'focus-trap' import { createPortal } from 'react-dom' +import IconClose from '@/assets/icons/IconClose.svg' import styles from './Modal.module.scss' +import { Button } from '@/shared/ui/Button/Button' interface IModalProps extends HTMLAttributes { isModalOpen: boolean @@ -22,6 +24,7 @@ interface IModalProps extends HTMLAttributes { export default function Modal({ isModalOpen, onClose, className, children }: IModalProps) { const modalRef = useRef(null) const [isModalClosing, setIsModalClosing] = useState(false) + const closeDelayTimeout = useRef(null) const handleClose = useCallback(() => { setIsModalClosing(true) @@ -58,44 +61,26 @@ export default function Modal({ isModalOpen, onClose, className, children }: IMo [handleClose, isModalOpen] ) - const handleClickWrapper = useCallback( - (event: MouseEvent) => { - if (modalRef.current === event.target && isModalOpen) { - const neighboringModals = document.querySelectorAll(`.${styles['modal-container']}`) - const lastNeighboringModal = neighboringModals[neighboringModals.length - 1] - const isLastModal = lastNeighboringModal && lastNeighboringModal.contains(modalRef.current) - if (isLastModal) { - handleClose() - } - } - }, - [handleClose, isModalOpen] - ) - // Для добавления слушателей событий при открытии модального окна и их удаления при его закрытии // Позволяет избежать возможных проблем с утечками памяти или продолжительной работы слушателей, когда они больше не нужны // useEffect обеспечивает активацию и деактивацию (через return) обработчиков событий useEffect(() => { - document.addEventListener('mousedown', handleClickWrapper) document.addEventListener('keydown', handleKeyDown) return () => { - document.removeEventListener('mousedown', handleClickWrapper) document.removeEventListener('keydown', handleKeyDown) } - }, [handleKeyDown, handleClickWrapper]) + }, [handleKeyDown]) // Таймер затираем на стадии размонтирования, т.к. реакт много раз рендерится и глобальная область заполняется useEffect(() => { - let closeTimeout: NodeJS.Timeout if (isModalClosing) { - closeTimeout = setTimeout(() => { + closeDelayTimeout.current = window.setTimeout(() => { closeModal() }, 300) - } - - return () => { - clearTimeout(closeTimeout) + } else if (closeDelayTimeout.current) { + clearTimeout(closeDelayTimeout.current) + closeDelayTimeout.current = null } }, [isModalClosing, closeModal]) @@ -116,6 +101,9 @@ export default function Modal({ isModalOpen, onClose, className, children }: IMo return createPortal(
+ {children}
, From b9211b3e19abd3adfb78f42af5513a05f24c3442 Mon Sep 17 00:00:00 2001 From: Artur Khelshtein Date: Tue, 23 Jan 2024 15:53:59 +0300 Subject: [PATCH 05/19] =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BD=D1=83=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D0=BE=D1=82=D0=B5=D1=80=D1=8F=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20rebase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/Contacts/contacts.module.scss | 2 +- src/shared/ui/Modal/Modal.tsx | 34 +++++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/features/Contacts/contacts.module.scss b/src/features/Contacts/contacts.module.scss index 1c32fb54..e8ada6e4 100644 --- a/src/features/Contacts/contacts.module.scss +++ b/src/features/Contacts/contacts.module.scss @@ -4,7 +4,7 @@ position: fixed; right: 20px; bottom: 20px; - z-index: 999; + z-index: 2; } .contactsMenu { diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index 1404d730..09937b6b 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -2,9 +2,7 @@ import React, { HTMLAttributes, useCallback, useEffect, useRef, useState } from import classNames from 'classnames' import { createFocusTrap } from 'focus-trap' import { createPortal } from 'react-dom' -import IconClose from '@/assets/icons/IconClose.svg' import styles from './Modal.module.scss' -import { Button } from '@/shared/ui/Button/Button' interface IModalProps extends HTMLAttributes { isModalOpen: boolean @@ -24,7 +22,6 @@ interface IModalProps extends HTMLAttributes { export default function Modal({ isModalOpen, onClose, className, children }: IModalProps) { const modalRef = useRef(null) const [isModalClosing, setIsModalClosing] = useState(false) - const closeDelayTimeout = useRef(null) const handleClose = useCallback(() => { setIsModalClosing(true) @@ -61,26 +58,44 @@ export default function Modal({ isModalOpen, onClose, className, children }: IMo [handleClose, isModalOpen] ) + const handleClickWrapper = useCallback( + (event: MouseEvent) => { + if (modalRef.current === event.target && isModalOpen) { + const neighboringModals = document.querySelectorAll(`.${styles['modal-container']}`) + const lastNeighboringModal = neighboringModals[neighboringModals.length - 1] + const isLastModal = lastNeighboringModal && lastNeighboringModal.contains(modalRef.current) + if (isLastModal) { + handleClose() + } + } + }, + [handleClose, isModalOpen] + ) + // Для добавления слушателей событий при открытии модального окна и их удаления при его закрытии // Позволяет избежать возможных проблем с утечками памяти или продолжительной работы слушателей, когда они больше не нужны // useEffect обеспечивает активацию и деактивацию (через return) обработчиков событий useEffect(() => { + document.addEventListener('mousedown', handleClickWrapper) document.addEventListener('keydown', handleKeyDown) return () => { + document.removeEventListener('mousedown', handleClickWrapper) document.removeEventListener('keydown', handleKeyDown) } - }, [handleKeyDown]) + }, [handleKeyDown, handleClickWrapper]) // Таймер затираем на стадии размонтирования, т.к. реакт много раз рендерится и глобальная область заполняется useEffect(() => { + let closeTimeout: NodeJS.Timeout if (isModalClosing) { - closeDelayTimeout.current = window.setTimeout(() => { + closeTimeout = setTimeout(() => { closeModal() }, 300) - } else if (closeDelayTimeout.current) { - clearTimeout(closeDelayTimeout.current) - closeDelayTimeout.current = null + } + + return () => { + clearTimeout(closeTimeout) } }, [isModalClosing, closeModal]) @@ -101,9 +116,6 @@ export default function Modal({ isModalOpen, onClose, className, children }: IMo return createPortal(
- {children}
, From b557b7e45cecb2113910dc23ef5222bf15561b84 Mon Sep 17 00:00:00 2001 From: Artur Khelshtein Date: Wed, 24 Jan 2024 11:16:25 +0300 Subject: [PATCH 06/19] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=BE=D1=82=D0=BA=D1=80=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BC=D0=BE=D0=B4=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=BE=D0=BA=D0=BD=D0=B0=20=D0=BF=D0=BE=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=85=20=D0=B4=D1=80=D1=83=D0=B3=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BE=D0=BA=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Modal/Modal.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index 09937b6b..8b2c3d8f 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -101,7 +101,17 @@ export default function Modal({ isModalOpen, onClose, className, children }: IMo useEffect(() => { const trap = createFocusTrap(modalRef.current as HTMLDivElement, { - allowOutsideClick: true + allowOutsideClick: true, + checkCanFocusTrap: async (): Promise => { + await new Promise(resolve => { + // Таймер для включения ловушки фокуса. Без него выпадает ошибка для вложенных модальных окон + // о том что необходим хотя бы один node для фокусировки. Также решалось добавлением пустой кнопки, + // но на неё можно было табом перейти + setTimeout(() => { + resolve() + }, 400) + }) + } }) if (isModalOpen) { From 217b932ba935c6fdfb29a9a23c2fdbe750df32f9 Mon Sep 17 00:00:00 2001 From: Yulia Avramenko Date: Wed, 24 Jan 2024 13:54:21 +0300 Subject: [PATCH 07/19] #146-bug-fix --- .../ui/CardReview/CardReview.stories.tsx | 2 +- .../CardReview/ui/CardReview/CardReview.tsx | 26 ++++++++++++------- .../model/services/getStoreReviews.ts | 4 +-- .../ui/ReviewsBlock/ReviewsBlock.stories.tsx | 1 - .../ui/ReviewsBlock/ReviewsBlock.tsx | 6 ++--- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx b/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx index f65f18ee..1289f31a 100644 --- a/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx +++ b/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx @@ -19,6 +19,6 @@ export const Default: Story = { name: 'Рейтинг нашего магазина', score: 4.3, text: 'Мы очень ним гордимся, это результат упорного труда в течении длительного времени и сейчас наша команда ежедневно работает над улучшением сервиса.', - date: '' + date: '2024-01-22T09:42:35.242681+03:00' } } diff --git a/src/entities/CardReview/ui/CardReview/CardReview.tsx b/src/entities/CardReview/ui/CardReview/CardReview.tsx index 2fd8311d..1181d05a 100644 --- a/src/entities/CardReview/ui/CardReview/CardReview.tsx +++ b/src/entities/CardReview/ui/CardReview/CardReview.tsx @@ -1,12 +1,12 @@ -import { FC, useEffect, useMemo, useState } from 'react' +import { FC, useMemo } from 'react' import IconStar from '@/assets/icons/IconStar' import Paragraph from '@/shared/ui/Paragraph/Paragraph' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' import Link from '@/shared/ui/Link/Link' import styles from './cardReview.module.scss' +import Subheading from '@/shared/ui/Subheading/Subheading' export type Props = { - // review: TReview pk: number text: string date: string @@ -14,21 +14,27 @@ export type Props = { name: string } +/** + * Отзыв + * @param {number} pk - id отзыва + * @param {string} text - текст отзыва + * @param {string} date - дата отзыва + * @param {number} score - очко рейтинга отзыва + * @param {string} name - имя оставившего отзыв + */ + const CardReview: FC = ({ pk, text, date, score, name }) => { - // const { review } = props - const [parsedDate, setParsedDate] = useState() const initials = useMemo(() => { return name.slice(0, 1) }, [0, 1]) const linkTextStyle = styles.link__text - useEffect(() => { + const newDate = useMemo(() => { const _parsedDate = new Date(date) - const day = _parsedDate.getDate() - const month = _parsedDate.toLocaleString('ru', { month: 'long' }) const year = _parsedDate.getFullYear() + const formatter = new Intl.DateTimeFormat('ru', { month: 'long', day: 'numeric' }).format(_parsedDate) - setParsedDate(`${day} ${month}, ${year}`) + return `${formatter}, ${year}` }, [date]) return ( @@ -61,13 +67,13 @@ const CardReview: FC = ({ pk, text, date, score, name }) => { {name} Оценил(а) магазин на {score} - +
{text} - {parsedDate} + {newDate}
Читать полный отзыв diff --git a/src/widgets/ReviewsBlock/model/services/getStoreReviews.ts b/src/widgets/ReviewsBlock/model/services/getStoreReviews.ts index 0d60e05e..686917fa 100644 --- a/src/widgets/ReviewsBlock/model/services/getStoreReviews.ts +++ b/src/widgets/ReviewsBlock/model/services/getStoreReviews.ts @@ -4,14 +4,14 @@ import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types' import { apiErrorIdentify } from '@/shared/api/apiErrorIdentify' import { StoreReviewData } from '../types/types' -export const getStoreReviews = createAsyncThunk>( //void1- выходные данные, void2- входные данные , thunkConfig- тип store +export const getStoreReviews = createAsyncThunk>( + //void1- выходные данные, void2- входные данные , thunkConfig- тип store 'store-reviews', // action type, первый аргумент async (_, thunkAPI) => { // второй аргумент- асинхронная функция , кот вызовет dispatch в компоненте const { rejectWithValue, extra } = thunkAPI try { const { data } = await extra.api.get(ApiRoutes.STORE_REVIEWS) - console.log(data) return data.results } catch (error) { diff --git a/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.stories.tsx b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.stories.tsx index 87f2cb3c..da46ff64 100644 --- a/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.stories.tsx +++ b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.stories.tsx @@ -17,7 +17,6 @@ type Story = StoryObj export const Default: Story = { args: { title: TEXT_CUSTOMERS_ABOUT_US, - // reviews: reviewsData, linkText: LINK_REVIEWS_ALL } } diff --git a/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx index ab0a9972..629ed3e5 100644 --- a/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx +++ b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx @@ -1,3 +1,6 @@ +import { getStoreReviewsSelector } from '../../model/selectors/selectors' +import { useSelector } from 'react-redux' +import { useAppDispatch } from '@/shared/libs/hooks/store' import { useEffect, type FC } from 'react' import IconHand from '@/assets/images/img-hand.png.png' import IconLink from '@/assets/icons/IconLink' @@ -6,9 +9,6 @@ import Link from '@/shared/ui/Link/Link' import styles from './reviewsBlock.module.scss' import CardReview from '@/entities/CardReview/ui/CardReview/CardReview' import { getStoreReviews } from '../../model/services/getStoreReviews' -import { getStoreReviewsSelector } from '../../model/selectors/selectors' -import { useSelector } from 'react-redux' -import { useAppDispatch } from '@/shared/libs/hooks/store' export type Props = { title: string From 368c5cecfaf16d53f13e288198bfecda94795efe Mon Sep 17 00:00:00 2001 From: Artur Khelshtein Date: Thu, 25 Jan 2024 00:13:31 +0300 Subject: [PATCH 08/19] =?UTF-8?q?=D0=BE=D0=BF=D0=B5=D1=87=D0=B0=D1=82?= =?UTF-8?q?=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/Payments/Payments.stories.ts | 2 +- src/mockData/coreBaseData.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/entities/Payments/Payments.stories.ts b/src/entities/Payments/Payments.stories.ts index ba7cf140..eec9be77 100644 --- a/src/entities/Payments/Payments.stories.ts +++ b/src/entities/Payments/Payments.stories.ts @@ -17,7 +17,7 @@ const data = { } ], support: { - name: 'Обрантый звонок', + name: 'Обратный звонок', phone_number: '+7 977 848-02-28' } } diff --git a/src/mockData/coreBaseData.ts b/src/mockData/coreBaseData.ts index f9382532..dc146c8a 100644 --- a/src/mockData/coreBaseData.ts +++ b/src/mockData/coreBaseData.ts @@ -6,7 +6,7 @@ export const coreBaseData = { title: 'Интернет-магазин «Maxboom.ru»' }, support: { - name: 'Обрантый звонок', + name: 'Обратный звонок', phone_number: '+7 977 848-02-28' } }, @@ -47,7 +47,7 @@ export const coreBaseData = { } ], support: { - callback: 'Обрантый звонок', + callback: 'Обратный звонок', phone_number: '+7 977 848-02-28' } } From a23075bb99bad1ceb03152cecf49a0b6d1cf8196 Mon Sep 17 00:00:00 2001 From: Artur Khelshtein Date: Thu, 25 Jan 2024 00:18:26 +0300 Subject: [PATCH 09/19] =?UTF-8?q?=D0=B2=D0=BD=D0=B5=D0=B4=D1=80=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BC=D0=BE=D0=B4=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=BE=D0=BA=D0=BD=D0=BE=20=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B7=D0=B2=D0=BE=D0=BD=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/CallBack/index.ts | 2 + src/features/CallBack/models/types/types.ts | 5 + .../CallBack/models/validation/validation.ts | 12 ++ .../CallBack/ui/CallBack/CallBack.module.scss | 86 +++++++++++++ .../CallBack/ui/CallBack/CallBack.tsx | 103 ++++++++++++++++ src/shared/ui/Modal/Modal.tsx | 15 ++- src/widgets/Footer/Footer.tsx | 115 +++++++++++------- 7 files changed, 289 insertions(+), 49 deletions(-) create mode 100644 src/features/CallBack/index.ts create mode 100644 src/features/CallBack/models/types/types.ts create mode 100644 src/features/CallBack/models/validation/validation.ts create mode 100644 src/features/CallBack/ui/CallBack/CallBack.module.scss create mode 100644 src/features/CallBack/ui/CallBack/CallBack.tsx diff --git a/src/features/CallBack/index.ts b/src/features/CallBack/index.ts new file mode 100644 index 00000000..9ffed0b8 --- /dev/null +++ b/src/features/CallBack/index.ts @@ -0,0 +1,2 @@ +import { CallBack } from './ui/CallBack/CallBack' +export default CallBack diff --git a/src/features/CallBack/models/types/types.ts b/src/features/CallBack/models/types/types.ts new file mode 100644 index 00000000..f16629ed --- /dev/null +++ b/src/features/CallBack/models/types/types.ts @@ -0,0 +1,5 @@ +export interface CallBackData { + name: string + phoneNumber: string + comment?: string +} diff --git a/src/features/CallBack/models/validation/validation.ts b/src/features/CallBack/models/validation/validation.ts new file mode 100644 index 00000000..e0f63acc --- /dev/null +++ b/src/features/CallBack/models/validation/validation.ts @@ -0,0 +1,12 @@ +import * as Yup from 'yup' + +export const validationSchema = Yup.object().shape({ + name: Yup.string() + .required('Введите имя') + .min(1, 'Минимальная длина имена 1 символ') + .max(32, 'Максимальная длина имени 32 символа'), + phoneNumber: Yup.string() + .required('Введите номер телефона') + .matches(/^\+7 \([0-9]{3}\) [0-9]{3}-[0-9]{2}-[0-9]{2}$/, 'Укажите корректный номер телефона'), + comment: Yup.string().max(255, 'Максимальная длина комментария 255 символов') +}) diff --git a/src/features/CallBack/ui/CallBack/CallBack.module.scss b/src/features/CallBack/ui/CallBack/CallBack.module.scss new file mode 100644 index 00000000..70b25ef9 --- /dev/null +++ b/src/features/CallBack/ui/CallBack/CallBack.module.scss @@ -0,0 +1,86 @@ +@use '../../../../shared/styles/utils/variables' as var; + +.form { + position: relative; + margin: 0 auto; + padding: 30px; + width: 450px; + display: flex; + flex-direction: column; + justify-content: center; + background-color: var.$white; + border-radius: 5px; +} + +.heading { + margin-bottom: 20px; + font-weight: 500; + font-size: 20px; +} + +.cross-button { + width: 15px; + height: 15px; + position: absolute; + top: 20px; + right: 20px; + cursor: pointer; + + path { + fill: var.$black; + transition: fill 300ms; + } + + &:hover { + path { + fill: var.$theme-primary-color; + } + } + + button { + padding: 0; + margin: 0; + border: none; + width: 100%; + height: 100%; + } +} + +.label { + position: relative; + font-size: 14px; +} + +.span { + color: var.$promo-color; + font-weight: 400; +} + +.input, +.textarea { + margin-top: 10px; + margin-bottom: 20px; + padding: 10px 16px; + border-radius: 5px; + background-color: var.$body-bg; + font-size: 16px; +} + +.input:focus, +.textarea:focus { + outline: 2px solid var.$theme-primary-color; +} + +.error { + position: absolute; + top: 65px; + left: 0; + font-size: 12px; + font-weight: 100; + color: var.$promo-color; +} + +.button { + width: 100%; + height: 56px; +} diff --git a/src/features/CallBack/ui/CallBack/CallBack.tsx b/src/features/CallBack/ui/CallBack/CallBack.tsx new file mode 100644 index 00000000..9a190345 --- /dev/null +++ b/src/features/CallBack/ui/CallBack/CallBack.tsx @@ -0,0 +1,103 @@ +import React, { useCallback } from 'react' +import { ErrorMessage, Field, Form, Formik, FormikHelpers } from 'formik' +import IconClose from '@/assets/icons/IconClose.svg' +import { Input } from '@/shared/ui/Input/Input' +import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button/Button' +import { Textarea } from '@/shared/ui/Textarea/Textarea' +import Heading from '@/shared/ui/Heading/Heading' +import Paragraph, { ParagraphTheme } from '@/shared/ui/Paragraph/Paragraph' +import { validationSchema } from '../../models/validation/validation' +import { CallBackData } from '../../models/types/types' +import styles from './CallBack.module.scss' + +interface CallBackProps { + setIsModalClosing: React.Dispatch> +} + +/** + * Форма обратного звонка + * Используется как children в компоненте модального окна. + * После заполнения формы отправляет данные в CRM. + * Для создания формы используется Formik, для валидации - Yup. + */ +export const CallBack: React.FC = ({ setIsModalClosing }) => { + const initialValues: CallBackData = { + name: '', + phoneNumber: '', + comment: '' + } + + const handleClose = useCallback(() => { + setIsModalClosing(true) + }, []) + + const handleSubmit = async (values: CallBackData, helpers: FormikHelpers) => { + setTimeout(() => { + helpers.resetForm() + }, 1000) + } + + return ( + + {({ isValid, dirty, isSubmitting }) => ( +
+
+ +
+ Заказать обратный звонок + + + + + + + +
+ )} +
+ ) +} diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index 8b2c3d8f..25d57739 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -1,4 +1,4 @@ -import React, { HTMLAttributes, useCallback, useEffect, useRef, useState } from 'react' +import React, { HTMLAttributes, useCallback, useEffect, useRef } from 'react' import classNames from 'classnames' import { createFocusTrap } from 'focus-trap' import { createPortal } from 'react-dom' @@ -6,6 +6,8 @@ import styles from './Modal.module.scss' interface IModalProps extends HTMLAttributes { isModalOpen: boolean + isModalClosing: boolean + setIsModalClosing: React.Dispatch> onClose: VoidFunction className?: string | undefined } @@ -19,9 +21,15 @@ interface IModalProps extends HTMLAttributes { * @param {function} onClose - handler function to close the modal. * @param {string} className - styles passed from the parent component. */ -export default function Modal({ isModalOpen, onClose, className, children }: IModalProps) { +export default function Modal({ + isModalOpen, + isModalClosing, + setIsModalClosing, + onClose, + className, + children +}: IModalProps) { const modalRef = useRef(null) - const [isModalClosing, setIsModalClosing] = useState(false) const handleClose = useCallback(() => { setIsModalClosing(true) @@ -91,6 +99,7 @@ export default function Modal({ isModalOpen, onClose, className, children }: IMo if (isModalClosing) { closeTimeout = setTimeout(() => { closeModal() + setIsModalClosing(false) }, 300) } diff --git a/src/widgets/Footer/Footer.tsx b/src/widgets/Footer/Footer.tsx index 57575fe8..243944b5 100644 --- a/src/widgets/Footer/Footer.tsx +++ b/src/widgets/Footer/Footer.tsx @@ -1,62 +1,85 @@ +import { useState } from 'react' import { coreBaseData } from '@/mockData/coreBaseData' import Logo from '@/shared/ui/logo/Logo' import Link from '@/shared/ui/Link/Link' +import { Button } from '@/shared/ui/Button/Button' +import Modal from '@/shared/ui/Modal/Modal' +import Payments from '@/entities/Payments/Payments' import SubscribeForm from '@/features/SubscribeForm/SubscribeForm' import styles from './footer.module.scss' -import Payments from '@/entities/Payments/Payments' -import { Button } from '@/shared/ui/Button/Button' +import CallBack from '@/features/CallBack' function Footer() { + const [isModalOpen, setIsModalOpen] = useState(false) + const [isModalClosing, setIsModalClosing] = useState(false) + + const changeModalState = () => { + setIsModalOpen(!isModalOpen) + } const onSubmitHandler = () => {} return ( -
-
-
-
- -

{coreBaseData.footer.company_info}

-
-
- -
-
-

Поддержка

-
-
    -
  • - - {coreBaseData.footer.support.phone_number} - -
  • -
  • - -
  • -
-

{coreBaseData.footer.support_work_time}

+ <> + {isModalOpen && ( + + + + )} +
+
+
+
+ +

{coreBaseData.footer.company_info}

+
+
+ +
+
+

Поддержка

+
+
    +
  • + + {coreBaseData.footer.support.phone_number} + +
  • +
  • + +
  • +
+

{coreBaseData.footer.support_work_time}

+
-
-
-
-

- Created by{' '} - - maxboom.ru - -

- +
+
+

+ Created by{' '} + + maxboom.ru + +

+ +
-
-
+
+ ) } From ab864eedd61ea7fff81bb4b81d52cecb8598debe Mon Sep 17 00:00:00 2001 From: Artur Khelshtein Date: Thu, 25 Jan 2024 00:19:27 +0300 Subject: [PATCH 10/19] =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=B2=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=BE=D0=B3=D0=B8=D1=87?= =?UTF-8?q?=D0=BD=D1=83=D1=8E=20=D1=84=D0=BE=D1=80=D0=BC=D1=83=20=D0=BA=20?= =?UTF-8?q?=D0=B5=D0=B4=D0=B8=D0=BD=D0=BE=D0=BE=D0=B1=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuickPurchaseForm.module.scss | 24 ++++++++++++++----- .../QuickPurchaseForm/QuickPurchaseForm.tsx | 4 ++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.module.scss b/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.module.scss index c28eb409..9e45bf9d 100644 --- a/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.module.scss +++ b/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.module.scss @@ -7,13 +7,20 @@ gap: 16px; margin: 0 auto; background-color: var.$white; - padding: 32px 24px; + padding: 30px; width: 420px; - border-radius: 10px; + border-radius: 5px; +} + +.heading { + margin-bottom: 20px; + font-weight: 500; + font-size: 20px; } .label { position: relative; + font-size: 14px; } .span { @@ -23,20 +30,25 @@ .input, .textarea { - margin: 12px 0 24px; + margin: 10px 0 20px; padding: 12px; border-radius: 10px; background-color: var.$body-bg; - border: 1px solid var.$border-color; + font-size: 16px; } .input:focus, .textarea:focus { - border: 1px solid var.$theme-primary-color; + outline: 2px solid var.$theme-primary-color; } .error { position: absolute; - top: 80px; + top: 65px; left: 0; } + +.button { + width: 100%; + height: 56px +} diff --git a/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.tsx b/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.tsx index f94a9800..ddc49d8b 100644 --- a/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.tsx +++ b/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.tsx @@ -35,7 +35,7 @@ export const QuickPurchaseForm: React.FC = () => { validateOnBlur={true}> {({ isValid, dirty, isSubmitting }) => (
- Быстрый заказ + Быстрый заказ