+
+export const Default: Story = {
+ args: {
+ children: (
+
+ Диск 3M Trizact Foam Disc P5000, 150 ммКомплект по фото. Забавно, но в сети запрос примеры текстов
+ копирайтеров почти неразлучен с хвостами типа лучшие.Топ-20 лучших продающих статей, 50 лучших текстов
+ для лендинга и т.д. Посыл прост: если вы копирайтеры, делайте так же, и Welcome To Top. А если
+ заказчики, то теперь вам ясно, что такое успешный копирайтинг. И никаких проблем? Как бы не так.
+ Проблема в том, что познание хорошего не работает без познания плохого. Слепое равнение на топов
+ гарантировано загонит копирайтера-новичка в творческий тупик. А клиента превратит в маньяка, не
+ способного объяснить, что ему не нравится. Конечно, контент контенту рознь. К примеру, не надо быть
+ копирайтером, дабы понять, что с этим текстом что-то не так:
+
+ )
+ }
+}
diff --git a/src/features/RollUp/RollUp.tsx b/src/features/RollUp/RollUp.tsx
new file mode 100644
index 00000000..404ef00b
--- /dev/null
+++ b/src/features/RollUp/RollUp.tsx
@@ -0,0 +1,43 @@
+import { useState, type FC, PropsWithChildren } from 'react'
+
+import IconArrowDown from '@/assets/icons/IconArrowDown.svg'
+import { Button, ButtonDesign, ButtonSize } from '@/shared/ui/Button/Button'
+import Heading, { HeadingType } from '@/shared/ui/Heading/Heading'
+
+import styles from './RollUp.module.scss'
+
+/**
+ * Компонент разворачивающегося блока
+ * @param {children} React.ReactNode - контент;
+ */
+export const RollUp: FC = ({ children }) => {
+ const [isRolledOut, setIsRolledOut] = useState(false)
+
+ const onButtonClickHandl = () => {
+ setIsRolledOut(!isRolledOut)
+ }
+
+ return (
+
+
+
+ Описание
+
+
+
+
+
+
+ {isRolledOut && children}
+
+
+ )
+}
diff --git a/src/features/SearchProduct/ui/SearchProduct.module.scss b/src/features/SearchProduct/ui/SearchProduct.module.scss
index b2279efd..31a51ff2 100644
--- a/src/features/SearchProduct/ui/SearchProduct.module.scss
+++ b/src/features/SearchProduct/ui/SearchProduct.module.scss
@@ -1,14 +1,16 @@
@use '@/shared/styles/utils/variables' as var;
.form {
+ grid-area: search;
+ position: relative;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
width: 100%;
+ max-width: 800px;
height: 46px;
background-color: var.$theme-secondary-color;
- display: flex;
- align-items: center;
- justify-content: space-between;
border-radius: 5px;
- position: relative;
}
.button {
diff --git a/src/features/SideBar/index.tsx b/src/features/SideBar/index.tsx
new file mode 100644
index 00000000..dcbb04b0
--- /dev/null
+++ b/src/features/SideBar/index.tsx
@@ -0,0 +1,2 @@
+import SideBar from './ui/SideBar'
+export default SideBar
diff --git a/src/features/SideBar/ui/SideBar.module.scss b/src/features/SideBar/ui/SideBar.module.scss
new file mode 100644
index 00000000..c4e3791e
--- /dev/null
+++ b/src/features/SideBar/ui/SideBar.module.scss
@@ -0,0 +1,51 @@
+@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;
+ transition: 0.25s;
+ }
+
+ &__headerText {
+ transition: 0.25s;
+ }
+
+ &__headerArrow {
+ fill: var.$body-color;
+ transition: 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/features/SideBar/ui/SideBar.stories.tsx b/src/features/SideBar/ui/SideBar.stories.tsx
new file mode 100644
index 00000000..a9489c90
--- /dev/null
+++ b/src/features/SideBar/ui/SideBar.stories.tsx
@@ -0,0 +1,48 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import SideBar from './SideBar'
+
+const meta = {
+ title: 'features/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/features/SideBar/ui/SideBar.tsx b/src/features/SideBar/ui/SideBar.tsx
new file mode 100644
index 00000000..2babfbdd
--- /dev/null
+++ b/src/features/SideBar/ui/SideBar.tsx
@@ -0,0 +1,70 @@
+import { KeyboardEvent, KeyboardEventHandler, ReactElement, useState, type FC } from 'react'
+
+import ArrowIcon from '@/assets/images/sideBarMenu/IconArrowDown.svg'
+import Paragraph from '@/shared/ui/Paragraph/Paragraph'
+
+import styles from './SideBar.module.scss'
+
+export interface ISideBar {
+ title?: string
+ isVisible?: boolean
+ onClick?: () => void
+ onKeyUp?: KeyboardEventHandler
+ children?: ReactElement | JSX.Element | JSX.Element[]
+}
+
+/**
+ * Компонент SideBar - кнопка, раскрывающаяся в бургер меню
+ * @param {string} title - название разворачивающейся кнопки;
+ * @param {boolean} isVisible - атрибут дающий видимость иконке стрелочки;
+ * @param {function} onClick - функция выхода из профиля handleLogOut;
+ * @param {function} onKeyUp - функция выхода из профиля handleLogOut при нажатии клавиши Enter;
+ * @param {JSX.Element} children - контент;
+ */
+
+const SideBar: FC = ({ title, isVisible, onClick, onKeyUp, children }) => {
+ const [isActive, setIsActive] = useState(false)
+
+ const handleClick = () => {
+ setIsActive(!isActive)
+ }
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.code === 'Enter' || e.code === 'Space') {
+ e.preventDefault()
+ e.stopPropagation()
+ handleClick()
+ }
+ }
+
+ return (
+
+
+
{title}
+ {isVisible && (
+
+ )}
+
+ {isVisible && (
+
+ {isActive && children}
+
+ )}
+
+ )
+}
+
+export default SideBar
diff --git a/src/features/SideBarMenuModal/SideBarLink/SideBarLink.module.scss b/src/features/SideBarMenuModal/SideBarLink/SideBarLink.module.scss
new file mode 100644
index 00000000..2313a5b5
--- /dev/null
+++ b/src/features/SideBarMenuModal/SideBarLink/SideBarLink.module.scss
@@ -0,0 +1,26 @@
+@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;
+ font-size: 15px;
+ color: var.$body-color;
+ fill: var.$body-color;
+ padding: 10px 15px;
+ cursor: pointer;
+ 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
new file mode 100644
index 00000000..784830b3
--- /dev/null
+++ b/src/features/SideBarMenuModal/SideBarLink/SideBarLink.tsx
@@ -0,0 +1,31 @@
+import { KeyboardEventHandler, FC } from 'react'
+
+import ArrowIcon from '@/assets/images/sideBarMenu/IconArrowDown.svg'
+
+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..bf5ba39b
--- /dev/null
+++ b/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.module.scss
@@ -0,0 +1,47 @@
+@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;
+ cursor: pointer;
+ transition: 0.25s;
+ }
+
+ &__route:hover {
+ color: var.$header-color;
+ }
+}
diff --git a/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.tsx b/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.tsx
new file mode 100644
index 00000000..72de537f
--- /dev/null
+++ b/src/features/SideBarMenuModal/SideBarSublinks/SideBarSublinks.tsx
@@ -0,0 +1,67 @@
+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 { IData } from '../model/types/types'
+
+import styles from './SideBarSublinks.module.scss'
+
+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 && (
+
+
+
+ {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/types/types.ts b/src/features/SideBarMenuModal/model/types/types.ts
new file mode 100644
index 00000000..6ff2a7d8
--- /dev/null
+++ b/src/features/SideBarMenuModal/model/types/types.ts
@@ -0,0 +1,10 @@
+export interface IData {
+ routes?: IRoute[]
+ subtitle?: string
+ route?: string
+}
+
+export interface IRoute {
+ subtitle?: string
+ route?: string
+}
diff --git a/src/features/SideBarMenuModal/ui/SideBarMenuModal.module.scss b/src/features/SideBarMenuModal/ui/SideBarMenuModal.module.scss
new file mode 100644
index 00000000..f16bd10a
--- /dev/null
+++ b/src/features/SideBarMenuModal/ui/SideBarMenuModal.module.scss
@@ -0,0 +1,45 @@
+@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;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+ color: var.$body-color;
+ padding: 10px 20px;
+ transition: 0.25s;
+ }
+
+ &__button:hover {
+ color: var.$header-color;
+ }
+}
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/features/Slider/model/types.ts b/src/features/Slider/model/types.ts
new file mode 100644
index 00000000..73f48f22
--- /dev/null
+++ b/src/features/Slider/model/types.ts
@@ -0,0 +1,4 @@
+export enum Direction {
+ PREV = 'prev',
+ NEXT = 'next'
+}
diff --git a/src/components/Dots/dots.module.scss b/src/features/Slider/ui/Dot/Dot.module.scss
similarity index 65%
rename from src/components/Dots/dots.module.scss
rename to src/features/Slider/ui/Dot/Dot.module.scss
index b2c4c10e..e7c06d4c 100644
--- a/src/components/Dots/dots.module.scss
+++ b/src/features/Slider/ui/Dot/Dot.module.scss
@@ -1,17 +1,7 @@
-@use '../../shared/styles/utils/mixins' as media;
-@use '../../shared/styles/utils/variables' as color;
-
-.slider__pagination {
- position: absolute;
- width: 100%;
- left: 0;
- bottom: 0;
-
- .dots {
- display: flex;
- justify-content: center;
- cursor: pointer;
+@use '@/shared/styles/utils/mixins' as media;
+@use '@/shared/styles/utils/variables' as color;
+.dots {
&__item {
list-style: none;
position: relative;
@@ -38,5 +28,4 @@
background-color: color.$white;
opacity: 1;
}
- }
-}
+ }
\ No newline at end of file
diff --git a/src/features/Slider/ui/Dot/Dot.tsx b/src/features/Slider/ui/Dot/Dot.tsx
new file mode 100644
index 00000000..8c80bd1c
--- /dev/null
+++ b/src/features/Slider/ui/Dot/Dot.tsx
@@ -0,0 +1,21 @@
+import { FC } from 'react'
+
+import styles from './Dot.module.scss'
+
+export type Props = {
+ isActive: boolean
+ onClick: () => void
+}
+
+/**
+ * Карточка из блока группы историй
+ * @param {TCard} card - параметры карточки из группы историй
+ */
+
+const Dot: FC = ({ isActive, onClick }) => {
+ return (
+
+ )
+}
+
+export default Dot
diff --git a/src/features/Slider/ui/Slider/Slider.module.scss b/src/features/Slider/ui/Slider/Slider.module.scss
new file mode 100644
index 00000000..d979d0e6
--- /dev/null
+++ b/src/features/Slider/ui/Slider/Slider.module.scss
@@ -0,0 +1,79 @@
+@use '@/shared/styles/utils/mixins' as media;
+@use '@/shared/styles/utils/variables' as color;
+
+.slider {
+ display: flex;
+ flex-grow: 1;
+ position: relative;
+ border-radius: 5px;
+ overflow: hidden;
+
+ &__list {
+ width: 100%;
+ display: flex;
+ position: absolute;
+ left: 0;
+ transition: transform 0.5s ease-in-out;
+ }
+ }
+
+.arrow__wrap_prev {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 8.5%;
+ height: 100%;
+ z-index: 1;
+ }
+
+ .arrow__wrap_next {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 8.5%;
+ height: 100%;
+ z-index: 1;
+ }
+
+ .slider__pagination {
+ position: absolute;
+ width: 100%;
+ left: 0;
+ bottom: 0;
+
+ .dots {
+ display: flex;
+ justify-content: center;
+ cursor: pointer;
+ }
+
+ }
+
+ .storybook {
+ width: 940px;
+ height: 370px;
+ }
+
+ .arrow__button {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ transition: background 0.25s;
+ z-index: 100;
+
+ &:hover{
+ background: rgba(255 255 255 / 5%);
+ }
+
+ @include media.respond-to('middle') {
+ display: none;
+ }
+
+ .icon {
+ path {
+ fill: color.$white;
+ }
+ }
+ }
+
+
diff --git a/src/features/Slider/ui/Slider/Slider.stories.tsx b/src/features/Slider/ui/Slider/Slider.stories.tsx
new file mode 100644
index 00000000..30edb111
--- /dev/null
+++ b/src/features/Slider/ui/Slider/Slider.stories.tsx
@@ -0,0 +1,30 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import img3 from '@/assets/images/slider/slide4-940x370.webp'
+
+import Slider from './Slider'
+import styles from './Slider.module.scss'
+
+const meta = {
+ title: 'features/Slider',
+ component: Slider,
+ parameters: {
+ layout: 'centered'
+ },
+
+ tags: ['autodocs']
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Normal: Story = {
+ args: {
+ children: [
+ ,
+ ,
+
+ ],
+ className: styles.storybook
+ }
+}
diff --git a/src/features/Slider/ui/Slider/Slider.tsx b/src/features/Slider/ui/Slider/Slider.tsx
new file mode 100644
index 00000000..f81245a0
--- /dev/null
+++ b/src/features/Slider/ui/Slider/Slider.tsx
@@ -0,0 +1,80 @@
+import { Children, FC, PropsWithChildren, ReactNode, useEffect, useState } from 'react'
+
+import IconLeftArrow from '@/assets/icons/IconLeftArrow.svg'
+import IconRightArrow from '@/assets/icons/IconRightArrow.svg'
+import { Button } from '@/shared/ui/Button/Button'
+
+import { Direction } from '../../model/types'
+import Dot from '../Dot/Dot'
+
+import styles from './Slider.module.scss'
+
+type TProps = {
+ className?: string
+}
+
+/**
+ * @param {string} className - для передачи дополнительных параметров стиля
+ */
+const Slider: FC> = ({ children, className, ...props }) => {
+ const [slideNumber, setSlideNumber] = useState(0)
+ const [localChildren, setLocalChildren] = useState>([])
+
+ function changeSlide(direction: Direction) {
+ let nextSlideNumber
+ if (direction === Direction.NEXT) {
+ nextSlideNumber = (slideNumber + 1) % localChildren.length
+ } else {
+ nextSlideNumber = (slideNumber - 1) % localChildren.length
+ }
+ if (nextSlideNumber < 0) {
+ nextSlideNumber = nextSlideNumber + localChildren.length
+ }
+ setSlideNumber(nextSlideNumber)
+ }
+
+ useEffect(() => {
+ setLocalChildren(Children.toArray(children))
+ }, [children])
+
+ return (
+
+
+ {
+ changeSlide(Direction.NEXT)
+ }}>
+
+
+
+
+ {children}
+
+
+ {
+ changeSlide(Direction.NEXT)
+ }}>
+
+
+
+
+
+ {localChildren.map((item, index) => (
+ {
+ setSlideNumber(index)
+ }}
+ />
+ ))}
+
+
+
+ )
+}
+
+export default Slider
diff --git a/src/features/SubscribeForm/subscribeForm.module.scss b/src/features/SubscribeForm/SubscribeForm.module.scss
similarity index 67%
rename from src/features/SubscribeForm/subscribeForm.module.scss
rename to src/features/SubscribeForm/SubscribeForm.module.scss
index a6bcd5da..fca147c9 100644
--- a/src/features/SubscribeForm/subscribeForm.module.scss
+++ b/src/features/SubscribeForm/SubscribeForm.module.scss
@@ -8,10 +8,15 @@
&_footer {
max-width: 570px;
row-gap: 10px;
+
+ @include media.respond-to('large') {
+ max-width: 100%;
+ margin-bottom: 70px;
+ }
}
&_subscribe {
- max-width: 740px;
+ width: 740px;
row-gap: 20px;
}
}
@@ -24,9 +29,11 @@
padding: 3px 3px 3px 14px;
border-radius: 5px;
position: relative;
+ border: 2px solid transparent;
+ transition: border-color 0.4s;
&:focus-within {
- outline: 2px solid var.$theme-primary-color;
+ border-color: var.$theme-primary-color;
}
&_footer {
@@ -36,12 +43,28 @@
&_subscribe {
background-color: var.$white;
}
+
+ @include media.respond-to('large') {
+ margin-top: 20px;
+ }
}
-.form > input {
- &::placeholder {
- color: var.$theme-primary-color;
- transition: color 0.25s ease-in-out;
+.field {
+ width: 100%;
+ padding: 10px 16px;
+ border: none;
+ outline: none;
+ box-shadow: none;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 1.5;
+
+ &_footer {
+ color: var.$white;
+ }
+
+ &_subscribe {
+ color: var.$black;
}
&:focus {
@@ -51,6 +74,20 @@
}
}
+.span {
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 1.5;
+
+ &_footer {
+ color: var.$white;
+ }
+
+ &_subscribe {
+ color: var.$black;
+ }
+}
+
.button {
padding: 14px 21px;
display: flex;
@@ -96,6 +133,11 @@
font-size: 16px;
font-weight: 500;
line-height: 16px;
+
+ @include media.respond-to('xl') {
+ font-size: 14.5px;
+ line-height: 1;
+ }
}
&_subscribe {
@@ -110,7 +152,7 @@
@include media.respond-to('large') {
display: flex;
- font-size: 16px;
+ font-size: 15px;
font-weight: 400;
line-height: 16px;
}
diff --git a/src/features/SubscribeForm/SubscribeForm.stories.ts b/src/features/SubscribeForm/SubscribeForm.stories.ts
new file mode 100644
index 00000000..b926e05c
--- /dev/null
+++ b/src/features/SubscribeForm/SubscribeForm.stories.ts
@@ -0,0 +1,23 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import SubscribeForm from './SubscribeForm'
+
+const meta = {
+ title: 'features/SubscribeForm',
+ component: SubscribeForm,
+ parameters: {
+ layout: 'centered'
+ }
+} as Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {
+ type: 'footer',
+ onSubmit: () => {
+ alert('Действие по сабмиту формы')
+ }
+ }
+}
diff --git a/src/features/SubscribeForm/SubscribeForm.tsx b/src/features/SubscribeForm/SubscribeForm.tsx
index 4282c9ca..53e6ae7e 100644
--- a/src/features/SubscribeForm/SubscribeForm.tsx
+++ b/src/features/SubscribeForm/SubscribeForm.tsx
@@ -1,25 +1,32 @@
import classNames from 'classnames'
-import { type FC, FormEvent } from 'react'
+import { Field, Form, Formik, FormikErrors, FormikTouched } from 'formik'
+import { FormEvent, useState, type FC } from 'react'
import SubscribeIcon from '@/assets/images/subscriptionForm/icon-subsc.svg'
-import { Input, InputSize, InputTheme } from '@/shared/ui/Input/Input'
+import { useResize } from '@/shared/libs/hooks/useResize'
+import { FormMsg } from '@/shared/ui/FormMsg/FormMsg'
+import { EMsgType } from '@/shared/ui/FormMsg/model/types/types'
+import Label from '@/shared/ui/Label/Label'
+import Span from '@/shared/ui/Span/Span'
-import styles from './subscribeForm.module.scss'
+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
-// 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 [showErrorMsg, setShowErrorMsg] = useState(false)
+ const { isScreenLg, isScreenMd, isScreenSm } = useResize()
+
const classNameContainer = classNames(styles.container, {
[styles.container]: true,
[styles.container_footer]: type === 'footer',
@@ -35,21 +42,83 @@ 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 classNameSpan = classNames({
+ [styles.span]: true,
+ [styles.span_footer]: type === 'footer',
+ [styles.span_subscribe]: type === 'subscribe'
+ })
+
+ const submitHandle = () => {
+ //@TODO: Доделать после появления эндпоинта на BE
+ onSubmit()
+ }
+
+ const preSubmitHandle = (
+ e: FormEvent,
+ touched: FormikTouched<{
+ email: string
+ }>,
+ errors: FormikErrors<{
+ email: string
+ }>
+ ) => {
+ e.preventDefault()
+ setShowErrorMsg(Boolean(touched.email && errors.email))
+ }
return (
-
- {/* @TODO: Добавить компонент Label
- https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/102 */}
-
Подписаться на рассылку
-
Мы не будем присылать вам спам. Только скидки и выгодные предложения
-
-
+
+ {({ isSubmitting, errors, touched }) => (
+
+ )}
+
)
}
diff --git a/src/features/SubscribeForm/model/constants/constants.ts b/src/features/SubscribeForm/model/constants/constants.ts
new file mode 100644
index 00000000..46387279
--- /dev/null
+++ b/src/features/SubscribeForm/model/constants/constants.ts
@@ -0,0 +1,2 @@
+export const EMAIL_VALIDATION_ERROR = 'E-mail адрес введён неверно!'
+export const EMAIL_REQUIRED_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..b37a9852
--- /dev/null
+++ b/src/features/SubscribeForm/model/validationSchema/validationSchema.ts
@@ -0,0 +1,7 @@
+import * as Yup from 'yup'
+
+import { EMAIL_REQUIRED_ERROR, EMAIL_VALIDATION_ERROR } from '../constants/constants'
+
+export const validationSchema = Yup.object().shape({
+ email: Yup.string().required(EMAIL_REQUIRED_ERROR).email(EMAIL_VALIDATION_ERROR)
+})
diff --git a/src/components/WidgetButtonsFunctions/WidgetButtonsFunctions.module.scss b/src/features/WidgetButtonsFunctions/WidgetButtonsFunctions.module.scss
similarity index 100%
rename from src/components/WidgetButtonsFunctions/WidgetButtonsFunctions.module.scss
rename to src/features/WidgetButtonsFunctions/WidgetButtonsFunctions.module.scss
diff --git a/src/features/WidgetButtonsFunctions/WidgetButtonsFunctions.stories.tsx b/src/features/WidgetButtonsFunctions/WidgetButtonsFunctions.stories.tsx
new file mode 100644
index 00000000..3f448eb8
--- /dev/null
+++ b/src/features/WidgetButtonsFunctions/WidgetButtonsFunctions.stories.tsx
@@ -0,0 +1,34 @@
+import { Story, Meta } from '@storybook/react'
+
+import { WidgetButtonsFunctions } from '@/features/WidgetButtonsFunctions/WidgetButtonsFunctions'
+import { ECardView } from '@/shared/model/types/common'
+
+export default {
+ title: 'features/WidgetButtonsFunction',
+ component: WidgetButtonsFunctions
+} as Meta
+
+type TWidgetButtonsFunctions = {
+ isLiked: boolean
+ handleLike: VoidFunction
+ isInCompared: boolean
+ handleAddToCompared: VoidFunction
+ layout: ECardView
+}
+
+const Template: Story = args =>
+
+export const Grid = Template.bind({})
+Grid.args = {
+ layout: ECardView.GRID
+}
+
+export const List = Template.bind({})
+List.args = {
+ layout: ECardView.LIST
+}
+
+export const Compact = Template.bind({})
+Compact.args = {
+ layout: ECardView.COMPACT
+}
diff --git a/src/components/WidgetButtonsFunctions/WidgetButtonsFunctions.tsx b/src/features/WidgetButtonsFunctions/WidgetButtonsFunctions.tsx
similarity index 84%
rename from src/components/WidgetButtonsFunctions/WidgetButtonsFunctions.tsx
rename to src/features/WidgetButtonsFunctions/WidgetButtonsFunctions.tsx
index bb9185d9..f8a69472 100644
--- a/src/components/WidgetButtonsFunctions/WidgetButtonsFunctions.tsx
+++ b/src/features/WidgetButtonsFunctions/WidgetButtonsFunctions.tsx
@@ -5,9 +5,8 @@ import IconCompare from '@/assets/icons/IconCompare.svg'
import IconLike from '@/assets/icons/IconLike'
import { ECardView } from '@/shared/model/types/common'
import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button/Button'
-
-import styles from '../ProductCard/ProductCard.module.scss'
-import { getStylesForCurrentLayout } from '../ProductCard/utils/utils'
+import { getStylesForCurrentLayout } from '@/shared/ui/ProductLabels/utils/utils'
+import styles from '@/widgets/ProductItem/ProductItem.module.scss'
import stylesSvg from './WidgetButtonsFunctions.module.scss'
@@ -47,8 +46,8 @@ export const WidgetButtonsFunctions: FC = ({
[getStylesForCurrentLayout('customButton', stylesSvg)[layout]]: layout
})}>