diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..22ead72b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +## Описание + +Что делает этот Pull Request? + +## Ссылка на задачу + +[Название задачи](ссылка_на_задачу) diff --git a/.github/workflows/good_food_frontend_workflow.yaml b/.github/workflows/good_food_frontend_workflow.yaml index a656dbbd..e2d65e68 100644 --- a/.github/workflows/good_food_frontend_workflow.yaml +++ b/.github/workflows/good_food_frontend_workflow.yaml @@ -8,7 +8,19 @@ jobs: check_codestyle: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 + - name: send start message + uses: appleboy/telegram-action@master + with: + to: ${{ secrets.TELEGRAM_TO }} + token: ${{ secrets.TELEGRAM_TOKEN }} + message: | + ${{ github.workflow }} started! + + Repository: ${{ github.repository }}. + Branch name: ${{ github.ref_name }}. + Commit author: ${{ github.actor }}. + Commit message: ${{ github.event.commits[0].message }}. - name: Use Node.js uses: actions/setup-node@v2 with: @@ -20,7 +32,7 @@ jobs: npm run lint:styles npm run lint:prettier - name: send message - if: ${{ github.ref != 'refs/heads/develop' || github.ref != 'refs/heads/main' }} + if: ${{ github.ref != 'refs/heads/main' }} uses: appleboy/telegram-action@master with: to: ${{ secrets.TELEGRAM_TO }} @@ -37,7 +49,7 @@ jobs: build_and_push_to_docker_hub: name: Push Docker image to Docker Hub runs-on: ubuntu-latest - if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' steps: - name: Check out the repo diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/package.json b/package.json index add4f68f..776605f6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "tsc && vite build", "lint:js": "eslint . --ext ts,tsx,js,jsx --report-unused-disable-directives --cache --max-warnings 0", "lint:js:fix": "npm run lint:js -- --fix", diff --git a/src/App.tsx b/src/App.tsx index 63ed8053..36706682 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,15 +22,9 @@ import NotFound from '@pages/not_found/not-found.tsx'; import { CartProvider } from '@contexts/cart-context.tsx'; import RecipeList from '@pages/recipe-list/index.tsx'; import Agreement from '@pages/agreement/index.tsx'; - -// импорт временных массивов для отображения каталогов и продуктов -// временное решение для верстки, потом удалить - -// import { mainPageBlockLinks, products } from './data/dataExamples.ts'; - -// примеры рендера каталогов -// -// +import DeliveryConditions from '@pages/delivery-conditions/index.tsx'; +import CheckoutSuccess from '@pages/checkout/checkout-success/index.tsx'; +import PaymentResults from '@pages/payment-results/index.tsx'; function App() { const { isLoggedIn } = useAuth(); @@ -46,6 +40,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> @@ -59,6 +54,16 @@ function App() { } /> } /> + } /> + } + /> + } + /> + } /> } /> diff --git a/src/assets/images/car-alt-min.svg b/src/assets/images/car-alt-min.svg new file mode 100644 index 00000000..207da658 --- /dev/null +++ b/src/assets/images/car-alt-min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/cart-add-min.svg b/src/assets/images/cart-add-min.svg new file mode 100644 index 00000000..0964f5bd --- /dev/null +++ b/src/assets/images/cart-add-min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/chevron-right-no-stroke.svg b/src/assets/images/chevron-right-no-stroke.svg new file mode 100644 index 00000000..bea4b50d --- /dev/null +++ b/src/assets/images/chevron-right-no-stroke.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/circle-not-ok.svg b/src/assets/images/circle-not-ok.svg new file mode 100644 index 00000000..eed08ad3 --- /dev/null +++ b/src/assets/images/circle-not-ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/circle-ok-min.svg b/src/assets/images/circle-ok-min.svg new file mode 100644 index 00000000..8e83b22d --- /dev/null +++ b/src/assets/images/circle-ok-min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/home-alt-min.svg b/src/assets/images/home-alt-min.svg new file mode 100644 index 00000000..fb437323 --- /dev/null +++ b/src/assets/images/home-alt-min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/profile/check-status.svg b/src/assets/images/profile/check-status.svg new file mode 100644 index 00000000..5144adb7 --- /dev/null +++ b/src/assets/images/profile/check-status.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/spacer-min.svg b/src/assets/images/spacer-min.svg new file mode 100644 index 00000000..aedb17de --- /dev/null +++ b/src/assets/images/spacer-min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/star-rating-filled.svg b/src/assets/images/star-rating-filled.svg new file mode 100644 index 00000000..f661d524 --- /dev/null +++ b/src/assets/images/star-rating-filled.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/images/star-rating-outlined.svg b/src/assets/images/star-rating-outlined.svg new file mode 100644 index 00000000..1086969e --- /dev/null +++ b/src/assets/images/star-rating-outlined.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/images/vspacer-min.svg b/src/assets/images/vspacer-min.svg new file mode 100644 index 00000000..a0001db8 --- /dev/null +++ b/src/assets/images/vspacer-min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Button/button.module.scss b/src/components/Button/button.module.scss index 38d985dc..75f6be7d 100644 --- a/src/components/Button/button.module.scss +++ b/src/components/Button/button.module.scss @@ -14,7 +14,7 @@ color: #fff; font-family: $ubuntu-font; font-size: 14px; - font-weight: 600; + font-weight: 400; line-height: 20px; cursor: pointer; @@ -25,8 +25,8 @@ transition: 0.5s; &:disabled { - background-color: black; - cursor: not-allowed; + background-color: $accent-color-lightest-green; + cursor: initial; } } @@ -43,7 +43,7 @@ color: #fff; font-family: $ubuntu-font; font-size: 14px; - font-weight: 600; + font-weight: 400; line-height: 20px; cursor: pointer; } @@ -135,6 +135,7 @@ width: 100%; height: 50px; cursor: pointer; + white-space: nowrap; &:not(.green-border-button__active):hover { color: white; @@ -167,6 +168,7 @@ width: 100%; height: 50px; transition: 0.5s; + white-space: nowrap; @media screen and (width <= 768px) { font-size: 13px; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 1f3791ec..da9e54e8 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -1,10 +1,14 @@ import clsx from 'clsx'; import styles from './button.module.scss'; -type ButtonProps = { +export type ButtonProps = { buttonText: string; - buttonStyle: string; - classNameActive?: string; + buttonStyle: + | 'green-border-button' + | 'green-border-button__active' + | 'greenish-button' + | 'green-button'; + classNameActive?: 'greenish-button__active' | ''; onClick?: () => void; disabled?: boolean; classNames?: string; diff --git a/src/components/breadcrumbs/index.tsx b/src/components/breadcrumbs/index.tsx index 6c677d2c..55e91ba8 100644 --- a/src/components/breadcrumbs/index.tsx +++ b/src/components/breadcrumbs/index.tsx @@ -32,6 +32,7 @@ const Breadcrumbs: React.FC = ({ productName, category }) => { recipes: 'Рецепты', contacts: 'Контакты', orders: 'Мои заказы', + order: 'Оформление заказа', }; if (category) { diff --git a/src/components/card-block-link/card-block-link.module.scss b/src/components/card-block-link/card-block-link.module.scss deleted file mode 100644 index 0c084173..00000000 --- a/src/components/card-block-link/card-block-link.module.scss +++ /dev/null @@ -1,53 +0,0 @@ -.card-block-link { - margin: 0; - width: 100%; - height: 200px; - background-color: gray; - border-radius: 16px; - transition: 0.2s; - overflow: hidden; - background-position: center; - background-repeat: no-repeat; - background-size: cover; - box-sizing: border-box; - display: inline-block; - text-decoration: none; - color: #fff; - - @media screen and (width <= 768px) { - height: 97px; - } - - &:hover { - cursor: pointer; - opacity: 0.7; - } -} - -.card-block-link__title { - margin: 0; - padding: 10px; - text-align: center; - max-width: 150px; - font-family: Ubuntu, Arial, sans-serif; - font-size: 24px; - font-style: normal; - font-weight: 700; - line-height: 140%; - box-sizing: border-box; - text-wrap: wrap; - position: relative; - - @media screen and (width <= 768px) { - max-width: 100px; - font-size: 15px; - } -} - -.card-block-link__title-wrapper { - display: flex; - align-items: center; - justify-content: center; - background-color: rgb(0 0 0 / 48.6%); - height: 100%; -} diff --git a/src/components/card-block-link/index.tsx b/src/components/card-block-link/index.tsx deleted file mode 100644 index 279b13da..00000000 --- a/src/components/card-block-link/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import styles from './card-block-link.module.scss'; -import { Link } from 'react-router-dom'; - -type CardBlockLinkProps = { - title: string; - link: string; - backgroundImage: string | undefined; -}; - -function CardBlockLink({ title, link, backgroundImage }: CardBlockLinkProps) { - return ( - -
-

{title}

-
- - ); -} - -export default CardBlockLink; diff --git a/src/components/card-catalog-link/card-catalog-link.module.scss b/src/components/card-catalog-link/card-catalog-link.module.scss index be7e7088..76e6fed3 100644 --- a/src/components/card-catalog-link/card-catalog-link.module.scss +++ b/src/components/card-catalog-link/card-catalog-link.module.scss @@ -19,33 +19,10 @@ } } -.card-catalog-link__title-container_type_bento-grid { - margin-bottom: 28px; -} - .card-catalog-link__title-container_type_single-row { margin-bottom: 16px; } -.card-catalog-link__title { - margin: 0; - color: #1a1a1a; - - @media screen and (width <= 768px) { - font-size: 24px; - } -} - -.card-catalog-link__arrow { - width: 20px; - height: 32px; - margin-top: auto; - background-position: center; - background-size: cover; - background-repeat: no-repeat; - background-image: url('@images/chevron-right.svg'); -} - .card-catalog-link__list-single { margin: 0; padding: 0; @@ -76,30 +53,6 @@ } } -.card-catalog-link__list-bento { - margin: 0; - padding: 0; - list-style: none; - gap: 20px; - display: grid; - grid-template-areas: - 'a a b b c c' - 'd d d e e e'; - margin-bottom: 13px; - - @media screen and (width <= 768px) { - grid-template-areas: - 'a b' - 'c d' - 'e .'; - gap: 4px; - } - - &:last-of-type { - margin-bottom: 0; - } -} - .card-catalog-link__list-item { margin: 0; padding: 0; diff --git a/src/components/card-catalog-link/index.tsx b/src/components/card-catalog-link/index.tsx index 9ea137f3..664a0482 100644 --- a/src/components/card-catalog-link/index.tsx +++ b/src/components/card-catalog-link/index.tsx @@ -1,16 +1,15 @@ /* eslint-disable */ -import CardBlockLink from '../card-block-link'; import clsx from 'clsx'; import ProductCard from '../product-card'; +import TitleArrowLink from '@components/title-arrow-link'; import styles from './card-catalog-link.module.scss'; -import { Link } from 'react-router-dom'; import { Product } from '@services/generated-api/data-contracts'; type CardCatalogLinkProps = { title: string; category?: string; array: Product[] | { title: string; link: string; backgroundImage: string }[]; - type: 'bento-grid' | 'single-row'; + type: 'single-row'; }; function CardCatalogLink({ title, array, type, category }: CardCatalogLinkProps) { @@ -21,18 +20,11 @@ function CardCatalogLink({ title, array, type, category }: CardCatalogLinkProps) styles[`card-catalog-link__title-container_type_${type}`] }`} > - -

{title}

- - +
    {array.map((item: Record, index: number) => ( @@ -41,17 +33,10 @@ function CardCatalogLink({ title, array, type, category }: CardCatalogLinkProps) key={index} style={{ gridArea: item.gridArea }} > - {type === 'bento-grid' && ( - - )} {type === 'single-row' && ( { + const [categories, setCategories] = useState([]); + const [numberToShow, setNumberToShow] = useState(6); + const { isMobileScreen } = useProfile(); + + const findNumbersToShow = useCallback(() => { + if (isMobileScreen) { + setNumberToShow(6); + return; + } + + setNumberToShow(5); + }, [isMobileScreen]); + + useEffect(() => { + findNumbersToShow(); + }, [findNumbersToShow, isMobileScreen]); + + const handleCategoryToggle = () => { + if (numberToShow < 12) { + setNumberToShow(12); + return; + } + findNumbersToShow(); + }; + + useEffect(() => { + api.categoriesList().then((categories) => setCategories(categories)); + }, []); + return ( + <> +
    + +
    +
      + {categories.slice(0, numberToShow).map((category) => ( +
    • + +
      + {`Категория: +
      + {category.name} +
      +
      + +
    • + ))} +
    +
    +

    + {numberToShow < 12 ? 'Развернуть' : 'Свернуть'} +

    + +
    + + ); +}; + +export default CatalogPromo; diff --git a/src/components/custom-nav-link/index.tsx b/src/components/custom-nav-link/index.tsx index a26ee13f..009c383c 100644 --- a/src/components/custom-nav-link/index.tsx +++ b/src/components/custom-nav-link/index.tsx @@ -17,7 +17,8 @@ const CustomNavLink: React.FC = ({ onClick, }) => { const location = useLocation(); - const isActive = location.pathname === to; + const pathnameWithHash = location.pathname.concat(location.hash); + const isActive = pathnameWithHash === to; return (

    углеводы - {`${carbohydrates}г`} + {`${carbonhydrates}г`}

    ккал diff --git a/src/components/making-order-btn/index.tsx b/src/components/making-order-btn/index.tsx index 2627ae28..d3d06043 100644 --- a/src/components/making-order-btn/index.tsx +++ b/src/components/making-order-btn/index.tsx @@ -1,15 +1,19 @@ import React from 'react'; import styles from './making-order-btn.module.scss'; -import Button from '@components/Button'; +import Button from '@components/button'; +import type { ButtonProps } from '@components/button'; -interface MakingOrderBtnProps { - onClick?: () => void; -} +interface MakingOrderBtnProps extends Pick {} -const MakingOrderBtn: React.FC = ({ onClick }) => { +const MakingOrderBtn: React.FC = ({ onClick, disabled }) => { return (

    - + {!isLoggedIn && ( + + )}
      Каталог - + + Корзина + + О нас - + Товары недели - + Рецепты - + Контакты
    diff --git a/src/components/navigation-bar/navigation-bar.module.scss b/src/components/navigation-bar/navigation-bar.module.scss index 49869906..7c682eb6 100644 --- a/src/components/navigation-bar/navigation-bar.module.scss +++ b/src/components/navigation-bar/navigation-bar.module.scss @@ -8,20 +8,15 @@ width: 100%; background-color: #fff; padding: 0; - display: flex; + display: none; justify-content: space-between; align-items: center; - z-index: 1000; + z-index: 150; transition: background-color 0.7s; -} -.burger-icon { - background: none; - border: none; - font-size: 24px; - color: #fff; - cursor: pointer; - transition: color 0.7s; + @media screen and (width <= 768px) { + display: flex; + } } .navigation-bar__nav { @@ -31,10 +26,9 @@ width: 100%; overflow: hidden; max-height: 0; - transition: max-height 0.7s ease-out; + transition: max-height 0.4s ease-out; border-radius: 0 0 8px 8px; - background: #f0f0f0; - box-shadow: 0 1px 1px 0 rgb(97 99 96 / 30%); + box-shadow: 0 10px 10px 0 rgb(97 99 96 / 50%); } .navigation-bar__button { @@ -53,7 +47,7 @@ pointer-events: auto; transition: 0.3s; cursor: pointer; - margin-top: 28px; + margin: 20px 0 30px; font-size: 13px; font-weight: 400; line-height: 140%; @@ -68,7 +62,7 @@ align-items: center; flex-direction: column; list-style: none; - margin: 26px 0; + margin: 0; width: 100%; padding: 0 20px; box-sizing: border-box; @@ -78,11 +72,11 @@ position: relative; display: flex; justify-content: center; - align-items: flex-start; + align-items: center; text-decoration: none; padding: 10px; height: 48px; - transition: background-color 0.7s; + transition: background-color 0.4s; width: 100%; box-sizing: border-box; color: var(--active, #1a1a1a); @@ -91,10 +85,6 @@ font-style: normal; font-weight: 400; line-height: 140%; - - &:not(:first-child) { - margin-top: 10px; - } } .navigation-bar__link::after { @@ -107,7 +97,11 @@ background: var(--inactive, #b8b8b8); } +.navigation-bar__link:last-of-type::after { + width: 0; +} + .navigation-bar__link_active { color: $green-primary-700; - background-color: rgb(108 108 108 / 25%); + background-color: $background-color-input-field; } diff --git a/src/components/navigation-icons/index.tsx b/src/components/navigation-icons/index.tsx index 6077540b..5a504c00 100644 --- a/src/components/navigation-icons/index.tsx +++ b/src/components/navigation-icons/index.tsx @@ -3,10 +3,12 @@ import { Link } from 'react-router-dom'; import { useAuth } from '@hooks/use-auth.ts'; import styles from './navigation-icons.module.scss'; import { usePopup } from '@hooks/use-popup.ts'; +import { useCart } from '@hooks/use-cart-context'; const NavigationIcons: React.FC = () => { const { isLoggedIn } = useAuth(); const { handleOpenPopup } = usePopup(); + const { cartData } = useCart(); return (
    @@ -20,6 +22,7 @@ const NavigationIcons: React.FC = () => { {
    + ); +}; + +export default PaymentButton; diff --git a/src/components/payment-button/payment-button.module.scss b/src/components/payment-button/payment-button.module.scss new file mode 100644 index 00000000..56e2afd2 --- /dev/null +++ b/src/components/payment-button/payment-button.module.scss @@ -0,0 +1,28 @@ +@use '@scss/variables' as *; + +.errorText { + padding-top: 10px; + min-height: 25px; + text-align: center; + color: $error-color; + font-size: 13px; + font-weight: 300; + line-height: 140%; + + @media screen and (width <= 768px) { + font-size: 12px; + padding-top: 3px; + min-height: 17px; + } +} + +.green-button__type_narrow { + max-width: 140px; +} + +.buttonContainer { + display: flex; + flex-direction: column; + justify-content: center; + width: max-content; +} diff --git a/src/components/payment/index.tsx b/src/components/payment/index.tsx new file mode 100644 index 00000000..f9f2703f --- /dev/null +++ b/src/components/payment/index.tsx @@ -0,0 +1,22 @@ +import React, { ReactNode } from 'react'; +import OurBlock from '@components/our-block'; +import styles from './payment.module.scss'; + +type PaymentProps = { + children?: ReactNode; + adviceText: string; +}; + +const Payment: React.FC = ({ children, adviceText }) => { + return ( +
    + {children} +
    +

    {adviceText}

    + +
    +
    + ); +}; + +export default Payment; diff --git a/src/components/payment/payment.module.scss b/src/components/payment/payment.module.scss new file mode 100644 index 00000000..66982e7e --- /dev/null +++ b/src/components/payment/payment.module.scss @@ -0,0 +1,33 @@ +@use '@scss/variables' as *; + +.payment { + display: flex; + flex-direction: column; + font-family: $ubuntu-font; +} + +.payment__ourBlock { + padding: 100px 128px 0; + max-width: 1024px; + + @media screen and (width <= 768px) { + padding: 40px 20px 0; + } + + @media screen and (width <= 550px) { + padding-right: 0; + } +} + +.payment__advice { + padding: 0 0 44px; + margin: 0; + font-size: 30px; + font-weight: 700; + line-height: 140%; + + @media screen and (width <= 768px) { + padding: 0 0 16px; + font-size: 18px; + } +} diff --git a/src/components/popups/popup-checkout-response/index.tsx b/src/components/popups/popup-checkout-response/index.tsx new file mode 100644 index 00000000..333ee930 --- /dev/null +++ b/src/components/popups/popup-checkout-response/index.tsx @@ -0,0 +1,24 @@ +import { usePopup } from '@hooks/use-popup'; +import styles from './popup-checkout-response.module.scss'; +import Popup from '@components/popup'; + +type PopupCheckoutResponseProps = { + text: string; + handleClose: () => void; +}; + +const PopupCheckoutResponse: React.FC = ({ + text, + handleClose, +}) => { + const { popupState } = usePopup(); + return ( + +
    +

    {text}

    +
    +
    + ); +}; + +export default PopupCheckoutResponse; diff --git a/src/components/popups/popup-checkout-response/popup-checkout-response.module.scss b/src/components/popups/popup-checkout-response/popup-checkout-response.module.scss new file mode 100644 index 00000000..7e73327f --- /dev/null +++ b/src/components/popups/popup-checkout-response/popup-checkout-response.module.scss @@ -0,0 +1,26 @@ +@use '@scss/variables' as *; + +.textContainer { + display: flex; + justify-content: center; + align-items: center; + max-width: 500px; + min-height: 200px; + padding: 50px; + font-size: 30px; +} + +.text { + margin: 0; + text-align: center; + color: $active-text-color; + font-family: $ubuntu-font; + font-size: 20px; + font-weight: 400; + line-height: 140%; + + @media screen and (width <= 768px) { + font-size: 14px; + font-weight: 500; + } +} diff --git a/src/components/popups/popup-recipe/index.tsx b/src/components/popups/popup-recipe/index.tsx index 1d481ce2..a969d58c 100644 --- a/src/components/popups/popup-recipe/index.tsx +++ b/src/components/popups/popup-recipe/index.tsx @@ -1,24 +1,12 @@ import React from 'react'; -import styles from './popup-recipe.module.scss'; import Popup from '@components/popup'; -import { usePopup } from '@hooks/use-popup'; import IngredientsListPopup from '@components/recipes-components/ingredients-list-popup'; import ProductsListPopup from '@components/recipes-components/products-list-popup'; +import type { RecipeIngredientsProps } from '@components/recipes-components/types'; +import { usePopup } from '@hooks/use-popup'; +import styles from './popup-recipe.module.scss'; -type RecipeIngredientsProps = { - ingredients: { - id: number; - name: string; - measure_unit: string; - quantity: number; - ingredient_photo: string; - photo?: string; - amount?: number; - price?: number; - }[]; -}; - -const PopupRecipe: React.FC = ({ ingredients }) => { +const PopupRecipe: React.FC = ({ ingredients, handleClick }) => { const { popupState: { openPopupRecipe }, handleClosePopup, @@ -33,8 +21,8 @@ const PopupRecipe: React.FC = ({ ingredients }) => { Выберите товары для добавления в корзину
    - - + +
    diff --git a/src/components/product-card/index.tsx b/src/components/product-card/index.tsx index 29e711db..3791b25f 100644 --- a/src/components/product-card/index.tsx +++ b/src/components/product-card/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import Button from '@components/Button'; +import Button from '@components/button'; import styles from './product-card.module.scss'; import { BASE_URL } from '@data/constants.ts'; import LikeIcon from '@images/like-icon.svg?react'; @@ -10,14 +10,14 @@ import { usePopup } from '@hooks/use-popup'; import { useCart } from '@hooks/use-cart-context.ts'; import CheckIcon from '@images/check.svg?react'; import api from '@services/api'; -import { toMeasureUnit } from '@utils/utils'; +import { translateMeasureUnit } from '@utils/utils'; type ProductCardProps = { cardName: string; price: number; final_price?: number; weight: number; - measureUnit?: string; + measureUnit: string; cardImage: string; category?: string; idCard: number; @@ -72,10 +72,13 @@ const ProductCard: React.FC = ({ if (isInCart) { return deleteCart(idCard); } - return updateCart(idCard, 1); + return updateCart([{ id: idCard, quantity: 1 }]); }; - const { newMeasureUnit, newWeight } = toMeasureUnit(measureUnit, weight); + const { measureUnit: newMeasureUnit, amount } = translateMeasureUnit( + measureUnit, + weight + ); return (
    @@ -101,7 +104,7 @@ const ProductCard: React.FC = ({ {`${newWeight} ${newMeasureUnit}`} + >{`${amount + newMeasureUnit}`}
    diff --git a/src/components/profile-components/order-status/index.tsx b/src/components/profile-components/order-status/index.tsx index 01137a6d..bdc14a9e 100644 --- a/src/components/profile-components/order-status/index.tsx +++ b/src/components/profile-components/order-status/index.tsx @@ -1,19 +1,14 @@ -import styles from './order-status.module.scss'; +import clsx from 'clsx'; import doneIcon from '@images/profile/home-status.svg'; import canceledIcon from '@images/profile/cancel-status.svg'; import orderedIcon from '@images/profile/payment-status.svg'; import deliveredIcon from '@images/profile/car-status.svg'; -import clsx from 'clsx'; +import checkedIcon from '@images/profile/check-status.svg'; +import { OrderStatusType } from '@pages/profile/utils/types'; +import styles from './order-status.module.scss'; type Props = { - status?: - | 'Ordered' - | 'In processing' - | 'Collecting' - | 'Gathered' - | 'In delivering' - | 'Delivered' - | 'Completed'; + status?: OrderStatusType; }; const statusObj = { @@ -33,13 +28,13 @@ const statusObj = { style: styles.canceled, }, Ordered: { - text: 'Оплачен', - image: orderedIcon, - style: styles.ordered, + text: 'Оформлен', + image: checkedIcon, + style: styles.checked, }, Gathered: { text: '', image: '', style: '' }, 'In processing': { text: '', image: '', style: '' }, - 'In delivering': { text: '', image: '', style: '' }, + 'In delivering': { text: 'Оплачен', image: orderedIcon, style: styles.ordered }, Collecting: { text: '', image: '', style: '' }, }; diff --git a/src/components/profile-components/order-status/order-status.module.scss b/src/components/profile-components/order-status/order-status.module.scss index cce1c642..6310459a 100644 --- a/src/components/profile-components/order-status/order-status.module.scss +++ b/src/components/profile-components/order-status/order-status.module.scss @@ -36,6 +36,10 @@ border: 1px solid $tip-color; } +.checked { + border: 1px solid $green-primary-700; +} + .icon { line-height: 0; width: 24px; diff --git a/src/components/profile-components/profile-order-mobile/index.tsx b/src/components/profile-components/profile-order-mobile/index.tsx index ddf2a7a2..b1aca348 100644 --- a/src/components/profile-components/profile-order-mobile/index.tsx +++ b/src/components/profile-components/profile-order-mobile/index.tsx @@ -1,42 +1,10 @@ +import clsx from 'clsx'; import ProductCard from '@components/product-card'; -import styles from './profile-order-mobile.module.scss'; +import PaymentButton from '@components/payment-button'; import OrderStatus from '../order-status'; -import clsx from 'clsx'; -// import { OrderList } from '@services/generated-api/data-contracts'; - -type OrderStatusType = - | 'Ordered' - | 'In processing' - | 'Collecting' - | 'Gathered' - | 'In delivering' - | 'Delivered' - | 'Completed'; - -type Product = { - amount: number; - final_price: number; - id: number; - measure_unit: string; - name: string; - quantity: string; - photo: string; - category: { - category_name: string; - category_slug: string; - }; -}; - -type CommonOrder = { - id: number; - order_number?: string; - ordering_date?: string; - total_price?: string; - payment_method?: string; - delivery_method?: string; - status?: OrderStatusType; - products: Array<{ product: Product; quantity: string }> | Product[]; -}; +import type { CommonOrder, Product } from '@pages/profile/utils/types'; +import { getDeliveryMethodRu, getPaymentMethodRu } from '@pages/profile/utils/utils'; +import styles from './profile-order-mobile.module.scss'; type Props = { readonly isShowedProductsDetails?: boolean; @@ -59,19 +27,6 @@ const ProfileOrderMobile = ({ products, } = order; - let payment_method_ru = - payment_method === 'Payment at the point of delivery' - ? 'Банковской картой' - : 'Наличные'; - - let delivery_method_ru; - if (delivery_method === 'Point of delivery') { - delivery_method_ru = 'Самовывоз'; - } else { - delivery_method_ru = 'Курьером'; - payment_method_ru += 'курьеру'; - } - const date = ordering_date && new Date(ordering_date).toLocaleDateString(); return (

    {`${total_price} руб.`}

    - + {order.is_paid || payment_method !== 'Online' ? ( + + ) : ( + + )}
    ); diff --git a/src/components/profile-components/profile-order/index.tsx b/src/components/profile-components/profile-order/index.tsx index 4e264a90..44af9dcf 100644 --- a/src/components/profile-components/profile-order/index.tsx +++ b/src/components/profile-components/profile-order/index.tsx @@ -1,40 +1,11 @@ -import styles from './profile-order.module.scss'; -import OrderStatus from '../order-status'; +import { Link } from 'react-router-dom'; import clsx from 'clsx'; - -type OrderStatusType = - | 'Ordered' - | 'In processing' - | 'Collecting' - | 'Gathered' - | 'In delivering' - | 'Delivered' - | 'Completed'; - -type Product = { - amount: number; - final_price: number; - id: number; - measure_unit: string; - name: string; - quantity: string; - photo: string; - category: { - category_name: string; - category_slug: string; - }; -}; - -type CommonOrder = { - id: number; - order_number?: string; - ordering_date?: string; - total_price?: string; - payment_method?: string; - delivery_method?: string; - status?: OrderStatusType; - products: Array<{ product: Product; quantity: string }> | Product[]; -}; +import PaymentButton from '@components/payment-button'; +import OrderStatus from '../order-status'; +import { translateMeasureUnit } from '@utils/utils'; +import type { CommonOrder, Product } from '@pages/profile/utils/types'; +import { getDeliveryMethodRu, getPaymentMethodRu } from '@pages/profile/utils/utils'; +import styles from './profile-order.module.scss'; type Props = { readonly isShowedProductsDetails?: boolean; @@ -57,25 +28,20 @@ const ProfileOrder = ({ products, } = order; - console.log(status); - let payment_method_ru = - payment_method === 'Payment at the point of delivery' - ? 'Банковской картой' - : 'Наличные'; - - let delivery_method_ru; - if (delivery_method === 'Point of delivery') { - delivery_method_ru = 'Самовывоз'; - } else { - delivery_method_ru = 'Курьером'; - payment_method_ru += 'курьеру'; - } + const getAmountWithMeasureUnit = ( + unitOfMeasure: string, + size: number, + multiplier: number + ) => { + const totalAmount = size * multiplier; + const { measureUnit, amount } = translateMeasureUnit(unitOfMeasure, totalAmount); + return `${amount} ${measureUnit}`; + }; const date = ordering_date && new Date(ordering_date).toLocaleDateString(); return ( <> - + ); }; diff --git a/src/components/profile-components/profile-order/profile-order.module.scss b/src/components/profile-components/profile-order/profile-order.module.scss index 2ffc34a8..4967cd15 100644 --- a/src/components/profile-components/profile-order/profile-order.module.scss +++ b/src/components/profile-components/profile-order/profile-order.module.scss @@ -56,7 +56,6 @@ &__name { justify-self: start; margin: 0; - grid-column: 2; text-align: left; } @@ -89,8 +88,19 @@ object-fit: cover; width: 100%; height: 100%; + } + + &__linkImage { + cursor: pointer; grid-column: 1; } + + &__linkText { + cursor: pointer; + text-decoration: none; + color: $active-text-color; + grid-column: 2; + } } .order-details { diff --git a/src/components/ratings-and-reviews-components/rating-display/index.tsx b/src/components/ratings-and-reviews-components/rating-display/index.tsx new file mode 100644 index 00000000..ed522fbf --- /dev/null +++ b/src/components/ratings-and-reviews-components/rating-display/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { starsArray } from '../utils/constants'; +import { IRatingsAndReviews } from '../utils/types'; +import styles from './rating-display.module.scss'; + +interface IRatingDisplay extends Pick {} + +const RatingDisplay: React.FC = ({ rating }) => { + return ( +
      + {starsArray.map((mark) => ( +
    • + ))} +
    + ); +}; + +export default RatingDisplay; diff --git a/src/components/ratings-and-reviews-components/rating-display/rating-display.module.scss b/src/components/ratings-and-reviews-components/rating-display/rating-display.module.scss new file mode 100644 index 00000000..edb381b2 --- /dev/null +++ b/src/components/ratings-and-reviews-components/rating-display/rating-display.module.scss @@ -0,0 +1,27 @@ +.container { + padding: 0; + margin: 0; + display: flex; + flex-direction: row; + gap: 10px; + + @media screen and (width <= 768px) { + gap: 4px; + } +} + +.star { + width: 24px; + aspect-ratio: 1; + background-image: url('@images/star-rating-outlined.svg'); + background-position: center; + background-size: cover; + + &.active { + background-image: url('@images/star-rating-filled.svg'); + } + + @media screen and (width <= 1180px) { + width: 12px; + } +} diff --git a/src/components/ratings-and-reviews-components/rating-input/index.tsx b/src/components/ratings-and-reviews-components/rating-input/index.tsx new file mode 100644 index 00000000..5ef2a557 --- /dev/null +++ b/src/components/ratings-and-reviews-components/rating-input/index.tsx @@ -0,0 +1,39 @@ +import React, { useState } from 'react'; +import { starsArray } from '../utils/constants'; +import { IRatingsAndReviews } from '../utils/types'; +import styles from './rating-input.module.scss'; + +interface IRatingInput extends Pick {} + +const RatingInput: React.FC = ({ rating, onRatingChange }) => { + const [hover, setHover] = useState(0); + + return ( +
    setHover(0)} + > + {starsArray.map((mark) => ( + + ))} +
    + ); +}; + +export default RatingInput; diff --git a/src/components/ratings-and-reviews-components/rating-input/rating-input.module.scss b/src/components/ratings-and-reviews-components/rating-input/rating-input.module.scss new file mode 100644 index 00000000..122c5e12 --- /dev/null +++ b/src/components/ratings-and-reviews-components/rating-input/rating-input.module.scss @@ -0,0 +1,44 @@ +.container { + padding: 0; + margin: 0; + background: none; + border: none; + display: flex; + flex-direction: row; + gap: 10px; +} + +.item { + padding: 0; + margin: 0; +} + +.input { + display: none; +} + +.star { + display: block; + padding: 0; + margin: 0; + width: 24px; + aspect-ratio: 1; + background-image: url('@images/star-rating-outlined.svg'); + background-color: transparent; + background-position: center; + background-size: cover; + border: none; + + &.active, + &.hovered { + background-image: url('@images/star-rating-filled.svg'); + } + + .clickable { + cursor: pointer; + } + + @media screen and (width <= 768px) { + width: 20px; + } +} diff --git a/src/components/ratings-and-reviews-components/ratings-and-reviews-widget/index.tsx b/src/components/ratings-and-reviews-components/ratings-and-reviews-widget/index.tsx new file mode 100644 index 00000000..aaab677c --- /dev/null +++ b/src/components/ratings-and-reviews-components/ratings-and-reviews-widget/index.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from 'react'; +import api from '@services/api'; +import ReviewAndRatingPostForm from '../review-and-rating-post-form'; +import RatingsBreakdown from '../ratings-breakdown'; +import ReviewsList from '../reviews-list'; +import { + IRatingsAndReviews, + IReview, + TRatingNullable, + TRatingsNullable, + TReviewNullable, + TReviewsNullable, +} from '../utils/types'; +import styles from './ratings-and-reviews-widget.module.scss'; + +interface IRatingsAndReviewsWidget extends Pick {} + +const RatingsAndReviewsWidget: React.FC = ({ productId }) => { + const [reviews, setReviews] = useState(null); + const [userReview, setUserReview] = useState(null); + const [reviewsWithText, setReviewsWithText] = useState(null); + const [ratings, setRatings] = useState(null); + const [hasOrderedThis, setHasOrderedThis] = useState(false); + const [userId, setUserId] = useState(null); + + useEffect(() => { + api + .usersMeRead() + .then((res) => setUserId(res.id)) + .catch((err) => console.log(err)); + }, []); + + useEffect(() => { + api + .reviewsList(productId) + .then((res) => { + res[0] && + res.sort(function (a: IReview, b: IReview) { + return Number(new Date(b.pub_date)) - Number(new Date(a.pub_date)); + }); + setReviews(res[0] ? res : null); + }) + .catch((err) => console.log(err)); + api + .productsOrderCheck(productId) + .then((res) => setHasOrderedThis(res.ordered)) + .catch((err) => console.log(err)); + }, [productId]); + + useEffect(() => { + if (!reviews) return; + const filtered = reviews.filter((review: IReview) => !!review.text); + const ratings = reviews.map((review: IReview) => review.score); + const userReview = reviews.find((review: IReview) => review.author.id === userId); + setReviewsWithText(filtered[0] ? filtered : null); + setRatings(ratings[0] ? ratings : null); + setUserReview(userReview || null); + }, [reviews, userId]); + + const handleAddReview = (review: IReview) => { + setUserReview(review); + setReviews(reviews ? [review, ...reviews] : [review]); + }; + + const handleUpdateReview = (review: IReview) => { + setUserReview(review); + const newReviews = reviews + ?.slice() + .map((item) => (item.id === review.id ? review : item)); + setReviews(newReviews || null); + }; + + return ( + (hasOrderedThis || reviews) && ( +
    +

    Отзывы

    +
    + {hasOrderedThis && ( +
    + +
    + )} + {ratings && ( + + {reviewsWithText && ( +
    + +
    + )} + +
    + +
    +
    + )} +
    +
    + ) + ); +}; + +export default RatingsAndReviewsWidget; diff --git a/src/components/ratings-and-reviews-components/ratings-and-reviews-widget/ratings-and-reviews-widget.module.scss b/src/components/ratings-and-reviews-components/ratings-and-reviews-widget/ratings-and-reviews-widget.module.scss new file mode 100644 index 00000000..3759bb73 --- /dev/null +++ b/src/components/ratings-and-reviews-components/ratings-and-reviews-widget/ratings-and-reviews-widget.module.scss @@ -0,0 +1,48 @@ +@use '@scss/_variables' as *; + +.section { + font-family: $ubuntu-font; + color: $active-text-color; + display: flex; + flex-direction: column; + gap: 40px; + + @media screen and (width <= 768px) { + gap: 20px; + } +} + +.grid { + display: grid; + grid-template-columns: 1fr 240px; + gap: 40px 107px; + + @media screen and (width <= 1180px) { + grid-template-columns: 1fr 160px; + gap: 40px 40px; + } + + @media screen and (width <= 768px) { + grid-template-columns: 1fr; + } +} + +.form { + grid-column: span 2; + + @media screen and (width <= 768px) { + grid-column: 1; + } +} + +.ratings { + padding-right: 40px; + + @media screen and (width <= 1180px) { + padding-right: 20px; + } + + @media screen and (width <= 768px) { + grid-row: 1; + } +} diff --git a/src/components/ratings-and-reviews-components/ratings-breakdown/index.tsx b/src/components/ratings-and-reviews-components/ratings-breakdown/index.tsx new file mode 100644 index 00000000..aebc4b3a --- /dev/null +++ b/src/components/ratings-and-reviews-components/ratings-breakdown/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import plural from '../utils/pluralizer'; +import { starsArray, ratingsTitleOptions } from '../utils/constants'; +import { IRatingsAndReviews } from '../utils/types'; +import styles from './ratings-breakdown.module.scss'; + +interface IRatingsBreakdown extends Pick {} + +const RatingsBreakdown: React.FC = ({ ratings }) => { + const sum = ratings.reduce((a, b) => a + b, 0); + const amount = ratings.length; + const average = (sum / amount).toFixed(1); + const title = ratingsTitleOptions[plural(amount)]; + + return ( + + ); +}; + +export default RatingsBreakdown; diff --git a/src/components/ratings-and-reviews-components/ratings-breakdown/ratings-breakdown.module.scss b/src/components/ratings-and-reviews-components/ratings-breakdown/ratings-breakdown.module.scss new file mode 100644 index 00000000..9c859110 --- /dev/null +++ b/src/components/ratings-and-reviews-components/ratings-breakdown/ratings-breakdown.module.scss @@ -0,0 +1,102 @@ +@use '@scss/_variables' as *; + +.container { + font-family: $ubuntu-font; + color: $active-text-color; + display: flex; + flex-direction: column; +} + +.average { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + font-size: 36px; + font-weight: 700; + line-height: 140%; + + @media screen and (width <= 768px) { + font-size: 15px; + } +} + +.bigStar { + width: 40px; + aspect-ratio: 1; + background-image: url('@images/star-rating-filled.svg'); + background-position: center; + background-size: cover; + + @media screen and (width <= 768px) { + width: 16px; + } +} + +.smallStar { + width: 24px; + aspect-ratio: 1; + background-image: url('@images/star-rating-filled.svg'); + background-position: center; + background-size: cover; + + @media screen and (width <= 1180px) { + width: 16px; + } +} + +.title { + padding: 0; + margin: 0; + font-size: 22px; + font-weight: 500; + line-height: 140%; + + @media screen and (width <= 768px) { + font-size: 12px; + font-weight: 400; + } +} + +.list { + padding: 20px 0 0; + margin: 0; + display: flex; + flex-direction: column-reverse; + gap: 12px; +} + +.item { + display: flex; + flex-direction: row; + align-items: center; + gap: 20px; +} + +.mark { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + font-size: 22px; + font-weight: 500; + line-height: 140%; + + @media screen and (width <= 1180px) { + font-size: 15px; + font-weight: 700; + } +} + +.progress { + appearance: none; + height: 2px; + + &::-webkit-progress-bar { + background-color: #293929; + } + + &::-webkit-progress-value { + background-color: #f5b63b; + } +} diff --git a/src/components/ratings-and-reviews-components/review-and-rating-post-form/index.tsx b/src/components/ratings-and-reviews-components/review-and-rating-post-form/index.tsx new file mode 100644 index 00000000..41ec3b5b --- /dev/null +++ b/src/components/ratings-and-reviews-components/review-and-rating-post-form/index.tsx @@ -0,0 +1,105 @@ +import React, { SyntheticEvent, useEffect } from 'react'; +import api from '@services/api'; +import { useFormAndValidation } from '@hooks/use-form-and-validation'; +import RatingInput from '@components/ratings-and-reviews-components/rating-input'; +import Button from '@components/Button'; +import { dateOptions } from '../utils/constants'; +import { IRatingsAndReviews, IReview, TRating, TReviewNullable } from '../utils/types'; +import styles from './review-and-rating-post-form.module.scss'; + +interface IReviewAndRatingPostForm + extends Pick { + review: TReviewNullable; +} + +const ReviewAndRatingPostForm: React.FC = ({ + review, + productId, + onAddReview, + onUpdateReview, +}) => { + const { values, setValues, handleChange } = useFormAndValidation({ + text: review?.text || '', + score: review?.score || 0, + }); + + useEffect(() => { + review && setValues({ text: review?.text, score: review?.score }); + }, [review, productId, setValues]); + + const handleReviewUpdate = (res: IReview) => { + onUpdateReview(res); + return res; + }; + + const handleReviewCreate = (res: IReview) => { + onAddReview(res); + return res; + }; + + const handleReviewSubmit = (e: SyntheticEvent) => { + e.preventDefault(); + + // TODO: Add error processing in catch block + if (review?.score && values.text && values.text !== review?.text) { + api + .reviewsUpdate(productId, review.id, { text: values.text } as Pick< + IReview, + 'text' + >) + .then(handleReviewUpdate) + .catch((err) => console.log(err)); + } + }; + + const handleRatingChange = (rating: TRating) => { + if (review?.score === rating) return; + + const updateReview = + review && review.score > 0 + ? api + .reviewsUpdate(productId, review.id, { + score: rating, + }) + .then(handleReviewUpdate) + : api.reviewsCreate(productId, { score: rating }).then(handleReviewCreate); + + // TODO: Add error processing in catch block + updateReview + .then(() => setValues({ ...values, score: rating })) + .catch((err) => console.log(err)); + }; + + return ( +
    +

    + {review ? 'Вы оценили товар ' : 'Оценить товар'} + {review && ( + + )} +

    + +
    +