diff --git a/src/assets/icons/IconLeftArrow.svg b/src/assets/icons/IconLeftArrow.svg new file mode 100644 index 00000000..b81652a8 --- /dev/null +++ b/src/assets/icons/IconLeftArrow.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/IconRightArrow.svg b/src/assets/icons/IconRightArrow.svg new file mode 100644 index 00000000..ae058501 --- /dev/null +++ b/src/assets/icons/IconRightArrow.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/dots.svg b/src/assets/icons/dots.svg new file mode 100644 index 00000000..e11ebf69 --- /dev/null +++ b/src/assets/icons/dots.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/CardReview/CardReview.module.scss b/src/components/CardReview/CardReview.module.scss deleted file mode 100644 index 6781b0a0..00000000 --- a/src/components/CardReview/CardReview.module.scss +++ /dev/null @@ -1,87 +0,0 @@ -@use '../../shared/styles/utils/variables' as color; - -.review { - display: flex; - flex-direction: column; - min-width: 340px; - align-items: flex-start; - justify-content: space-between; - padding: 35px 30px 40px 40px; - gap: 9px; - - .title { - display: flex; - align-items: center; - gap: 3px; - } - - p { - font-size: 0.9375rem; - font-weight: 400; - line-height: 1.7; - } - - &__header { - display: flex; - align-items: center; - line-height: 1.62; - gap: 11px; - - h3 { - font-size: 1rem; - font-weight: 500; - } - - span { - font-size: 0.875rem; - font-weight: 400; - display: flex; - align-items: center; - gap: 3px; - } - } - - &__initials { - width: 40px; - height: 40px; - border-radius: 50%; - background: color.$bg-gradient-green; - color: color.$white; - font-size: 1.125rem; - font-weight: 500; - line-height: 1.2; - display: flex; - align-items: center; - justify-content: center; - } - - &__data { - font-weight: 400; - line-height: 1.7; - - p { - overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 5; - -webkit-box-orient: vertical; - text-overflow: clip; - } - - span { - font-size: 0.875rem; - opacity: 0.4; - } - } -} - -.link__text { - font-size: 0.9375rem; - line-height: 1.7; - font-weight: 500; - text-decoration: none; - color: color.$link-color; - - &:hover { - opacity: 0.7; - } -} diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx deleted file mode 100644 index d9c5163a..00000000 --- a/src/components/Footer/Footer.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import Logo from '../logo/Logo' -import VisaIcon from '@/assets/images/footer/visa.svg' -import MastercardIcon from '@/assets/images/footer/mastercard.svg' -import MirIcon from '@/assets/images/footer/mir.svg' -import WebmoneyIcon from '@/assets/images/footer/webmoney.svg' -import UmoneyIcon from '@/assets/images/footer/io.svg' -import SubscribeForm from '../SubscribeForm/SubscribeForm' -import Link from '@/shared/ui/Link/Link' -import styles from './footer.module.scss' - -function Footer() { - const onSubmitHandler = () => {} - return ( - - ) -} - -export default Footer diff --git a/src/components/ReviewsBlock/ReviewsBlock.module.scss b/src/components/ReviewsBlock/ReviewsBlock.module.scss deleted file mode 100644 index 05d5dc2a..00000000 --- a/src/components/ReviewsBlock/ReviewsBlock.module.scss +++ /dev/null @@ -1,59 +0,0 @@ -@use '../../shared/styles/utils/variables' as color; - -.wrapper { - width: 100%; - margin: 0 auto; - - h2 { - font-size: #{'min(max(18px, 1.6vw), 20px)'}; - line-height: 115%; - font-weight: 500; - } - - .header { - display: flex; - justify-content: space-between; - align-items: flex-end; - margin: 0 25px; - } - - .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; - max-width: 100%; - padding: 20px 25px; - overflow: auto hidden; - cursor: grab; - - &::-webkit-scrollbar { - height: 3px; - } - - &::-webkit-scrollbar-thumb { - background: color.$theme-primary-color; - cursor: grab; - } - - &::-webkit-scrollbar-track { - margin-left: 25px; - margin-right: 25px; - } - } -} diff --git a/src/components/SearchItem/search-item.module.scss b/src/components/SearchItem/search-item.module.scss deleted file mode 100644 index 8371e4e2..00000000 --- a/src/components/SearchItem/search-item.module.scss +++ /dev/null @@ -1,57 +0,0 @@ -.link { - padding: 20px 30px; - display: flex; - align-items: center; - min-height: 30px; - color: #343434; - text-decoration: none; - transition: color 0.25s; -} - -.image { - width: 34px; - height: 34px; - margin-right: 20px; - flex: 0 0 auto; -} - -.number { - margin: 0; - font-size: 14px; - line-height: 12px; - font-weight: 400; - color: #bdc2d3; -} - -.price { - display: flex; - align-items: center; - font-size: 16px; - font-weight: 400; - margin: 0; - color: #343434; -} - -.wrapper { - display: flex; - flex-direction: column; - margin-right: 20px; - flex: 1; -} - -.paragraph { - font-size: 15px; - font-weight: 400; - line-height: 1.2; - margin: 0 0 5px; -} - -.price-wrapper { - display: flex; - flex: 0 0 auto; - column-gap: 20px; -} - -.image-link { - visibility: visible; -} diff --git a/src/components/Subscribe/Subscribe.tsx b/src/components/Subscribe/Subscribe.tsx index 6b514c39..c4a18f33 100644 --- a/src/components/Subscribe/Subscribe.tsx +++ b/src/components/Subscribe/Subscribe.tsx @@ -1,6 +1,6 @@ import styles from './Subscribe.module.scss' import imgSubs from '@/assets/images/img-subsc-small.png' -import SubscribeForm from '../SubscribeForm/SubscribeForm' +import SubscribeForm from '../../features/SubscribeForm/SubscribeForm' const Subscribe = () => { const onSubmitHandler = () => {} diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx index 2eaab324..e3cb12ee 100644 --- a/src/components/header/header.tsx +++ b/src/components/header/header.tsx @@ -1,7 +1,7 @@ +import { coreBaseData } from '@/mockData/coreBaseData' import { useMemo } from 'react' import classNames from 'classnames' -import Logo from '../logo/Logo' -import Search from '../search/search' +import Logo from '../../shared/ui/logo/Logo' import ArrowIcon from '@/assets/icons/arrow.svg' import LightningIcon from '@/assets/images/header/lightning.svg' import ContextMenuElement from '../ContextMenuElement/ContextMenuElement' @@ -15,6 +15,7 @@ import { CatalogLinksId } from '@/shared/config/catalogLinks/catalogLinks' import Link from '@/shared/ui/Link/Link' import IconCategories from '@/assets/icons/IconCategories.svg' import styles from './header.module.scss' +import SearchProduct from '@/features/SearchProduct' function Header() { const aboutUsNode = useMemo( @@ -151,8 +152,14 @@ function Header() {
- - + +
diff --git a/src/components/logo/Logo.tsx b/src/components/logo/Logo.tsx deleted file mode 100644 index b9514daf..00000000 --- a/src/components/logo/Logo.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { FC } from 'react' -import logo from '@/assets/images/logo/maxboom.jpg' -import styles from './logo.module.scss' -import Link from '@/shared/ui/Link/Link' - -type TLogoProps = { - width: string - height: string -} -/** - * @param {string} width - ширина логотипа - * @param {string} height - высота логотипа - */ - -const Logo: FC = ({ width, height }) => { - return ( -
- - maxboom - -
- ) -} - -export default Logo diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx deleted file mode 100644 index 7e232b57..00000000 --- a/src/components/search/search.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { FC, useEffect, useState } from 'react' -import styles from './search.module.scss' -import { searchResponseData } from '@/mockData/searchData' -import { TResultData } from '@/shared/model/types/common' -import SearchResult from '../searchResult/searchResult' -import { Input, InputSize, InputTheme } from '@/shared/ui/Input/Input' - -// @TODO: Перевести форму на Formik + Yup -// https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/92 -const Search: FC> = () => { - const [visible, setVisability] = useState(false) - const [resultData, setResultData] = useState({ data: [], success: false }) // TODO: move to redux - - const inputEventHandler = () => { - setResultData(searchResponseData) - } - - const closeContextMenuHandler = (e: Event) => { - const searchResultNode = document.querySelector(styles.result) - const withinBoundaries = e.composedPath().includes(searchResultNode!) - - if (!withinBoundaries && visible) { - setVisability(false) - } - } - - useEffect(() => { - if (resultData.success) { - setVisability(true) - } - }, [resultData]) - - useEffect(() => { - document.addEventListener('click', closeContextMenuHandler) - return () => { - document.removeEventListener('click', closeContextMenuHandler) - } - }, []) - - return ( -
- - - {visible && } - - ) -} - -export default Search diff --git a/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx b/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx new file mode 100644 index 00000000..c45fa99b --- /dev/null +++ b/src/entities/CardReview/ui/CardReview/CardReview.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react' +import CardReview from './CardReview' + +const meta = { + title: 'entities/CardReview', + component: CardReview, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + review: { + id: 1, + name: 'Рейтинг нашего магазина', + score: '4.3', + text: 'Мы очень ним гордимся, это результат упорного труда в течении длительного времени и сейчас наша команда ежедневно работает над улучшением сервиса.', + date: '' + } + } +} diff --git a/src/components/CardReview/CardReview.tsx b/src/entities/CardReview/ui/CardReview/CardReview.tsx similarity index 97% rename from src/components/CardReview/CardReview.tsx rename to src/entities/CardReview/ui/CardReview/CardReview.tsx index 5e84f19b..b77adfb1 100644 --- a/src/components/CardReview/CardReview.tsx +++ b/src/entities/CardReview/ui/CardReview/CardReview.tsx @@ -4,7 +4,7 @@ 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 styles from './cardReview.module.scss' export type Props = { review: TReview diff --git a/src/entities/CardReview/ui/CardReview/cardReview.module.scss b/src/entities/CardReview/ui/CardReview/cardReview.module.scss new file mode 100644 index 00000000..296f8a0a --- /dev/null +++ b/src/entities/CardReview/ui/CardReview/cardReview.module.scss @@ -0,0 +1,87 @@ +@use '../../../../shared/styles/utils/variables' as color; + +.review { + display: flex; + flex-direction: column; + min-width: 340px; + align-items: flex-start; + justify-content: space-between; + padding: 35px 30px 40px 40px; + gap: 9px; + + .title { + display: flex; + align-items: center; + gap: 3px; + } + + p { + font-size: 0.9375rem; + font-weight: 400; + line-height: 1.7; + } + + &__header { + display: flex; + align-items: center; + line-height: 1.62; + gap: 11px; + + h3 { + font-size: 1rem; + font-weight: 500; + } + + span { + font-size: 0.875rem; + font-weight: 400; + display: flex; + align-items: center; + gap: 3px; + } + } + + &__initials { + width: 40px; + height: 40px; + border-radius: 50%; + background: color.$bg-gradient-green; + color: color.$white; + font-size: 1.125rem; + font-weight: 500; + line-height: 1.2; + display: flex; + align-items: center; + justify-content: center; + } + + &__data { + font-weight: 400; + line-height: 1.7; + + p { + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 5; + -webkit-box-orient: vertical; + text-overflow: clip; + } + + span { + font-size: 0.875rem; + opacity: 0.4; + } + } +} + +.link__text { + font-size: 0.9375rem; + line-height: 1.7; + font-weight: 500; + text-decoration: none; + color: color.$link-color; + + &:hover { + opacity: 0.7; + } +} \ No newline at end of file diff --git "a/src/entities/\320\241ontactCard/\320\241ontactCard.tsx" b/src/entities/ContactCard/ContactCard.tsx similarity index 94% rename from "src/entities/\320\241ontactCard/\320\241ontactCard.tsx" rename to src/entities/ContactCard/ContactCard.tsx index 95935675..f84f04bf 100644 --- "a/src/entities/\320\241ontactCard/\320\241ontactCard.tsx" +++ b/src/entities/ContactCard/ContactCard.tsx @@ -23,7 +23,7 @@ const ContactCard: FC = ({ messenger, Icon }) => {
- {messenger.title} + {messenger.title} ) diff --git "a/src/entities/\320\241ontactCard/contactCard.module.scss" b/src/entities/ContactCard/contactCard.module.scss similarity index 81% rename from "src/entities/\320\241ontactCard/contactCard.module.scss" rename to src/entities/ContactCard/contactCard.module.scss index 7fd48054..9520775b 100644 --- "a/src/entities/\320\241ontactCard/contactCard.module.scss" +++ b/src/entities/ContactCard/contactCard.module.scss @@ -19,10 +19,4 @@ width: 60px; height: 60px; margin-bottom: 12px; -} - -.text { - color: var.$body-color; - font-size: 15px; - text-align: center; -} +} \ No newline at end of file diff --git a/src/entities/Payments/Payments.stories.ts b/src/entities/Payments/Payments.stories.ts new file mode 100644 index 00000000..ba7cf140 --- /dev/null +++ b/src/entities/Payments/Payments.stories.ts @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import Payments from './Payments' + +const data = { + footer: { + additional_logos: [ + { + image: 'https://maxboom.ru/image/catalog/demo-prostore/svg-icon/icon-payments-visa.svg', + url: '//visa.ru', + title: 'visa' + }, + { + image: 'https://maxboom.ru/image/catalog/demo-prostore/svg-icon/io.svg', + url: '//yoomoney.ru', + title: 'yoomoney' + } + ], + support: { + name: 'Обрантый звонок', + phone_number: '+7 977 848-02-28' + } + } +} + +const meta = { + title: 'entities/Payments', + component: Payments, + parameters: { + layout: 'centered' + } +} as Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + data: data + } +} diff --git a/src/entities/Payments/Payments.tsx b/src/entities/Payments/Payments.tsx new file mode 100644 index 00000000..a762662b --- /dev/null +++ b/src/entities/Payments/Payments.tsx @@ -0,0 +1,34 @@ +import { type FC } from 'react' +import Link from '@/shared/ui/Link/Link' +import styles from './payments.module.scss' + +type TPayments = { + data: { footer: { additional_logos: Logo[] } } +} + +type Logo = { + image: string + title: string + url: string +} + +/** платежная система + * @param {string} image - путь к картинке + * @param {string} title - название + * @param {string} url - путь к сайту + */ +const Payments: FC = ({ data }) => { + return ( +
    + {data.footer.additional_logos.map(logo => ( +
  • + + {logo.title} + +
  • + ))} +
+ ) +} + +export default Payments diff --git a/src/entities/Payments/payments.module.scss b/src/entities/Payments/payments.module.scss new file mode 100644 index 00000000..1a1ccbe0 --- /dev/null +++ b/src/entities/Payments/payments.module.scss @@ -0,0 +1,39 @@ +@use '/src/shared/styles/utils/variables' as var; + +.payments { + margin: 0; + padding: 0; + display: flex; + column-gap: 3px; + list-style: none; + margin-right: 8px; +} + +.payment { + &-item { + display: flex; + width: 34px; + height: 24px; + padding: 3px; + justify-content: center; + align-items: center; + border-radius: 4px; + background: var.$bg-no-image; + } + + &-icon { + display: flex; + width: 100%; + height: 100%; + } + + &-nav { + margin: 0; + padding: 0; + list-style: none; + display: flex; + align-items: center; + justify-content: center; + column-gap: 3px; + } +} diff --git a/src/entities/ProductEntity/ui/ProductEntity/ProductEntity.module.scss b/src/entities/ProductEntity/ui/ProductEntity/ProductEntity.module.scss new file mode 100644 index 00000000..6f73d6fc --- /dev/null +++ b/src/entities/ProductEntity/ui/ProductEntity/ProductEntity.module.scss @@ -0,0 +1,71 @@ +@use '../../../../shared/styles/utils/variables' as var; +@use '../../../../shared/styles/utils/mixins' as media; + + + +.description { + display: flex; + align-items: center; + padding: 10px; + background: var.$white; + border-radius: 10px; + height: 110px; + width: 400px; + + @include media.respond-to('large') { + width: 75%; + } +} + +.frame { + width: 90px; + height: 90px; + display: flex; + align-items: center; + justify-content: center; + padding: 10px; + margin: 0 20px 0 0; + background: var.$white; + border-radius: 5px; + + @include media.respond-to('large') { + border: 1px solid var.$light-grey; + } +} + +.image { + display: flex; + max-height: 100%; + width: 70px; + height: 70px; + object-fit: cover; +} + +.description_wrapper { + display: flex; + flex-direction: column; + max-height: 80px; +} + +.number { + margin: 0 0 5px; + font-size: 14px; + line-height: 1; + font-weight: 400; + color: var.$subtitle-color; +} + +.name { + display: flex; + font-size: 15px; + line-height: 1.35; + font-weight: 700; + color: var.$body-color; + text-overflow: ellipsis; + overflow: hidden; + width: 250px; + + @include media.respond-to('large') { + width: 100%; + } +} \ No newline at end of file diff --git a/src/entities/ProductEntity/ui/ProductEntity/ProductEntity.tsx b/src/entities/ProductEntity/ui/ProductEntity/ProductEntity.tsx new file mode 100644 index 00000000..0ea9e606 --- /dev/null +++ b/src/entities/ProductEntity/ui/ProductEntity/ProductEntity.tsx @@ -0,0 +1,17 @@ +import { TProduct } from '@/mockData/productsData' +import styles from './ProductEntity.module.scss' +import { type FC } from 'react' + +export const ProductEntity: FC = product => { + return ( +
+
+ {'product'} +
+
+ {product.article} + {product.name} +
+
+ ) +} diff --git a/src/components/SearchItem/searchItem.module.scss b/src/entities/SearchItem/SearchItem.module.scss similarity index 94% rename from src/components/SearchItem/searchItem.module.scss rename to src/entities/SearchItem/SearchItem.module.scss index 4b9996af..f15b65b8 100644 --- a/src/components/SearchItem/searchItem.module.scss +++ b/src/entities/SearchItem/SearchItem.module.scss @@ -1,4 +1,4 @@ -@use '../../shared/styles/utils/variables' as var; +@use '@/shared/styles/utils/variables' as var; .link { padding: 20px 30px; @@ -17,23 +17,6 @@ flex: 0 0 auto; } -.number { - margin: 0; - font-size: 14px; - line-height: 12px; - font-weight: 400; - color: var.$body-color-light-grey; -} - -.price { - display: flex; - align-items: center; - font-size: 16px; - font-weight: 400; - margin: 0; - color: var.$body-color; -} - .wrapper { display: flex; flex-direction: column; @@ -48,12 +31,29 @@ margin: 0 0 5px; } +.number { + margin: 0; + font-size: 14px; + line-height: 12px; + font-weight: 400; + color: var.$body-color-light-grey; +} + .price-wrapper { display: flex; flex: 0 0 auto; column-gap: 20px; } +.price { + display: flex; + align-items: center; + font-size: 16px; + font-weight: 400; + margin: 0; + color: var.$body-color; +} + .image-link { visibility: visible; } diff --git a/src/entities/SearchItem/SearchItem.stories.tsx b/src/entities/SearchItem/SearchItem.stories.tsx new file mode 100644 index 00000000..35f22c83 --- /dev/null +++ b/src/entities/SearchItem/SearchItem.stories.tsx @@ -0,0 +1,22 @@ +import { Meta, Story } from '@storybook/react' +import { searchResponseData } from '@/mockData/searchData' +import SearchItem from './SearchItem' +import { TProduct } from '@/shared/model/types/common' +import { TLinkProps } from '@/shared/ui/Link/Link' + +export default { + title: 'Entities/SearchItem', + component: SearchItem, + parameters: { + layout: 'centered' + } +} as Meta + +type StoryProps = TProduct & TLinkProps + +const Template: Story = args => + +export const Default: Story = Template.bind({}) +Default.args = { + ...(searchResponseData.data[2] as StoryProps) +} diff --git a/src/components/SearchItem/SearchItem.tsx b/src/entities/SearchItem/SearchItem.tsx similarity index 58% rename from src/components/SearchItem/SearchItem.tsx rename to src/entities/SearchItem/SearchItem.tsx index deab9b8a..ca657e43 100644 --- a/src/components/SearchItem/SearchItem.tsx +++ b/src/entities/SearchItem/SearchItem.tsx @@ -1,32 +1,33 @@ -import { FC, useMemo, useState } from 'react' +import { type FC, useMemo, useState } from 'react' import ArrowRightIcon from '@/assets/images/searchItem/arrow-right.svg' import { TProduct } from '@/shared/model/types/common' import Link, { TLinkProps } from '@/shared/ui/Link/Link' -import styles from './searchItem.module.scss' +import styles from './SearchItem.module.scss' /** - * @param {string} image - фото товара - * @param {string} name - название товара - * @param {number} number - артикул товара - * @param {string} price - цена товара с валютой + * Компонент элемента из поисковой выдачи для тултипа-подсказки */ const SearchItem: FC = props => { const { image, name, number, price, ...otherProps } = props - const [isVisible, setVisability] = useState(false) + const [isVisible, setVisibility] = useState(false) const handleMouseEnter = () => { - setVisability(true) + setVisibility(true) } const handleMouseLeave = () => { - setVisability(false) + setVisibility(false) } const arrowNode = useMemo(() => , []) return ( - - product + + product

{name}

{number} diff --git a/src/components/SubscribeForm/SubscribeForm.tsx b/src/entities/SubscribeForm/SubscribeForm.tsx similarity index 52% rename from src/components/SubscribeForm/SubscribeForm.tsx rename to src/entities/SubscribeForm/SubscribeForm.tsx index a66df0e5..c581dd39 100644 --- a/src/components/SubscribeForm/SubscribeForm.tsx +++ b/src/entities/SubscribeForm/SubscribeForm.tsx @@ -1,7 +1,8 @@ -import { FC, FormEvent } from 'react' +import { type FC, FormEvent } from 'react' import SubscribeIcon from '@/assets/images/subscriptionForm/icon-subsc.svg' import styles from './subscribeForm.module.scss' import { Input, InputSize, InputTheme } from '@/shared/ui/Input/Input' +import classNames from 'classnames' type TSubscribeForm = { type: 'footer' | 'subscribe' @@ -16,30 +17,32 @@ type TSubscribeForm = { * @param {string} className - нужно будет, если захотят переиспользовать компонент * @param {string} onSubmit - функция для обработки формы */ -const SubscribeForm: FC = ({ className, type, onSubmit }) => { - let classNameLabel = styles.label_footer - if (type === 'subscribe') { - classNameLabel = styles.label_subscribe - } - - let classNameForm = styles.form_footer - if (type === 'subscribe') { - classNameForm = styles.form_subscribe - } - let classNameContainer = styles.container_footer - if (type === 'subscribe') { - classNameContainer = styles.container_subscribe - } +const SubscribeForm: FC = ({ type, onSubmit, className = '' }) => { + const classNameContainer = classNames(styles.container, { + [styles.container]: true, + [styles.container_footer]: type === 'footer', + [styles.container_subscribe]: type === 'subscribe' + }) + const classNameLabel = classNames({ + [styles.label]: true, + [styles.label_footer]: type === 'footer', + [styles.label_subscribe]: type === 'subscribe' + }) + const classNameForm = classNames({ + [styles.form]: true, + [styles.form_footer]: type === 'footer', + [styles.form_subscribe]: type === 'subscribe' + }) return ( -
+
{/* @TODO: Добавить компонент Label https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/102 */} - Подписаться на рассылку - Мы не будем присылать вам спам. Только скидки и выгодные предложения -
+ +

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

+ - diff --git a/src/components/SubscribeForm/subscribeForm.module.scss b/src/entities/SubscribeForm/subscribeForm.module.scss similarity index 96% rename from src/components/SubscribeForm/subscribeForm.module.scss rename to src/entities/SubscribeForm/subscribeForm.module.scss index e8302735..b1097a5c 100644 --- a/src/components/SubscribeForm/subscribeForm.module.scss +++ b/src/entities/SubscribeForm/subscribeForm.module.scss @@ -96,10 +96,10 @@ .caption { display: none; - @include media.respond-to('middle') { + @include media.respond-to('large') { display: flex; font-size: 16px; - font-weight: 500; + font-weight: 400; line-height: 16px; } } diff --git a/src/features/CartCouponApply/ui/CartCouponApply/CartCouponApply.module.scss b/src/features/CartCouponApply/ui/CartCouponApply/CartCouponApply.module.scss new file mode 100644 index 00000000..77126f42 --- /dev/null +++ b/src/features/CartCouponApply/ui/CartCouponApply/CartCouponApply.module.scss @@ -0,0 +1,93 @@ +@use '../../../../shared/styles/utils/variables' as var; +@use '../../../../shared/styles/utils/mixins' as media; + +.container { + display: flex; + flex-direction: column; + padding: 30px 40px; + background-color: var.$white; + width: 460px; + border-radius: 5px; + + @include media.respond-to('large') { + width: 100% + } +} + +.wrapper { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + +.title { + font-size: 18px; + line-height: 1.2; + font-weight: 600; +} + +.button_change { + background-color: inherit; + border: none; + font-size: 14px; + line-height: 1.2; + font-weight: 400; + color: var.$link-color; + + &:hover { + opacity: 0.8; + } +} + +.link { + text-decoration: none; +} + +.form { + display: flex; + align-items: center; + justify-content: space-between; + border: 1px solid var.$light-grey; + border-radius: 5px; + padding: 3px 3px 3px 20px; + + &:focus-within { + border: 1px solid var.$black; + } +} + +.input { + width: 100%; + background: inherit; + font-size: 14px; + line-height: 24px; + font-weight: 400; + color: var.$black; + appearance: none; + transition: 0.25s; + + &::placeholder { + color: var.$body-color-light-grey; + } +} + +.button { + display: flex; + align-items: center; + justify-content: center; + width: 108px; + height: 36px; + padding: 5px 20px; + background: var.$light-grey; + color: var.$black; + font-size: 14px; + border-radius: 5px; + font-weight: 700; + + &:hover { + background-color: var.$theme-primary-color; + color: var.$white; + } +} \ No newline at end of file diff --git a/src/features/CartCouponApply/ui/CartCouponApply/CartCouponApply.tsx b/src/features/CartCouponApply/ui/CartCouponApply/CartCouponApply.tsx new file mode 100644 index 00000000..c74b5458 --- /dev/null +++ b/src/features/CartCouponApply/ui/CartCouponApply/CartCouponApply.tsx @@ -0,0 +1,38 @@ +import { useState } from 'react' +import styles from './CartCouponApply.module.scss' + +const states = { + CERTIFICATE: 'CERTIFICATE', + COUPON: 'COUPON' +} + +export const CartCouponApply: React.FC = () => { + const [value, setValue] = useState(states.CERTIFICATE) + + function changeCouponToCertificate() { + if (value === states.CERTIFICATE) { + setValue(states.COUPON) + } else { + setValue(states.CERTIFICATE) + } + } + + const title = value === states.CERTIFICATE ? 'Сертификат' : 'Купон' + const buttonText = value === states.CERTIFICATE ? 'Ввести купон' : 'Ввести сертификат' + const placeholder = value === states.CERTIFICATE ? 'Ввести код сертификата' : 'Ввести код купона' + + return ( +
+
+

{title}

+ +
+ + + + +
+ ) +} diff --git a/src/features/CartEdit/ui/CartEdit/CartEdit.module.scss b/src/features/CartEdit/ui/CartEdit/CartEdit.module.scss new file mode 100644 index 00000000..8958d2b4 --- /dev/null +++ b/src/features/CartEdit/ui/CartEdit/CartEdit.module.scss @@ -0,0 +1,120 @@ +@use '../../../../shared/styles/utils/variables' as var; +@use '../../../../shared/styles/utils/mixins' as media; + +.container { + display: flex; + align-items: center; + width: 938px; + height: 140px; + background-color: var.$white; + border-radius: 10px; + padding: 20px; + position: relative; + + @include media.respond-to('large') { + width: 100%; + flex-direction: column; + height: 200px; + row-gap: 20px; + padding-right: 20px; + } +} + +.product { + display: flex; + align-items: center; + width: 70%; + + @include media.respond-to('large') { + width: 100%; + } +} + + +.input { + display: flex; + font-size: 18px; + line-height: 28px; + font-weight: 500; + letter-spacing: 0.1px; + background: inherit; + height: 100%; + min-width: 20px; + max-width: 40px; + text-align: center; + appearance: none; + outline: none; + margin-left: 1px; + margin-right: 1px; +} + +.counter { + border: 1px solid var.$light-grey; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: space-between; + background-color: inherit; + padding: 4px; + height: 48px; + + @include media.respond-to('large') { + width: 100%; + } +} + +.button { + border: none; + background: none; + padding: 0; + font-family: inherit; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 100%; + transition: background 0.25s; +} + +.button_decrease:hover { + background-color: var.$light-grey; + border-radius: 4px 0 0 4px; +} + +.button_increase:hover { + background-color: var.$light-grey; + border-radius: 0 4px 4px 0; +} + +.sum_wrapper { + display: flex; + flex-direction: column; + padding-left: 5%; + margin-right: 10%; + + @include media.respond-to('large') { + width: 25%; + } +} + +.sum { + font-size: 18px; + line-height: 1.2; + font-weight: 400; + white-space: nowrap; +} + +.price { + font-size: 13.5px; + color: var.$body-color-light-grey; + line-height: 1.2; + font-weight: 400; + white-space: nowrap; +} + +.button_dots { + position: absolute; + z-index: 90; + top: 0; + right: 0; +} \ No newline at end of file diff --git a/src/features/CartEdit/ui/CartEdit/CartEdit.tsx b/src/features/CartEdit/ui/CartEdit/CartEdit.tsx new file mode 100644 index 00000000..1487e24b --- /dev/null +++ b/src/features/CartEdit/ui/CartEdit/CartEdit.tsx @@ -0,0 +1,89 @@ +import { TCartItemExt } from '@/mockData/cartData' +import styles from './CartEdit.module.scss' +import { useEffect, useState } from 'react' +import ButtonDots from '@/shared/ui/ButtonDots/ButtonDots' +import { ProductEntity } from '@/entities/ProductEntity/ui/ProductEntity/ProductEntity' + +type TProps = { + product: TCartItemExt + decreaseQuantity: (productArticle: string) => void + increaseQuantity: (productArticle: string) => void + setQuantity: (productArticle: string, quantity: number) => void + removeProduct: (productArticle: string) => void +} + +/** + * @param {TCartItemExt} product - это продукт для определения состояния + */ + +export const CartEdit: React.FC = (props: TProps) => { + const [amount, setAmount] = useState(0) + + useEffect(() => { + setAmount(props.product.quantity) + }, [props.product.quantity]) + + return ( + <> +
+
+ +
+

+ {props.product.price * props.product.quantity} {props.product.currency} +

+

+ {props.product.price} {props.product.currency}/шт +

+
+
+
+ + { + props.setQuantity(props.product.article, Number(e.target.value)) + }}> + +
+ +
+ + ) +} diff --git a/src/features/Contacts/Contacts.tsx b/src/features/Contacts/Contacts.tsx index 81cdc939..c0499344 100644 --- a/src/features/Contacts/Contacts.tsx +++ b/src/features/Contacts/Contacts.tsx @@ -1,10 +1,11 @@ -import { FC, useState } from 'react' +import { type FC, useState } from 'react' +import classNames from 'classnames' import { TMessenger } from '@/models/MessengerModel' import styles from './contacts.module.scss' -import ContactCard from '@/entities/СontactCard/СontactCard' import { Button, ButtonTheme, ButtonDesign } from '@/shared/ui/Button/Button' import MessageIcon from '@/assets/icons/chat.svg' import CloseIcon from '@/assets/icons/IconMessageClose.svg' +import ContactCard from '@/entities/ContactCard/ContactCard' export type PropsContacts = { messenger: TMessenger[] @@ -24,7 +25,7 @@ const Contacts: FC = ({ messenger }) => { return (
-
+
    {messenger.map(item => ( @@ -32,7 +33,11 @@ const Contacts: FC = ({ messenger }) => {
- + {visible && } + + ) +} + +export default SearchProduct diff --git a/src/features/SubscribeForm/SubscribeForm.tsx b/src/features/SubscribeForm/SubscribeForm.tsx new file mode 100644 index 00000000..c581dd39 --- /dev/null +++ b/src/features/SubscribeForm/SubscribeForm.tsx @@ -0,0 +1,54 @@ +import { type FC, FormEvent } from 'react' +import SubscribeIcon from '@/assets/images/subscriptionForm/icon-subsc.svg' +import styles from './subscribeForm.module.scss' +import { Input, InputSize, InputTheme } from '@/shared/ui/Input/Input' +import classNames from 'classnames' + +type TSubscribeForm = { + type: 'footer' | 'subscribe' + className?: string + onSubmit: (event: FormEvent) => void +} + +// @TODO: Перевести форму на Formik + Yup +// https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/91 +/** + * @param {string} type - определяет внешний вид для компонентов footer и для subscribe + * @param {string} className - нужно будет, если захотят переиспользовать компонент + * @param {string} onSubmit - функция для обработки формы + */ +const SubscribeForm: FC = ({ type, onSubmit, className = '' }) => { + const classNameContainer = classNames(styles.container, { + [styles.container]: true, + [styles.container_footer]: type === 'footer', + [styles.container_subscribe]: type === 'subscribe' + }) + const classNameLabel = classNames({ + [styles.label]: true, + [styles.label_footer]: type === 'footer', + [styles.label_subscribe]: type === 'subscribe' + }) + const classNameForm = classNames({ + [styles.form]: true, + [styles.form_footer]: type === 'footer', + [styles.form_subscribe]: type === 'subscribe' + }) + + return ( +
+ {/* @TODO: Добавить компонент Label + https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/102 */} + +

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

+
+ + +
+
+ ) +} + +export default SubscribeForm diff --git a/src/features/SubscribeForm/subscribeForm.module.scss b/src/features/SubscribeForm/subscribeForm.module.scss new file mode 100644 index 00000000..b1097a5c --- /dev/null +++ b/src/features/SubscribeForm/subscribeForm.module.scss @@ -0,0 +1,105 @@ +@use '../../shared/styles/utils/variables' as var; +@use '../../shared/styles/utils/mixins' as media; + +.container { + display: flex; + flex-direction: column; + + &_footer { + width: 570px; + row-gap: 10px; + } + + &_subscribe { + width: 740px; + row-gap: 20px; + } +} + +.form { + height: 60px; + display: flex; + align-items: center; + justify-content: space-between; + border-radius: 5px; + position: relative; + + &:focus-within { + border: 2px solid var.$theme-primary-color; + } + + &_footer { + background-color: var.$footer-form-bg; + } + + &_subscribe { + background-color: var.$white; + } +} + +.button { + padding: 14px 21px; + display: flex; + align-items: center; + justify-content: center; + background-color: var.$theme-primary-color; + border: none; + box-sizing: border-box; + color: var.$white; + border-radius: 5px; + margin-right: 5px; + font-size: 16px; + font-weight: 500; + line-height: 19.2px; + transition: + border-color 0.25s, + color 0.25s, + background 0.25s; + + &__img { + width: 24px; + height: 24px; + margin-left: 27px; + margin-bottom: 2px; + + + @include media.respond-to('small') { + display: none; + } + } + + &:hover { + background-color: var.$theme-secondary-color; + } +} + +.label { + margin: 0; + padding: 0; + font-weight: 500; + font-style: normal; + + &_footer { + color: var.$white; + font-size: 16px; + font-weight: 500; + line-height: 16px; + } + + &_subscribe { + color: var.$black; + font-size: 18px; + line-height: 18px; + } +} + +.caption { + display: none; + + @include media.respond-to('large') { + display: flex; + font-size: 16px; + font-weight: 400; + line-height: 16px; + } +} diff --git a/src/mockData/cartData.ts b/src/mockData/cartData.ts new file mode 100644 index 00000000..79a6e404 --- /dev/null +++ b/src/mockData/cartData.ts @@ -0,0 +1,34 @@ +import { TProduct } from './productsData' + +// позже отправится в Redux +export type TCartItem = { + article: string + quantity: number +} +export type TCartItemExt = TCartItem & TProduct + +export type TCart = { + products: TCartItem[] + weight: number + unit: string +} + +export const cartData: TCart = { + products: [ + { + article: '1229239191', + quantity: 4 + }, + { + article: '1229239192', + quantity: 2 + }, + { + article: '1229239193', + quantity: 1 + } + ], + + weight: 30, + unit: 'кг' +} diff --git a/src/mockData/coreBaseData.ts b/src/mockData/coreBaseData.ts new file mode 100644 index 00000000..3e7ac7a0 --- /dev/null +++ b/src/mockData/coreBaseData.ts @@ -0,0 +1,54 @@ +export const coreBaseData = { + header: { + main_logo: { + image: 'https://maxboom.ru/image/catalog/Logo/IMG_5735.JPG', + url: '/', + title: 'Интернет-магазин «Maxboom.ru»' + }, + support: { + name: 'Обрантый звонок', + phone_number: '+7 977 848-02-28' + } + }, + footer: { + company_info: 'Интернет-магазин «Maxboom.ru»', + disclaimer: 'Мы не будем присылать вам спам. Только скидки и выгодные предложения', + support_work_time: 'Будни, с 10.00 до 20.00', + main_logo: { + image: 'https://maxboom.ru/image/catalog/Logo/IMG_5735.JPG', + url: '/', + title: 'Интернет-магазин «Maxboom.ru»' + }, + additional_logos: [ + { + image: 'https://maxboom.ru/image/catalog/demo-prostore/svg-icon/icon-payments-visa.svg', + url: '//visa.ru', + title: 'visa' + }, + { + image: 'https://maxboom.ru/image/catalog/demo-prostore/svg-icon/icon-payments-mastercard.svg', + url: '//mastercard.ru', + title: 'mastercard' + }, + { + image: 'https://maxboom.ru/image/catalog/demo-prostore/svg-icon/mir-logo-h14px.svg', + url: '//mironline.ru', + title: 'мир' + }, + { + image: 'https://maxboom.ru/image/catalog/demo-prostore/svg-icon/icons8-webmoney-25.svg', + url: '//webmoney.ru', + title: 'webmoney' + }, + { + image: 'https://maxboom.ru/image/catalog/demo-prostore/svg-icon/io.svg', + url: '//yoomoney.ru', + title: 'yoomoney' + } + ], + support: { + name: 'Обрантый звонок', + phone_number: '+7 977 848-02-28' + } + } +} diff --git a/src/mockData/productsData.ts b/src/mockData/productsData.ts new file mode 100644 index 00000000..bc6ac062 --- /dev/null +++ b/src/mockData/productsData.ts @@ -0,0 +1,33 @@ +import image1 from '@/assets/images/product/2-500x500.webp' + +export type TProduct = { + src: string + name: string + article: string // Unique identifier + price: number + currency: string +} + +export const productsData: TProduct[] = [ + { + src: image1, + name: 'Автомобильный переходник fjhfjbfffffffffffffffffffhj hfjhsdddddddddddd hfkjhsdfkhs jhdfkjhsd kfhs dfkjsh jdsfhsdfh hdfhsd1', + article: '1229239191', // Unique identifier + price: 781, + currency: '₽' + }, + { + src: image1, + name: 'Автомобильный переходник 2', + article: '1229239192', + price: 782, + currency: '₽' + }, + { + src: image1, + name: 'Автомобильный переходник 3', + article: '1229239193', + price: 783, + currency: '₽' + } +] diff --git a/src/pages/CartPage/CartPage.module.scss b/src/pages/CartPage/CartPage.module.scss index a97c1975..f753140e 100644 --- a/src/pages/CartPage/CartPage.module.scss +++ b/src/pages/CartPage/CartPage.module.scss @@ -1,3 +1,54 @@ +@use '../../shared/styles/utils/variables' as var; +@use '../../shared/styles/utils/mixins' as media; + +.container { + display: flex; + column-gap: 20px; + margin-bottom: 160px; + + @include media.respond-to('large') { + flex-direction: column; + row-gap: 20px; + width: 100%; + padding-left: 20px; + padding-right: 20px; + } +} + +.cards { + display: flex; + flex-direction: column; + row-gap: 10px; +} + +.wrapper { + display: flex; + flex-direction: column; + row-gap: 10px; +} + .heading { - align-self: flex-start; + margin-top: 20px; +} + +.titles { + display: flex; + flex-direction: column; + align-self: start; + padding-left: 30px; + row-gap: 10px; + margin-bottom: 20px; + margin-top: 25px; } + +.link { + text-decoration: none; + color: var.$subtitle-color; + font-size: 13.5px; + font-weight: 400; + line-height: 16.2px; + + &:hover { + color: var.$theme-primary-color; + } +} \ No newline at end of file diff --git a/src/pages/CartPage/CartPage.tsx b/src/pages/CartPage/CartPage.tsx index bf8c2f2d..2f4aeae1 100644 --- a/src/pages/CartPage/CartPage.tsx +++ b/src/pages/CartPage/CartPage.tsx @@ -1,16 +1,155 @@ +import { CartCouponApply } from '@/features/CartCouponApply/ui/CartCouponApply/CartCouponApply' +import { CartEdit } from '@/features/CartEdit/ui/CartEdit/CartEdit' +import { MakeOrder } from '@/widgets/MakeOrder/ui/MakeOrder/MakeOrder' +import styles from './CartPage.module.scss' import WrapperForMainContent from '@/components/WrapperForMainContent/WrapperForMainContent' -import Heading from '@/shared/ui/Heading/Heading' +import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' import Subheading from '@/shared/ui/Subheading/Subheading' -import styles from './CartPage.module.scss' +import { useEffect, useState } from 'react' +import { TCart, TCartItem, TCartItemExt, cartData } from '@/mockData/cartData' +import { getProduct } from '@/shared/api/maxboom/product' +import { Link } from 'react-router-dom' +import { TOrder } from '@/shared/model/types/common' -/** - * Страница с корзиной товаров - */ const CartPage = () => { + const [cart, updateCart] = useState(cartData) + const [cartWithProducts, updateCartWithProducts] = useState([]) + const [order, setOrder] = useState({ + amount: 0, + productsSum: 0, + currency: '₽', + productsSumWithoutDiscount: 0, + total: 0 + }) + + useEffect(() => { + const _products = cart.products.map(item => { + const product = getProduct(item.article)! + return { ...product, quantity: item.quantity } + }) + updateCartWithProducts(_products) + }, [cart]) + + useEffect(() => { + let amount = 0 + let productsSum = 0 + for (let i = 0; i < cartWithProducts.length; i = i + 1) { + amount += cartWithProducts[i].quantity + productsSum += cartWithProducts[i].quantity * cartWithProducts[i].price + } + + setOrder({ + ...order, + amount: amount, + productsSum: productsSum, + productsSumWithoutDiscount: productsSum, + total: productsSum + }) + }, [cartWithProducts]) + + function increaseQuantity(productArticle: string) { + const products = new Array() + + for (let i = 0; i < cart.products.length; i = i + 1) { + const item = cart.products[i] + if (item.article === productArticle) { + if (item.quantity < 99) { + item.quantity = item.quantity + 1 + products.push(item) + } + } else { + products.push(item) + } + } + + updateCart({ + ...cart, + products: products + }) + } + + function decreaseQuantity(productArticle: string) { + const products = new Array() + for (let i = 0; i < cart.products.length; i = i + 1) { + const item = cart.products[i] + if (item.article === productArticle) { + if (item.quantity > 1) { + item.quantity = item.quantity - 1 + } else { + item.quantity = 0 + } + } + products.push(item) + } + updateCart({ + ...cart, + products: products + }) + } + function setQuantity(productArticle: string, quantity: number) { + const products = new Array() + for (let i = 0; i < cart.products.length; i = i + 1) { + const item = cart.products[i] + if (item.article === productArticle) { + item.quantity = quantity + } + products.push(item) + } + updateCart({ + ...cart, + products: products + }) + } + + function removeProduct(productArticle: string) { + const products = new Array() + for (let i = 0; i < cart.products.length; i = i + 1) { + const item = cart.products[i] + if (item.article !== productArticle) { + products.push(item) + } + } + updateCart({ + ...cart, + products: products + }) + } + return ( - Корзина - В разработке +
+ + Оформление заказа ({cart.weight.toFixed(2)} {cart.unit}) + + + + Главная + {' '} + / Корзина покупок + + + Ваши покупки + +
+
+
+ {cartWithProducts.map(item => { + return ( + + ) + })} +
+
+ + +
+
) } diff --git a/src/pages/MainPage/MainPage.tsx b/src/pages/MainPage/MainPage.tsx index e459038a..b3d5a5ef 100644 --- a/src/pages/MainPage/MainPage.tsx +++ b/src/pages/MainPage/MainPage.tsx @@ -15,11 +15,12 @@ import { TEXT_CUSTOMERS_ABOUT_US, LINK_REVIEWS_ALL } from '@/shared/constants/constants' -import ContainerReviews from '@/components/ReviewsBlock/ReviewsBlock' import ArticleBlock from '@/components/ArticleBlock/ArticleBlock' 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 ( <> @@ -29,7 +30,7 @@ const MainPage = () => { - + diff --git a/src/pages/ProductsPage/ProductsPage.tsx b/src/pages/ProductsPage/ProductsPage.tsx index eef6c2a7..d3794ff9 100644 --- a/src/pages/ProductsPage/ProductsPage.tsx +++ b/src/pages/ProductsPage/ProductsPage.tsx @@ -20,8 +20,6 @@ import styles from './ProductsPage.module.scss' * Реализована пагинация. */ export const ProductsPage = () => { - const { id } = useParams() - console.log('Категория', id) const [cardView, setCardView] = useState(ECardView.GRID) const [currentPage, setCurrentPage] = useState(1) const [isModalOpen, setIsModalOpen] = useState(false) diff --git a/src/pages/RootPage/RootPage.tsx b/src/pages/RootPage/RootPage.tsx index 45d9302f..7f16f267 100644 --- a/src/pages/RootPage/RootPage.tsx +++ b/src/pages/RootPage/RootPage.tsx @@ -1,6 +1,6 @@ import { Outlet } from 'react-router' import Header from '@/components/header/header' -import Footer from '@/components/Footer/Footer' +import Footer from '@/widgets/Footer/Footer' import styles from './root.module.scss' import Contact from '../../features/Contacts/Contacts' import { messengerArray } from '@/shared/model/types/messengerArray' diff --git a/src/shared/api/maxboom/product.ts b/src/shared/api/maxboom/product.ts new file mode 100644 index 00000000..c4cb150b --- /dev/null +++ b/src/shared/api/maxboom/product.ts @@ -0,0 +1,9 @@ +import { productsData } from '@/mockData/productsData' + +export function getProduct(article: string) { + const products = productsData.filter(item => item.article === article) + if (products.length === 0) { + return null + } + return products[0] +} diff --git a/src/assets/images/footer/icon-subsc.svg b/src/shared/images/footer/icon-subsc.svg similarity index 100% rename from src/assets/images/footer/icon-subsc.svg rename to src/shared/images/footer/icon-subsc.svg diff --git a/src/assets/images/footer/io.svg b/src/shared/images/footer/io.svg similarity index 100% rename from src/assets/images/footer/io.svg rename to src/shared/images/footer/io.svg diff --git a/src/assets/images/footer/mastercard.svg b/src/shared/images/footer/mastercard.svg similarity index 100% rename from src/assets/images/footer/mastercard.svg rename to src/shared/images/footer/mastercard.svg diff --git a/src/assets/images/footer/mir.svg b/src/shared/images/footer/mir.svg similarity index 100% rename from src/assets/images/footer/mir.svg rename to src/shared/images/footer/mir.svg diff --git a/src/assets/images/footer/visa.svg b/src/shared/images/footer/visa.svg similarity index 100% rename from src/assets/images/footer/visa.svg rename to src/shared/images/footer/visa.svg diff --git a/src/assets/images/footer/webmoney.svg b/src/shared/images/footer/webmoney.svg similarity index 100% rename from src/assets/images/footer/webmoney.svg rename to src/shared/images/footer/webmoney.svg diff --git a/src/shared/model/types/common.ts b/src/shared/model/types/common.ts index 519068c3..8bab6046 100644 --- a/src/shared/model/types/common.ts +++ b/src/shared/model/types/common.ts @@ -19,3 +19,11 @@ export enum ECardView { LIST = 'list', COMPACT = 'compact' } + +export type TOrder = { + amount: number + productsSum: number + currency: string + productsSumWithoutDiscount: number + total: number +} diff --git a/src/shared/styles/utils/_variables.scss b/src/shared/styles/utils/_variables.scss index ae6dc8fd..ec658f55 100644 --- a/src/shared/styles/utils/_variables.scss +++ b/src/shared/styles/utils/_variables.scss @@ -21,3 +21,4 @@ $theme-purple-500: #1306a1; // исправить цвет $theme-purple-400: #1306a1; // Исправить цвет $product-label-hit: #800080; $product-avaliability-status: #13d154; +$light-grey: #F1F1F5; \ No newline at end of file diff --git a/src/shared/ui/ButtonDots/ButtonDots.module.scss b/src/shared/ui/ButtonDots/ButtonDots.module.scss new file mode 100644 index 00000000..11841132 --- /dev/null +++ b/src/shared/ui/ButtonDots/ButtonDots.module.scss @@ -0,0 +1,73 @@ +@use '../../../shared/styles/utils/variables' as var; +@use '../../../shared/styles/utils/mixins' as media; + + +.button { + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + padding: 20px; + border: none; + background-color: inherit; + position: relative; + right: 0; + top: 0; + border-radius: 10px; + z-index: 10px; +} + +.container { + position: relative; +} + +.wrapper { + position: absolute; + top: 40px; + right: -210px; + background: var.$white; + box-shadow: 0 0 20px rgba(170 189 206 /25%); + border-radius: 5px; + padding: 15px; + min-width: 228px; + transition: opacity 0.25s, visibility 0.25s; + z-index: 100; + + @include media.respond-to('large') { + top: 60px; + right: 0; + } +} + +.menu { + margin: 0; + padding: 0; + list-style: none; + white-space: nowrap; +} + +.line { + margin-top: 15px; + margin-bottom: 15px; + height: 1px; + width: 100%; + color: var.$body-color-light-grey; +} + +.item { + &:first-child { + border-bottom: 1px solid var.$body-color-light-grey; + margin-bottom: 15px; + padding-bottom: 15px; + } +} + +.menu_button { + font-size: 15px; + line-height: 1.2; + font-weight: 400; + + &:hover { + color: var.$theme-primary-color; + } +} \ No newline at end of file diff --git a/src/shared/ui/ButtonDots/ButtonDots.stories.tsx b/src/shared/ui/ButtonDots/ButtonDots.stories.tsx new file mode 100644 index 00000000..b77b1c7d --- /dev/null +++ b/src/shared/ui/ButtonDots/ButtonDots.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/react' +import ButtonDots from './ButtonDots' + +const meta = { + title: 'shared/ButtonDots', + component: ButtonDots, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + article: '01929039', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + removeProduct: (article: string) => {} + } +} diff --git a/src/shared/ui/ButtonDots/ButtonDots.tsx b/src/shared/ui/ButtonDots/ButtonDots.tsx new file mode 100644 index 00000000..850f21aa --- /dev/null +++ b/src/shared/ui/ButtonDots/ButtonDots.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useRef, useState } from 'react' +import styles from './ButtonDots.module.scss' + +type TProps = { + className?: string + article: string + removeProduct: (productArticle: string) => void +} + +/** + * @param {string} article - это номер артикула для понимая, с каким продуктом мы имеем дело + */ + +export const ButtonDots: React.FC = props => { + const [isMenuOpen, setIsMenuOpen] = useState(false) + const refContextMenu = useRef(null) + const refDotsButtton = useRef(null) + + useEffect(() => { + if (isMenuOpen) { + document.addEventListener('click', closeByOverlayHandler) + } + + return () => { + document.removeEventListener('click', closeByOverlayHandler) + } + }, [isMenuOpen]) + + function closeByOverlayHandler(ev: MouseEvent) { + const withinContextMenuBoundaries = refContextMenu.current!.contains(ev.target as Node) + const withinDotsButtonBoundaries = refDotsButtton.current!.contains(ev.target as Node) + + if (!withinContextMenuBoundaries && !withinDotsButtonBoundaries) { + setIsMenuOpen(false) + } + } + + function deleteProductHandler() { + setIsMenuOpen(false) + props.removeProduct(props.article) + } + function addToFavoritesHandler() { + setIsMenuOpen(false) + // add an action for this button + } + + const onClickDotsHandler = () => { + if (!isMenuOpen) { + setIsMenuOpen(true) + } + } + + return ( +
+
+ + {isMenuOpen && ( +
+
    +
  • + +
  • + +
  • + +
  • +
+
+ )} +
+
+ ) +} +export default ButtonDots diff --git a/src/shared/ui/Heading/Heading.stories.tsx b/src/shared/ui/Heading/Heading.stories.tsx index 9c84dd48..c0eca174 100644 --- a/src/shared/ui/Heading/Heading.stories.tsx +++ b/src/shared/ui/Heading/Heading.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react' import Heading, { HeadingType } from './Heading' const meta = { - title: 'Heading', + title: 'shared/Heading', component: Heading, parameters: { layout: 'centered' diff --git a/src/shared/ui/Paragraph/Paragraph.stories.tsx b/src/shared/ui/Paragraph/Paragraph.stories.tsx index ac7d2c3f..a9472858 100644 --- a/src/shared/ui/Paragraph/Paragraph.stories.tsx +++ b/src/shared/ui/Paragraph/Paragraph.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react' import Paragraph, { ParagraphTheme } from './Paragraph' const meta = { - title: 'Paragraph', + title: 'shared/Paragraph', component: Paragraph, parameters: { layout: 'centered' diff --git a/src/shared/ui/Subheading/Subheading.stories.tsx b/src/shared/ui/Subheading/Subheading.stories.tsx index bf05731c..c3aa132b 100644 --- a/src/shared/ui/Subheading/Subheading.stories.tsx +++ b/src/shared/ui/Subheading/Subheading.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react' import Subheading from './Subheading' const meta = { - title: 'Subheading', + title: 'shared/Subheading', component: Subheading, parameters: { layout: 'centered' diff --git a/src/shared/ui/logo/Logo.tsx b/src/shared/ui/logo/Logo.tsx new file mode 100644 index 00000000..f7e9eea7 --- /dev/null +++ b/src/shared/ui/logo/Logo.tsx @@ -0,0 +1,30 @@ +import { type FC } from 'react' +import styles from './logo.module.scss' +import Link from '@/shared/ui/Link/Link' + +type TLogoProps = { + image: string + title: string + url: string + width: string + height: string +} +/** + * @param {string} image - путь к логотипу на сервере + * @param {string} title - лейбл логотипа (и альт) + * @param {string} url - путь ссылки по нажатию на логотип + * @param {string} width - ширина логотипа + * @param {string} height - высота логотипа + */ + +const Logo: FC = ({ image, title, url, width, height }) => { + return ( +
+ + {title} + +
+ ) +} + +export default Logo diff --git a/src/components/logo/logo.module.scss b/src/shared/ui/logo/logo.module.scss similarity index 100% rename from src/components/logo/logo.module.scss rename to src/shared/ui/logo/logo.module.scss diff --git a/src/widgets/Footer/Footer.tsx b/src/widgets/Footer/Footer.tsx new file mode 100644 index 00000000..75bc7485 --- /dev/null +++ b/src/widgets/Footer/Footer.tsx @@ -0,0 +1,62 @@ +import { coreBaseData } from '@/mockData/coreBaseData' +import Logo from '@/shared/ui/logo/Logo' +import Link from '@/shared/ui/Link/Link' +import SubscribeForm from '@/features/SubscribeForm/SubscribeForm' +import styles from './footer.module.scss' +import Payments from '@/entities/Payments/Payments' + +function Footer() { + const onSubmitHandler = () => {} + return ( +
+
+
+
+ +

{coreBaseData.footer.company_info}

+
+
+ +
+
+

Поддержка

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

{coreBaseData.footer.support_work_time}

+
+
+
+
+
+

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

+ +
+
+
+
+ ) +} + +export default Footer diff --git a/src/components/Footer/footer.module.scss b/src/widgets/Footer/footer.module.scss similarity index 83% rename from src/components/Footer/footer.module.scss rename to src/widgets/Footer/footer.module.scss index 5b90d1c4..cb37481c 100644 --- a/src/components/Footer/footer.module.scss +++ b/src/widgets/Footer/footer.module.scss @@ -1,4 +1,4 @@ -@use '../../shared/styles/utils/variables' as var; +@use '/src/shared/styles/utils/variables' as var; .footer { background-color: var.$footer-bg; @@ -99,10 +99,6 @@ } } - &__payment-link { - text-decoration: none; - } - &__hours { margin: 0; padding: 0; @@ -149,35 +145,6 @@ } } - &__payments { - margin: 0; - padding: 0; - display: flex; - column-gap: 3px; - list-style: none; - margin-right: 8px; - } - - &__payment-item { - display: flex; - width: 34px; - height: 24px; - justify-content: center; - align-items: center; - border-radius: 4px; - background: var.$bg-no-image; - } - - &__payment-nav { - margin: 0; - padding: 0; - list-style: none; - display: flex; - align-items: center; - justify-content: center; - column-gap: 3px; - } - &__nav { margin: 0; padding: 0; diff --git a/src/widgets/MakeOrder/ui/MakeOrder/MakeOrder.module.scss b/src/widgets/MakeOrder/ui/MakeOrder/MakeOrder.module.scss new file mode 100644 index 00000000..3ad9205f --- /dev/null +++ b/src/widgets/MakeOrder/ui/MakeOrder/MakeOrder.module.scss @@ -0,0 +1,123 @@ +@use '../../../../shared/styles/utils/variables' as var; +@use '../../../../shared/styles/utils/mixins' as media; + + +.container { + display: flex; + flex-direction: column; + padding: 40px; + background-color: var.$white; + width: 460px; + border-radius: 5px; + + @include media.respond-to('large') { + width: 100% + } +} + +.title { + margin: 0 0 20px; + font-size: 18px; + line-height: 1.2; + font-weight: 600; +} + +.table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + margin: 0; + font-size: 16px; + line-height: 1.25; + font-weight: 400; + margin-bottom: 20px; +} + +.table td { + padding-top: 5px; +} + +.table th { + padding-top: 25px; + +} + +.total { + text-align: left; +} + + +.mark { + background: none; + color: var.$body-color-light-grey; + padding: 2px; +} + +.right { + text-align: right +} + +.paragraph { + margin: 0; + font-size: 14px; + line-height: 1.43; + font-weight: 400; + color: var.$body-color-light-grey; +} + +.link_order { + display: flex; + align-items: center; + justify-content: center; + background-color: var.$theme-primary-color; + color: var.$white; + width: 100%; + height: 58px; + border: 1px solid transparent; + border-radius: 5px; + font-size: 16px; + line-height: 1.2; + font-weight: 700; + text-align: center; + text-decoration: none; + transition: border-color 0.25s, color 0.25s, background 0.25s; + appearance: none; + + &:hover { + opacity: 0.8; + } + +} + +.line { + width: 100%; + height: 0.5px; + background-color: var.$light-grey; + margin-top: 40px; + margin-bottom: 25px; +} + +.quick_order { + display: flex; + align-items: center; + justify-content: center; + background-color: var.$light-grey; + color: var.$black; + width: 100%; + height: 48px; + border: 1px solid transparent; + border-radius: 5px; + font-size: 16px; + line-height: 1.2; + font-weight: 700; + text-align: center; + text-decoration: none; + transition: border-color 0.25s, color 0.25s, background 0.25s; + appearance: none; + margin-bottom: 15px; + + &:hover { + background-color: var.$theme-primary-color; + color: var.$white; + } +} \ No newline at end of file diff --git a/src/widgets/MakeOrder/ui/MakeOrder/MakeOrder.tsx b/src/widgets/MakeOrder/ui/MakeOrder/MakeOrder.tsx new file mode 100644 index 00000000..d4389322 --- /dev/null +++ b/src/widgets/MakeOrder/ui/MakeOrder/MakeOrder.tsx @@ -0,0 +1,42 @@ +import { Link } from 'react-router-dom' +import styles from './MakeOrder.module.scss' +import { TOrder } from '@/shared/model/types/common' + + +export const MakeOrder: React.FC = (props: TOrder) => { + return ( +
+

О заказе

+ + + + + + + + + + + + + + + + + +
+ Товары {props.amount} + {`${props.productsSum} ${props.currency}`}
Сумма{`${props.productsSumWithoutDiscount} ${props.currency}`}
Итого{`${props.total} ${props.currency}`}
+ + + Оформление заказа + + +
+ + + +

Наш менеджер перезвонит вам и оформит заказ за вас

+
+ ) +} diff --git a/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.stories.tsx b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.stories.tsx new file mode 100644 index 00000000..82c25356 --- /dev/null +++ b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.stories.tsx @@ -0,0 +1,24 @@ +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 = { + title: 'widgets/ReviewsBlock', + component: ReviewsBlock, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + title: TEXT_CUSTOMERS_ABOUT_US, + reviews: reviewsData, + linkText: LINK_REVIEWS_ALL + } +} diff --git a/src/components/ReviewsBlock/ReviewsBlock.tsx b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx similarity index 86% rename from src/components/ReviewsBlock/ReviewsBlock.tsx rename to src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx index 2eb5291f..fa75e9ce 100644 --- a/src/components/ReviewsBlock/ReviewsBlock.tsx +++ b/src/widgets/ReviewsBlock/ui/ReviewsBlock/ReviewsBlock.tsx @@ -1,11 +1,11 @@ -import { FC } from 'react' -import CardReview from '../CardReview/CardReview' +import { type FC } from 'react' import { TReview } from '@/models/ReviewModel' 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 styles from './reviewsBlock.module.scss' +import CardReview from '@/entities/CardReview/ui/CardReview/CardReview' export type Props = { title: string @@ -30,7 +30,7 @@ const ReviewsBlock: FC = props => {
{title} - + иконка diff --git a/src/widgets/ReviewsBlock/ui/ReviewsBlock/reviewsBlock.module.scss b/src/widgets/ReviewsBlock/ui/ReviewsBlock/reviewsBlock.module.scss new file mode 100644 index 00000000..d8e4ec64 --- /dev/null +++ b/src/widgets/ReviewsBlock/ui/ReviewsBlock/reviewsBlock.module.scss @@ -0,0 +1,59 @@ +@use '../../../../shared/styles/utils/variables' as color; + +.wrapper { + width: 100%; + margin: 0 auto; + + h2 { + font-size: #{'min(max(18px, 1.6vw), 20px)'}; + line-height: 115%; + font-weight: 500; + } + + .header { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin: 0 25px; + } + + .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; + max-width: 100%; + padding: 20px 25px; + overflow: auto hidden; + cursor: grab; + + &::-webkit-scrollbar { + height: 3px; + } + + &::-webkit-scrollbar-thumb { + background: color.$theme-primary-color; + cursor: grab; + } + + &::-webkit-scrollbar-track { + margin-left: 25px; + margin-right: 25px; + } + } +} \ No newline at end of file diff --git a/src/components/searchResult/searchResult.module.scss b/src/widgets/SearchResult/SearchResult.module.scss similarity index 84% rename from src/components/searchResult/searchResult.module.scss rename to src/widgets/SearchResult/SearchResult.module.scss index 61832a32..ef545279 100644 --- a/src/components/searchResult/searchResult.module.scss +++ b/src/widgets/SearchResult/SearchResult.module.scss @@ -1,6 +1,8 @@ @use '@/shared/styles/utils/variables' as var; .result { + // @TODO: Определить ширину в соответствии с шириной формы поиска + // https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/174 width: 800px; background-color: var.$white; position: absolute; diff --git a/src/widgets/SearchResult/SearchResult.stories.tsx b/src/widgets/SearchResult/SearchResult.stories.tsx new file mode 100644 index 00000000..716aaec1 --- /dev/null +++ b/src/widgets/SearchResult/SearchResult.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, Story } from '@storybook/react' +import SearchResult from './SearchResult' +import { searchResponseData } from '@/mockData/searchData' + +export default { + title: 'Widgets/SearchResult', + component: SearchResult +} as Meta + +const Template: Story = () => { + return +} + +export const Default = Template.bind({}) +Default.args = {} diff --git a/src/components/searchResult/searchResult.tsx b/src/widgets/SearchResult/SearchResult.tsx similarity index 63% rename from src/components/searchResult/searchResult.tsx rename to src/widgets/SearchResult/SearchResult.tsx index c7e21b43..d5163210 100644 --- a/src/components/searchResult/searchResult.tsx +++ b/src/widgets/SearchResult/SearchResult.tsx @@ -1,30 +1,32 @@ -import { FC } from 'react' -import SearchItem from '../SearchItem/SearchItem' +import { forwardRef } from 'react' +import SearchItem from '@/entities/SearchItem/SearchItem' import { SEARCH_CATEGORY, SEARCH_PRODUCT } from '@/shared/constants/constants' import type { TProduct } from '@/shared/model/types/common' import { TCategory } from '@/models/CategoryModel' import SearchIcon from '@/assets/images/search/search-icon.svg' import Link from '@/shared/ui/Link/Link' -import styles from './searchResult.module.scss' import { Routes } from '@/shared/config/routerConfig/routes' +import styles from './SearchResult.module.scss' type TProps = { results: Array } /** - * @param {string} results - массив поисковой выдачи + * Компонент тултипа-подсказки при вводе поискового запроса + * @param {Array} props.results - подсказка, получаемая с бэка, при вводе текста в поисковую строку; + * @param {React.MutableRefObject} ref - реф из компонента SearchProduct; */ -const SearchResult: FC = ({ results }) => { +const SearchResult = forwardRef(({ results }, ref) => { return ( -
+
    {results.map((item, index) => { if (item.type === SEARCH_CATEGORY) { return (
  • - +

    {item.name}

    Категория @@ -39,9 +41,11 @@ const SearchResult: FC = ({ results }) => {
  • ) } + + return null })} -
  • +
  • Показать все товары @@ -49,6 +53,8 @@ const SearchResult: FC = ({ results }) => {
) -} +}) + +SearchResult.displayName = 'SearchResult' export default SearchResult