diff --git a/config/storybook/decorators/StoreDecorator.js b/config/storybook/decorators/StoreDecorator.js index 3094a499..cb6e3cb8 100644 --- a/config/storybook/decorators/StoreDecorator.js +++ b/config/storybook/decorators/StoreDecorator.js @@ -1,4 +1,4 @@ -import {StoreProvider} from "../../../src/app/providers/SroreProvider/index.ts"; +import {StoreProvider} from "../../../src/app/providers/StoreProvider/index.ts"; export const StoreDecorator = (initialState) => (Story) => ( diff --git a/src/app/providers/SroreProvider/config/StateSchema.ts b/src/app/providers/StoreProvider/config/StateSchema.ts similarity index 69% rename from src/app/providers/SroreProvider/config/StateSchema.ts rename to src/app/providers/StoreProvider/config/StateSchema.ts index 7f7cb585..68f23cdb 100644 --- a/src/app/providers/SroreProvider/config/StateSchema.ts +++ b/src/app/providers/StoreProvider/config/StateSchema.ts @@ -1,12 +1,16 @@ import { CategorySchema } from '@/entities/Category/types/types' +import { SearchResultSchema } from '@/features/SearchProduct/types/types' import { LoginSchema } from '@/features/login/model/types/types' import { BrandSchema } from '@/widgets/BrandBlock/types/types' import { ApiInstance } from '@/shared/api/api' +import { StoreReviewsSchema } from '@/widgets/ReviewsBlock/model/types/types' export interface StateSchema { login: LoginSchema + storeReviews: StoreReviewsSchema category: CategorySchema brand: BrandSchema + searchResult: SearchResultSchema } export interface ThunkExtraArg { diff --git a/src/app/providers/SroreProvider/config/store.ts b/src/app/providers/StoreProvider/config/store.ts similarity index 69% rename from src/app/providers/SroreProvider/config/store.ts rename to src/app/providers/StoreProvider/config/store.ts index c0273043..8219c22a 100644 --- a/src/app/providers/SroreProvider/config/store.ts +++ b/src/app/providers/StoreProvider/config/store.ts @@ -4,16 +4,20 @@ import { StateSchema, ThunkExtraArg } from './StateSchema' import { $api } from '@/shared/api/api' import categorySlice from '@/entities/Category/slice/categorySlice' import brandSlice from '@/widgets/BrandBlock/slice/brandSlice' +import searchProductSlice from '@/features/SearchProduct/slice/searchProductSlice' +import { storeReviewsReducer } from '@/widgets/ReviewsBlock/model/slice/reviewsSlice' export type RootState = StateSchema -const rootReducer: ReducersMapObject = { +const rootReducer: ReducersMapObject = { login: loginReducer, category: categorySlice, - brand: brandSlice + brand: brandSlice, + searchResult: searchProductSlice, + storeReviews: storeReviewsReducer } -export function createReduxStore(initialState: StateSchema) { +export function createReduxStore(initialState: RootState) { const extraArg: ThunkExtraArg = { api: $api } diff --git a/src/app/providers/SroreProvider/index.ts b/src/app/providers/StoreProvider/index.ts similarity index 100% rename from src/app/providers/SroreProvider/index.ts rename to src/app/providers/StoreProvider/index.ts diff --git a/src/app/providers/SroreProvider/ui/StoreProvider.tsx b/src/app/providers/StoreProvider/ui/StoreProvider.tsx similarity index 100% rename from src/app/providers/SroreProvider/ui/StoreProvider.tsx rename to src/app/providers/StoreProvider/ui/StoreProvider.tsx diff --git a/src/app/router/AppRouter/ui/AppRouter.tsx b/src/app/router/AppRouter/ui/AppRouter.tsx index 416bf6a7..28420af7 100644 --- a/src/app/router/AppRouter/ui/AppRouter.tsx +++ b/src/app/router/AppRouter/ui/AppRouter.tsx @@ -9,6 +9,7 @@ import LoginPage from '@/pages/LoginPage/LoginPage' import ComparePage from '@/pages/ComparePage/ComparePage' import FavoritesPage from '@/pages/FavoritesPage/FavoritesPage' import CartPage from '@/pages/CartPage/CartPage' +import SearchResultsPage from '@/pages/SearchResultsPage/SearchResultsPage' export const AppRouter = createBrowserRouter([ { @@ -48,6 +49,10 @@ export const AppRouter = createBrowserRouter([ { path: Routes.CART, element: + }, + { + path: Routes.SEARCH, + element: } ] } diff --git a/src/components/CardPreview/CardPreview.tsx b/src/components/CardPreview/CardPreview.tsx index 90c5f8b8..0a43987c 100644 --- a/src/components/CardPreview/CardPreview.tsx +++ b/src/components/CardPreview/CardPreview.tsx @@ -17,6 +17,7 @@ export const CardPreview: FC = () => { const [isLiked, setIsLiked] = useState(false) const [isInCompared, setIsInCompared] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false) + const [isModalClosing, setIsModalClosing] = useState(false) const handleAddToCart = () => { setIsInCart(!isInCart) @@ -45,9 +46,13 @@ export const CardPreview: FC = () => { return ( <> {isModalOpen && ( - + }> - + )} diff --git a/src/components/HeaderAccount/HeaderAccount.tsx b/src/components/HeaderAccount/HeaderAccount.tsx index 2036faa0..d450c9f7 100644 --- a/src/components/HeaderAccount/HeaderAccount.tsx +++ b/src/components/HeaderAccount/HeaderAccount.tsx @@ -28,6 +28,7 @@ const LazyLoginForm = lazy(() => import('@/features/login/index')) */ const HeaderAccount: FC = ({ counter, total }) => { const [isModalOpen, setIsModalOpen] = useState(false) + const [isModalClosing, setIsModalClosing] = useState(false) const dispatch = useAppDispatch() const isAuth = useSelector(getUserAuthStatus) @@ -54,7 +55,11 @@ const HeaderAccount: FC = ({ counter, total }) => { return ( <> {isModalOpen && ( - + }> diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx index 5518edd5..6fcef70a 100644 --- a/src/components/header/header.tsx +++ b/src/components/header/header.tsx @@ -17,7 +17,7 @@ import { linkItems } from '@/mockData/catalogListData' import styles from './header.module.scss' import { useDispatch, useSelector } from 'react-redux' import { fetchCategories } from '@/entities/Category/slice/categorySlice' -import { AppDispatch } from '@/app/providers/SroreProvider/config/store' +import { AppDispatch } from '@/app/providers/StoreProvider/config/store' import { selectCategories, selectDisplayedCategories } from '@/entities/Category/selectors/categorySelectors' import CatalogNodeItem from '@/widgets/CatalogNodeItem/CatalogNodeItem' import NavigationLink from '@/widgets/NavigationLink/NavigationLink' diff --git a/src/entities/BlogCard/BlogCard.module.scss b/src/entities/BlogCard/BlogCard.module.scss new file mode 100644 index 00000000..5ee6b802 --- /dev/null +++ b/src/entities/BlogCard/BlogCard.module.scss @@ -0,0 +1,36 @@ +@use '@/app/styles/index' as var; + +.card { + min-width: 340px; + position: relative; + transition: transform 0.3s ease-in-out; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 15px; + + &:hover { + transform: scale(1.1, 1.05); + transition: transform 0.3s ease-in-out; + } + + img { + border-radius: 6px; + transition: transform 0.3s ease-in-out; + scroll-snap-align: start; + } + + .heading + { + font-size: #{'min(max(14px, 1.2vw), 16px)'}; + } + + &:hover .heading { + color: var.$link-color; + } + + span { + color: var.$body-color-light-grey; + } + +} \ No newline at end of file diff --git a/src/entities/BlogCard/BlogCard.stories.tsx b/src/entities/BlogCard/BlogCard.stories.tsx new file mode 100644 index 00000000..52f4421e --- /dev/null +++ b/src/entities/BlogCard/BlogCard.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' +import BlogCard from './BlogCard' +import Img1 from '@/assets/images/blog/img-blog-01.png' + +const meta = { + title: 'entities/BlogCard', + component: BlogCard, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + card: { + id: 1, + src: Img1, + alt: 'Покупай и не жди. До -50% на весь электротранспорт!', + title: 'Покупай и не жди. До -50% на весь электротранспорт!', + date: '8 Мая, 2022' + } + } +} diff --git a/src/entities/BlogCard/BlogCard.tsx b/src/entities/BlogCard/BlogCard.tsx new file mode 100644 index 00000000..f4f64202 --- /dev/null +++ b/src/entities/BlogCard/BlogCard.tsx @@ -0,0 +1,28 @@ +import { FC } from 'react' +import { TCard } from '@/models/CardModel' +import styles from './BlogCard.module.scss' +import Link from '@/shared/ui/Link/Link' +import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' + +export type Props = { + card: TCard +} + +/** + * Карточка из блока блог + * @param {TCard} card - параметры карточки из блога + */ + +const BlogCard: FC = ({ card }) => { + return ( + + {card.alt} + + {card.title} + + {card.date} + + ) +} + +export default BlogCard diff --git a/src/entities/BrandCard/BrandCard.module.scss b/src/entities/BrandCard/BrandCard.module.scss index 677de855..d7137d83 100644 --- a/src/entities/BrandCard/BrandCard.module.scss +++ b/src/entities/BrandCard/BrandCard.module.scss @@ -49,9 +49,5 @@ .img { display: block; - - @include media.respond-to('small') { - width: 120px; - height: 120px; - } + max-width: 100%; } diff --git a/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx b/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx index c45fa99b..1289f31a 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: '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 b77adfb1..1181d05a 100644 --- a/src/entities/CardReview/ui/CardReview/CardReview.tsx +++ b/src/entities/CardReview/ui/CardReview/CardReview.tsx @@ -1,32 +1,52 @@ import { FC, useMemo } from 'react' -import { TReview } from '@/models/ReviewModel' 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 + score: number + name: string } -const CardReview: FC = props => { - const { review } = props +/** + * Отзыв + * @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 initials = useMemo(() => { - return review.name.slice(0, 1) + return name.slice(0, 1) }, [0, 1]) const linkTextStyle = styles.link__text + const newDate = useMemo(() => { + const _parsedDate = new Date(date) + const year = _parsedDate.getFullYear() + const formatter = new Intl.DateTimeFormat('ru', { month: 'long', day: 'numeric' }).format(_parsedDate) + + return `${formatter}, ${year}` + }, [date]) + return (
- {review.id === 0 ? ( + {pk === 0 ? ( <> - {review.name} - {review.score} + {name} - {score} -

{review.text}

+

{text}

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

{initials}
- {review.name} + {name} - Оценил(а) магазин на {review.score} - + Оценил(а) магазин на {score} +
- {review.text} - {review.date} + {text} + {newDate}
Читать полный отзыв diff --git a/src/entities/Category/selectors/categorySelectors.ts b/src/entities/Category/selectors/categorySelectors.ts index 5f33d316..05535e0b 100644 --- a/src/entities/Category/selectors/categorySelectors.ts +++ b/src/entities/Category/selectors/categorySelectors.ts @@ -1,4 +1,4 @@ -import { RootState } from '@/app/providers/SroreProvider/config/store' +import { RootState } from '@/app/providers/StoreProvider/config/store' export const selectCategories = (state: RootState) => state.category.categories export const selectDisplayedCategories = (state: RootState) => state.category.displayedCategories diff --git a/src/entities/Category/slice/categorySlice.ts b/src/entities/Category/slice/categorySlice.ts index d25d5dc3..cc77499a 100644 --- a/src/entities/Category/slice/categorySlice.ts +++ b/src/entities/Category/slice/categorySlice.ts @@ -1,6 +1,6 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types' -import { ThunkConfig } from '@/app/providers/SroreProvider/config/StateSchema' +import { ThunkConfig } from '@/app/providers/StoreProvider/config/StateSchema' import { Category, CategorySchema } from '../types/types' import { apiErrorIdentify } from '@/shared/api/apiErrorIdentify' import { rejectedPayloadHandle } from '@/shared/api/rejectedPayloadHandle' diff --git a/src/entities/NewsCard/NewsCard.module.scss b/src/entities/NewsCard/NewsCard.module.scss new file mode 100644 index 00000000..84722723 --- /dev/null +++ b/src/entities/NewsCard/NewsCard.module.scss @@ -0,0 +1,49 @@ +@use '@/app/styles/index' as var; + +.card { + min-width: 340px; + position: relative; + transition: transform 0.3s ease-in-out; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 15px; + + &:hover { + transform: scale(1.1, 1.05); + transition: transform 0.3s ease-in-out; + } + + img { + border-radius: 6px; + transition: transform 0.3s ease-in-out; + scroll-snap-align: start; + } + + .heading + { + font-size: #{'min(max(14px, 1.2vw), 16px)'}; + } + + &:hover .heading { + color: var.$link-color; + } + + span { + color: var.$body-color-light-grey; + } + + .promo { + display: inline-block; + position: absolute; + top: 15px; + left: 15px; + background: var.$promo-color; + border-radius: 5px; + padding: 5px 10px; + color: var.$white; + font-size: 15px; + line-height: 120%; + font-weight: 500; + } +} \ No newline at end of file diff --git a/src/entities/NewsCard/NewsCard.stories.tsx b/src/entities/NewsCard/NewsCard.stories.tsx new file mode 100644 index 00000000..5b63741a --- /dev/null +++ b/src/entities/NewsCard/NewsCard.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react' +import NewsCard from './NewsCard' +import Img1 from '@/assets/images/news/img-news-01.png' + +const meta = { + title: 'entities/NewsCard', + component: NewsCard, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + card: { + id: 1, + src: Img1, + alt: 'Покупай и не жди. До -50% на весь электротранспорт!', + title: 'Покупай и не жди. До -50% на весь электротранспорт!', + date: '15 Мая, 2022', + promo: true + } + } +} diff --git a/src/entities/NewsCard/NewsCard.tsx b/src/entities/NewsCard/NewsCard.tsx new file mode 100644 index 00000000..006c6674 --- /dev/null +++ b/src/entities/NewsCard/NewsCard.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react' +import { TCard } from '@/models/CardModel' +import { TEXT_PROMO } from '@/shared/constants/constants' +import styles from './NewsCard.module.scss' +import Link from '@/shared/ui/Link/Link' +import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' + +export type Props = { + card: TCard +} + +/** + * Карточка из блока группы новостей + * @param {TCard} card - параметры карточки из группы новостей + */ + +const NewsCard: FC = ({ card }) => { + return ( + + {card.alt} + + {card.title} + + {card.date} + {card.promo ? {TEXT_PROMO} : null} + + ) +} + +export default NewsCard 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/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..0af86877 --- /dev/null +++ b/src/features/CallBack/ui/CallBack/CallBack.module.scss @@ -0,0 +1,69 @@ +@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; + padding: 0; + border: none; + cursor: pointer; + + path { + width: 100%; + height: 100%; + fill: var.$black; + transition: fill 300ms; + } + + &:hover { + path { + fill: var.$theme-primary-color; + } + } +} + +.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; + bottom: 0; + left: 0; +} + +.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..053d0a5b --- /dev/null +++ b/src/features/CallBack/ui/CallBack/CallBack.tsx @@ -0,0 +1,112 @@ +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 Label from '@/shared/ui/Label/Label' +import Span from '@/shared/ui/Span/Span' +import { validationSchema } from '@/features/CallBack/models/validation/validation' +import { CallBackData } from '@/features/CallBack/models/types/types' +import styles from './CallBack.module.scss' + +interface CallBackProps { + setIsModalClosing: React.Dispatch> +} + +/** + * Форма обратного звонка + * Используется как children в компоненте модального окна. + * После заполнения формы отправляет данные в CRM. + * Для создания формы используется Formik, для валидации - Yup. + * setIsModalClosing - функция установки булевого значения, для обозначения состояние процесса закрытия модального окна + */ +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/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/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.module.scss b/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.module.scss index c28eb409..2181e2c0 100644 --- a/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.module.scss +++ b/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.module.scss @@ -1,42 +1,69 @@ @use '../../../../shared/styles/utils/variables' as var; .form { + position: relative; display: flex; flex-direction: column; justify-content: center; gap: 16px; margin: 0 auto; background-color: var.$white; - padding: 32px 24px; + padding: 30px; width: 420px; - border-radius: 10px; + border-radius: 5px; } -.label { - position: relative; +.heading { + margin-bottom: 20px; + font-weight: 500; + font-size: 20px; } -.span { - color: var.$promo-color; - font-weight: 100; +.cross-button { + width: 15px; + height: 15px; + position: absolute; + top: 20px; + right: 20px; + padding: 0; + border: none; + cursor: pointer; + + path { + width: 100%; + height: 100%; + fill: var.$black; + transition: fill 300ms; + } + + &:hover { + path { + fill: var.$theme-primary-color; + } + } } .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..dace388c 100644 --- a/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.tsx +++ b/src/features/QuickPurchase/ui/QuickPurchaseForm/QuickPurchaseForm.tsx @@ -1,26 +1,38 @@ +import React, { useCallback } from 'react' import { Formik, Field, Form, ErrorMessage, 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 '../../model/validation/validation' -import { IFormValues } from '../../model/types/types' +import Label from '@/shared/ui/Label/Label' +import Span from '@/shared/ui/Span/Span' +import { validationSchema } from '@/features/QuickPurchase/model/validation/validation' +import { IFormValues } from '@/features/QuickPurchase/model/types/types' import styles from './QuickPurchaseForm.module.scss' +interface QuickPurchaseProps { + setIsModalClosing: React.Dispatch> +} + /** * Форма быстрого оформления заказа. * Используется как children в компоненте модального окна. * После заполнения формы отправляет данные в CRM. * Для создания формы используется Formik, для валидации - Yup. */ -export const QuickPurchaseForm: React.FC = () => { +export const QuickPurchaseForm: React.FC = ({ setIsModalClosing }) => { const initialValues: IFormValues = { name: '', phoneNumber: '', comment: '' } + const handleClose = useCallback(() => { + setIsModalClosing(true) + }, []) + const handleSubmit = (values: IFormValues, helpers: FormikHelpers) => { setTimeout(() => { helpers.resetForm() @@ -35,10 +47,13 @@ export const QuickPurchaseForm: React.FC = () => { validateOnBlur={true}> {({ isValid, dirty, isSubmitting }) => (
- Быстрый заказ - - - {children} , diff --git a/src/shared/ui/Scroll/Scroll.module.scss b/src/shared/ui/Scroll/Scroll.module.scss new file mode 100644 index 00000000..9847bb0d --- /dev/null +++ b/src/shared/ui/Scroll/Scroll.module.scss @@ -0,0 +1,33 @@ +@use '@/app/styles/index' as var; + +.extra { + background-color: orange; + min-width: 200px; +} + +.additional { + width: 1000px; + height: 100px; +} + +.scroll { + display: flex; + gap: 20px; + padding: 0 10px 20px; + overflow: auto hidden; + cursor: grab; + + &::-webkit-scrollbar { + height: 3px; + } + + &::-webkit-scrollbar-thumb { + background: var.$theme-primary-color; + cursor: grab; + } + + &::-webkit-scrollbar-track { + margin-left: 10px; + margin-right: 10px; + } + } \ No newline at end of file diff --git a/src/shared/ui/Scroll/Scroll.stories.tsx b/src/shared/ui/Scroll/Scroll.stories.tsx new file mode 100644 index 00000000..0c28653e --- /dev/null +++ b/src/shared/ui/Scroll/Scroll.stories.tsx @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { FC } from 'react' +import Scroll from './Scroll' +import styles from './Scroll.module.scss' + +const StorybookWrapper: FC = () => { + return ( + +
+
+
+
+
+
+ ) +} + +const meta = { + title: 'shared/Scroll', + component: StorybookWrapper, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + children:
+ } +} diff --git a/src/shared/ui/Scroll/Scroll.tsx b/src/shared/ui/Scroll/Scroll.tsx new file mode 100644 index 00000000..eb0114a6 --- /dev/null +++ b/src/shared/ui/Scroll/Scroll.tsx @@ -0,0 +1,18 @@ +import React, { FC } from 'react' +import styles from './Scroll.module.scss' + +export type TProps = { + className?: string + children: React.ReactNode +} + +/** + * Scrollbar + * @param {string} className - для дополнительных свойств + * @param {React.ReactNode} children - элементы внутри компонента + */ +const Scroll: FC = ({ className, children }) => { + return
{children}
+} + +export default Scroll diff --git a/src/shared/ui/Span/Span.module.scss b/src/shared/ui/Span/Span.module.scss new file mode 100644 index 00000000..346ac699 --- /dev/null +++ b/src/shared/ui/Span/Span.module.scss @@ -0,0 +1,16 @@ +@use '../../styles/utils/variables' as var; + +.span { + color: var.$promo-color; + font-size: 15px; + font-weight: 400; + line-height: 28px; + + // theme + &.error { + font-size: 12px; + line-height: 16px; + font-weight: 100; + color: var.$promo-color; + } +} diff --git a/src/shared/ui/Span/Span.stories.tsx b/src/shared/ui/Span/Span.stories.tsx new file mode 100644 index 00000000..25a74a06 --- /dev/null +++ b/src/shared/ui/Span/Span.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react' +import Span from './Span' + +const meta = { + title: 'shared/Span', + component: Span, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + children: '*' + } +} diff --git a/src/shared/ui/Span/Span.tsx b/src/shared/ui/Span/Span.tsx new file mode 100644 index 00000000..ec9fc49c --- /dev/null +++ b/src/shared/ui/Span/Span.tsx @@ -0,0 +1,19 @@ +import React, { FC } from 'react' +import styles from './Span.module.scss' + +type TSpanProps = React.HTMLAttributes & { + className?: string +} + +/** + * @param {string} className - для передачи дополнительных параметров стиля + */ +const Span: FC = ({ children, className, ...props }) => { + return ( + + {children} + + ) +} + +export default Span diff --git a/src/widgets/BlogBlock/ui/BlogBlock.module.scss b/src/widgets/BlogBlock/ui/BlogBlock.module.scss new file mode 100644 index 00000000..3409d113 --- /dev/null +++ b/src/widgets/BlogBlock/ui/BlogBlock.module.scss @@ -0,0 +1,65 @@ +@use '@/app/styles/index' as var; + +.storybook { + width: 1080px; +} + +.wrapper { + width: 100%; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 18px; + + h2 { + font-size: #{'min(max(18px, 1.6vw), 20px)'}; + line-height: 115%; + font-weight: 500; + } + + article { + display: flex; + justify-content: space-between; + align-items: flex-end; + padding: 0 10px; + } + + .link { + font-size: 15px; + line-height: 120%; + font-weight: 500; + + &:hover { + opacity: 0.7; + } + } + + .svg { + width: 1.25rem; + height: 0.5625rem; + margin-left: 0.5rem; + vertical-align: middle; + } + + ul { + display: flex; + gap: 20px; + padding: 0 10px 20px; + overflow: auto hidden; + cursor: grab; + + &::-webkit-scrollbar { + height: 3px; + } + + &::-webkit-scrollbar-thumb { + background: var.$theme-primary-color; + cursor: grab; + } + + &::-webkit-scrollbar-track { + margin-left: 10px; + margin-right: 10px; + } + } +} \ No newline at end of file diff --git a/src/widgets/BlogBlock/ui/BlogBlock.stories.tsx b/src/widgets/BlogBlock/ui/BlogBlock.stories.tsx new file mode 100644 index 00000000..1c9ee805 --- /dev/null +++ b/src/widgets/BlogBlock/ui/BlogBlock.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react' +import styles from './BlogBlock.module.scss' +import { FC } from 'react' +import BlogBlock from './BlogBlock' + +const StorybookWrapper: FC = () => { + return ( +
+ +
+ ) +} + +const meta = { + title: 'widgets/BlogBlock', + component: StorybookWrapper, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/src/widgets/BlogBlock/ui/BlogBlock.tsx b/src/widgets/BlogBlock/ui/BlogBlock.tsx new file mode 100644 index 00000000..a7188d5e --- /dev/null +++ b/src/widgets/BlogBlock/ui/BlogBlock.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react' +import IconLink from '@/assets/icons/IconLink' +import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' +import Link from '@/shared/ui/Link/Link' +import styles from './BlogBlock.module.scss' +import Scroll from '@/shared/ui/Scroll/Scroll' +import { blogData } from '@/mockData/blogData' +import BlogCard from '@/entities/BlogCard/BlogCard' + +/** + * Блок группы карточек блога + */ + +const BlogBlock: FC = () => { + return ( +
+
+ Блог + + Показать все + + +
+ + {blogData.map(item => ( + + ))} + +
+ ) +} + +export default BlogBlock diff --git a/src/widgets/BrandBlock/selectors/selectors.ts b/src/widgets/BrandBlock/selectors/selectors.ts index 84442082..4529e743 100644 --- a/src/widgets/BrandBlock/selectors/selectors.ts +++ b/src/widgets/BrandBlock/selectors/selectors.ts @@ -1,3 +1,3 @@ -import { StateSchema } from '@/app/providers/SroreProvider' +import { StateSchema } from '@/app/providers/StoreProvider' export const brandSelector = (state: StateSchema) => state.brand.brands diff --git a/src/widgets/BrandBlock/slice/brandSlice.ts b/src/widgets/BrandBlock/slice/brandSlice.ts index c5b876b0..df80ce9c 100644 --- a/src/widgets/BrandBlock/slice/brandSlice.ts +++ b/src/widgets/BrandBlock/slice/brandSlice.ts @@ -1,6 +1,6 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types' -import { ThunkConfig } from '@/app/providers/SroreProvider/config/StateSchema' +import { ThunkConfig } from '@/app/providers/StoreProvider/config/StateSchema' import { Brand, BrandSchema } from '../types/types' import { apiErrorIdentify } from '@/shared/api/apiErrorIdentify' import { rejectedPayloadHandle } from '@/shared/api/rejectedPayloadHandle' diff --git a/src/widgets/BrandBlock/ui/BrandBlock/BrandBlock.tsx b/src/widgets/BrandBlock/ui/BrandBlock/BrandBlock.tsx index 65aba522..7e5aa95f 100644 --- a/src/widgets/BrandBlock/ui/BrandBlock/BrandBlock.tsx +++ b/src/widgets/BrandBlock/ui/BrandBlock/BrandBlock.tsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux' import { Routes } from '@/shared/config/routerConfig/routes' import { brandSelector } from '../../selectors/selectors' import { fetchBrands } from '../../slice/brandSlice' -import { AppDispatch } from '@/app/providers/SroreProvider/config/store' +import { AppDispatch } from '@/app/providers/StoreProvider/config/store' import IconLink from '@/assets/icons/IconLink' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' import Link from '@/shared/ui/Link/Link' diff --git a/src/widgets/Footer/Footer.tsx b/src/widgets/Footer/Footer.tsx index 57575fe8..c1be99dc 100644 --- a/src/widgets/Footer/Footer.tsx +++ b/src/widgets/Footer/Footer.tsx @@ -1,62 +1,88 @@ +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 CallBack from '@/features/CallBack' import styles from './footer.module.scss' -import Payments from '@/entities/Payments/Payments' -import { Button } from '@/shared/ui/Button/Button' +import Paragraph from '@/shared/ui/Paragraph/Paragraph' 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 + + + +
-
-
+
+ ) } diff --git a/src/widgets/NewsBlock/ui/NewsBlock.module.scss b/src/widgets/NewsBlock/ui/NewsBlock.module.scss new file mode 100644 index 00000000..3409d113 --- /dev/null +++ b/src/widgets/NewsBlock/ui/NewsBlock.module.scss @@ -0,0 +1,65 @@ +@use '@/app/styles/index' as var; + +.storybook { + width: 1080px; +} + +.wrapper { + width: 100%; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 18px; + + h2 { + font-size: #{'min(max(18px, 1.6vw), 20px)'}; + line-height: 115%; + font-weight: 500; + } + + article { + display: flex; + justify-content: space-between; + align-items: flex-end; + padding: 0 10px; + } + + .link { + font-size: 15px; + line-height: 120%; + font-weight: 500; + + &:hover { + opacity: 0.7; + } + } + + .svg { + width: 1.25rem; + height: 0.5625rem; + margin-left: 0.5rem; + vertical-align: middle; + } + + ul { + display: flex; + gap: 20px; + padding: 0 10px 20px; + overflow: auto hidden; + cursor: grab; + + &::-webkit-scrollbar { + height: 3px; + } + + &::-webkit-scrollbar-thumb { + background: var.$theme-primary-color; + cursor: grab; + } + + &::-webkit-scrollbar-track { + margin-left: 10px; + margin-right: 10px; + } + } +} \ No newline at end of file diff --git a/src/widgets/NewsBlock/ui/NewsBlock.stories.tsx b/src/widgets/NewsBlock/ui/NewsBlock.stories.tsx new file mode 100644 index 00000000..3b80068a --- /dev/null +++ b/src/widgets/NewsBlock/ui/NewsBlock.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react' +import styles from './NewsBlock.module.scss' +import { FC } from 'react' +import NewsBlock from './NewsBlock' + +const StorybookWrapper: FC = () => { + return ( +
+ +
+ ) +} + +const meta = { + title: 'widgets/NewsBlock', + component: StorybookWrapper, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/src/widgets/NewsBlock/ui/NewsBlock.tsx b/src/widgets/NewsBlock/ui/NewsBlock.tsx new file mode 100644 index 00000000..521490da --- /dev/null +++ b/src/widgets/NewsBlock/ui/NewsBlock.tsx @@ -0,0 +1,32 @@ +import { FC } from 'react' +import IconLink from '@/assets/icons/IconLink' +import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' +import Link from '@/shared/ui/Link/Link' +import styles from './NewsBlock.module.scss' +import NewsCard from '@/entities/NewsCard/NewsCard' +import { newsData } from '@/mockData/newsData' +import Scroll from '@/shared/ui/Scroll/Scroll' + +/** + * Блок группы новостей + */ +const NewsBlock: FC = () => { + return ( +
+
+ Новости + + Все новости + + +
+ + {newsData.map(item => ( + + ))} + +
+ ) +} + +export default NewsBlock diff --git a/src/widgets/ReviewsBlock/model/selectors/selectors.ts b/src/widgets/ReviewsBlock/model/selectors/selectors.ts new file mode 100644 index 00000000..a851295d --- /dev/null +++ b/src/widgets/ReviewsBlock/model/selectors/selectors.ts @@ -0,0 +1,5 @@ +import { StateSchema } from '@/app/providers/StoreProvider' + +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..798312fc --- /dev/null +++ b/src/widgets/ReviewsBlock/model/services/getStoreReviews.ts @@ -0,0 +1,21 @@ +import { createAsyncThunk } from '@reduxjs/toolkit' +import { ThunkConfig } from '@/app/providers/StoreProvider/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) + + 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..da46ff64 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,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 fa75e9ce..629ed3e5 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 { 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' 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' 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 => ( - + ))}