From 1ddd733c1550a6acbe8ae0ab263b000de624a50c Mon Sep 17 00:00:00 2001 From: maXimalist Date: Sat, 4 Dec 2021 14:53:44 +0300 Subject: [PATCH 1/2] sprint-2/step-2 --- package-lock.json | 129 ++++++++++++ package.json | 6 + src/components/app/app.jsx | 72 +++---- .../bun-ingredient/bun-ingredient.jsx | 37 ++++ .../burger-constructor/burger-constructor.jsx | 189 ++++++------------ .../burger-constructor.module.css | 86 +------- .../burger-ingredients/burger-ingredients.jsx | 122 ++--------- .../burger-ingredients.module.css | 25 --- src/components/hocs/with-modal.js | 32 +-- .../ingredient-details/ingredient-details.jsx | 11 +- .../ingredient-item/ingredient-item.jsx | 11 +- .../ingredient-item.module.css | 5 + .../ingredients-list-container.jsx | 78 ++++++++ .../ingredients-list-container.module.css | 16 ++ .../ingredients-list/ingredients-list.jsx | 38 ++-- .../ingredients-tabs/ingredients-tabs.jsx | 34 ++++ .../ingredients-tabs.module.css | 7 + .../main-ingredient/main-ingredient.jsx | 81 ++++++++ .../main-ingredient.module.css | 34 ++++ .../main-ingredients-list.jsx | 31 +++ .../main-ingredients-list.module.css | 20 ++ .../no-ingredients/no-ingredients.jsx | 17 ++ .../no-ingredients/no-ingredients.module.css | 16 ++ .../order-details/order-details.jsx | 11 +- .../submit-section/submit-section.jsx | 34 ++++ .../submit-section/submit-section.module.css | 27 +++ src/index.js | 8 +- src/services/api.js | 18 ++ src/services/app-context.js | 3 - src/services/ducks/app.js | 34 ++++ src/services/ducks/burger-ingredients.js | 85 ++++++++ src/services/ducks/index.js | 15 ++ src/services/ducks/ingredient.js | 25 +++ src/services/ducks/ingredients.js | 56 ++++++ src/services/ducks/order.js | 50 +++++ src/services/modules/app.js | 80 -------- src/services/store.js | 14 ++ src/utils/constants.js | 35 +++- 38 files changed, 1079 insertions(+), 513 deletions(-) create mode 100644 src/components/bun-ingredient/bun-ingredient.jsx create mode 100644 src/components/ingredients-list-container/ingredients-list-container.jsx create mode 100644 src/components/ingredients-list-container/ingredients-list-container.module.css create mode 100644 src/components/ingredients-tabs/ingredients-tabs.jsx create mode 100644 src/components/ingredients-tabs/ingredients-tabs.module.css create mode 100644 src/components/main-ingredient/main-ingredient.jsx create mode 100644 src/components/main-ingredient/main-ingredient.module.css create mode 100644 src/components/main-ingredients-list/main-ingredients-list.jsx create mode 100644 src/components/main-ingredients-list/main-ingredients-list.module.css create mode 100644 src/components/no-ingredients/no-ingredients.jsx create mode 100644 src/components/no-ingredients/no-ingredients.module.css create mode 100644 src/components/submit-section/submit-section.jsx create mode 100644 src/components/submit-section/submit-section.module.css create mode 100644 src/services/api.js delete mode 100644 src/services/app-context.js create mode 100644 src/services/ducks/app.js create mode 100644 src/services/ducks/burger-ingredients.js create mode 100644 src/services/ducks/index.js create mode 100644 src/services/ducks/ingredient.js create mode 100644 src/services/ducks/ingredients.js create mode 100644 src/services/ducks/order.js delete mode 100644 src/services/modules/app.js create mode 100644 src/services/store.js diff --git a/package-lock.json b/package-lock.json index bf20ae2..5dee8d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1873,6 +1873,39 @@ } } }, + "@react-dnd/asap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz", + "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==" + }, + "@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" + }, + "@reduxjs/toolkit": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.6.2.tgz", + "integrity": "sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA==", + "requires": { + "immer": "^9.0.6", + "redux": "^4.1.0", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" + }, + "dependencies": { + "immer": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.7.tgz", + "integrity": "sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA==" + } + } + }, "@rollup/plugin-node-resolve": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", @@ -2285,6 +2318,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", @@ -2383,6 +2425,17 @@ "@types/react": "*" } }, + "@types/react-redux": { + "version": "7.1.20", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.20.tgz", + "integrity": "sha512-q42es4c8iIeTgcnB+yJgRTTzftv3eYYvCZOh1Ckn2eX/3o5TdsQYKUWpLoLuGlcY/p+VAhV9IOEZJcWk/vfkXw==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/resolve": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", @@ -5095,6 +5148,16 @@ "path-type": "^4.0.0" } }, + "dnd-core": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz", + "integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==", + "requires": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.1.1" + } + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -7066,6 +7129,14 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -12132,6 +12203,26 @@ } } }, + "react-dnd": { + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.4.tgz", + "integrity": "sha512-AFJJXzUIWp5WAhgvI85ESkDCawM0lhoVvfo/lrseLXwFdH3kEO3v8I2C81QPqBW2UEyJBIPStOhPMGYGFtq/bg==", + "requires": { + "@react-dnd/invariant": "^2.0.0", + "@react-dnd/shallowequal": "^2.0.0", + "dnd-core": "14.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + } + }, + "react-dnd-html5-backend": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz", + "integrity": "sha512-QgN6rYrOm4UUj6tIvN8ovImu6uP48xBXF2rzVsp6tvj6d5XQ7OjHI4SJ/ZgGobOneRAU3WCX4f8DGCYx0tuhlw==", + "requires": { + "dnd-core": "14.0.1" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -12152,6 +12243,26 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-redux": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz", + "integrity": "sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "dependencies": { + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + } + } + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -12298,6 +12409,19 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", + "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-thunk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", + "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==" + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -12502,6 +12626,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "reselect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.5.tgz", + "integrity": "sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ==" + }, "resolve": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", diff --git a/package.json b/package.json index 22e5f86..d86fea3 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@reduxjs/toolkit": "^1.6.2", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", @@ -12,8 +13,13 @@ "@types/react-dom": "^17.0.10", "@ya.praktikum/react-developer-burger-ui-components": "^1.12.0", "react": "^17.0.2", + "react-dnd": "^14.0.4", + "react-dnd-html5-backend": "^14.0.2", "react-dom": "^17.0.2", + "react-redux": "^7.2.6", "react-scripts": "4.0.3", + "redux": "^4.1.2", + "redux-thunk": "^2.4.1", "typescript": "^4.4.4", "web-vitals": "^1.1.2" }, diff --git a/src/components/app/app.jsx b/src/components/app/app.jsx index 8c8bd1e..76e8556 100644 --- a/src/components/app/app.jsx +++ b/src/components/app/app.jsx @@ -1,11 +1,14 @@ -import { useReducer, useEffect } from 'react'; +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; import s from './app.module.css'; -import { AppContext } from '../../services/app-context'; -import { ENDPOINT, ApiRoute } from '../../utils/constants'; -import appReducer, { - FETCH_INGREDIENTS, - FETCH_INGREDIENTS_ERROR, -} from '../../services/modules/app'; +import { ModalType } from '../../utils/constants'; +import { + getIngredientsState, + fetchAllIngredients, +} from '../../services/ducks/ingredients'; +import { getAppState } from '../../services/ducks/app'; // Components import AppHeader from '../app-header/app-header'; @@ -20,60 +23,31 @@ import withModal from '../hocs/with-modal'; const WithModalBurgerConstructor = withModal(BurgerConstructor); const WithModalBurgerIngredients = withModal(BurgerIngredients); -const initialAppState = { - ingredients: [], - ingredient: null, - burgerData: { - bunIngredient: null, - mainIngredients: [], - }, - orderNumber: null, - totalPrice: 0, - modalType: null, - loading: true, - error: false, -}; - const App = () => { - const [state, dispatch] = useReducer(appReducer, initialAppState); - const { loading, error } = state; + const dispatch = useDispatch(); + const { isLoading } = useSelector(getIngredientsState); + const { isError } = useSelector(getAppState); useEffect(() => { - const getData = async () => { - try { - const response = await fetch(`${ENDPOINT}/${ApiRoute.INGREDIENTS}`); - - if (response?.ok) { - const { data: ingredients } = await response.json(); - return dispatch({ - type: FETCH_INGREDIENTS, - payload: ingredients, - }); - } - - throw new Error(); - } catch (err) { - dispatch({ - type: FETCH_INGREDIENTS_ERROR, - }); - } + const fetchIngredients = async () => { + await dispatch(fetchAllIngredients()); }; - getData(); - }, []); + fetchIngredients(); + }, [dispatch]); - if (loading) return ; + if (isLoading) return ; return ( <> - {error ? ( + {isError ? ( Произошла ошибка при загрузке данных... ) : (
- - - - + + + +
)} diff --git a/src/components/bun-ingredient/bun-ingredient.jsx b/src/components/bun-ingredient/bun-ingredient.jsx new file mode 100644 index 0000000..54fa6a2 --- /dev/null +++ b/src/components/bun-ingredient/bun-ingredient.jsx @@ -0,0 +1,37 @@ +import { memo } from 'react'; +import pt from 'prop-types'; +import { PropValidator } from '../../utils/prop-validator'; +import { ConstructorElement } from '@ya.praktikum/react-developer-burger-ui-components'; +import { BunPosition } from '../../utils/constants'; + +// Components +import NoIngredient from '../no-ingredient/no-ingredient'; + +const BunIngredient = ({ ingredient, position }) => { + const positionName = position === BunPosition.TOP ? 'верх' : 'низ'; + + if (!ingredient) + return Выберите булку ({positionName}); + + const { name, price, image_mobile } = ingredient; + + return ( + + ); +}; + +BunIngredient.propTypes = { + ingredient: pt.oneOfType([ + PropValidator.INGREDIENT.isRequired, + pt.oneOf([null]).isRequired, + ]), + position: pt.string.isRequired, +}; + +export default memo(BunIngredient); diff --git a/src/components/burger-constructor/burger-constructor.jsx b/src/components/burger-constructor/burger-constructor.jsx index 230ac57..75a638a 100644 --- a/src/components/burger-constructor/burger-constructor.jsx +++ b/src/components/burger-constructor/burger-constructor.jsx @@ -1,149 +1,80 @@ -import { useContext, useEffect } from 'react'; -import { - Button, - ConstructorElement, - CurrencyIcon, - DragIcon, - BurgerIcon, -} from '@ya.praktikum/react-developer-burger-ui-components'; +import { useCallback } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useDrop } from 'react-dnd'; import s from './burger-constructor.module.css'; -import { AppContext } from '../../services/app-context'; -import { ENDPOINT, ApiRoute, ModalType } from '../../utils/constants'; import { - SET_ORDER_NUMBER, - SEND_ORDER_ERROR, - DELETE_INGREDIENT, - SET_TOTAL_PRICE, -} from '../../services/modules/app'; + getBurgerIngredients, + getTotalPrice, + addIngredient, +} from '../../services/ducks/burger-ingredients'; +import { sendOrder, getOrderState } from '../../services/ducks/order'; +import { openModal } from '../../services/ducks/app'; +import { BunPosition, ModalType } from '../../utils/constants'; // Components -import NoIngredient from '../no-ingredient/no-ingredient'; +import BunIngredient from '../bun-ingredient/bun-ingredient'; +import MainIngredientsList from '../main-ingredients-list/main-ingredients-list'; +import SubmitSection from '../submit-section/submit-section'; +import NoIngredients from '../no-ingredients/no-ingredients'; const BurgerConstructor = () => { - const { - burgerData: { bunIngredient, mainIngredients }, - totalPrice, - dispatch, - } = useContext(AppContext); + const dispatch = useDispatch(); + const [{ isHover }, dropRef] = useDrop({ + accept: 'ingredient', + drop(ingredient) { + handleDrop(ingredient); + }, + collect: (monitor) => ({ + isHover: monitor.isOver(), + }), + }); + const { bunIngredient, mainIngredients } = useSelector(getBurgerIngredients); + const { isLoading } = useSelector(getOrderState); + const totalPrice = useSelector(getTotalPrice); const isIngredientsExist = bunIngredient || mainIngredients.length > 0; - const handleSendOrder = async () => { - const ingredients = [bunIngredient, ...mainIngredients].map( + const handleSendOrder = useCallback(async () => { + const ingredientIds = [bunIngredient, ...mainIngredients].map( ({ _id }) => _id ); - - try { - const response = await fetch(`${ENDPOINT}/${ApiRoute.ORDERS}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ingredients }), - }); - - if (response?.ok) { - const { order } = await response.json(); - return dispatch({ - type: SET_ORDER_NUMBER, - payload: { orderNumber: order.number, modalType: ModalType.ORDER }, - }); - } - - throw new Error(); - } catch (err) { - dispatch({ - type: SEND_ORDER_ERROR, - }); - } - }; - - const handleDeleteIngredient = (ingredientIndex) => { - const newMainIngredients = mainIngredients.filter( - (_, index) => index !== ingredientIndex - ); - dispatch({ type: DELETE_INGREDIENT, payload: newMainIngredients }); - }; - - useEffect(() => { - const bunPrice = bunIngredient?.price * 2 || 0; - - const totalPrice = - mainIngredients.reduce( - (totalPrice, { price }) => (totalPrice += price), - 0 - ) + bunPrice; - - dispatch({ - type: SET_TOTAL_PRICE, - payload: totalPrice, - }); + await dispatch(sendOrder(ingredientIds)); + dispatch(openModal(ModalType.ORDER)); }, [bunIngredient, mainIngredients, dispatch]); + const handleDrop = useCallback( + (ingredient) => { + dispatch(addIngredient(ingredient)); + }, + [dispatch] + ); + return ( -
+
{!isIngredientsExist ? ( -
- -

- Выберите ингредиенты для вашего бургера -

-
+ ) : ( <> -
- {bunIngredient ? ( - - ) : ( - Выберите булку (верх) - )} -
    - {mainIngredients.length ? ( - mainIngredients.map(({ name, price, image_mobile }, index) => ( -
  • - - handleDeleteIngredient(index)} - /> -
  • - )) - ) : ( - Выберите основные ингредиенты - )} -
- {bunIngredient ? ( - - ) : ( - Выберите булку (низ) - )} -
-
-

- {totalPrice}  - -

- +
+ + +
+ )}
diff --git a/src/components/burger-constructor/burger-constructor.module.css b/src/components/burger-constructor/burger-constructor.module.css index 8cd6839..03aa24f 100644 --- a/src/components/burger-constructor/burger-constructor.module.css +++ b/src/components/burger-constructor/burger-constructor.module.css @@ -4,6 +4,11 @@ width: 100%; } +.hoverBurgerConstructorSection { + composes: burgerConstructorSection; + outline: 3px dashed rgba(255, 255, 255, 0.4); +} + .ingredientsContainer { display: flex; flex-flow: column wrap; @@ -11,88 +16,7 @@ gap: 16px; } -.noIngredientsBlock { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 100%; - display: flex; - flex-flow: column; - align-items: center; -} - -.noIngredientsBlock svg { - width: 30%; - height: 30%; - margin-bottom: 40px; -} - .ingredientsContainer > div { max-width: 536px; width: 100%; } - -.ingredientsList { - display: flex; - align-items: center; - flex-flow: column; - gap: 16px; - max-height: 465px; - overflow-y: auto; -} - -.ingredientsList::-webkit-scrollbar { - width: 8px; -} - -.ingredientsList::-webkit-scrollbar-track { - background: #2f2f37; -} - -.ingredientsList::-webkit-scrollbar-thumb { - background: #8585ad; -} - -.ingredientItem { - display: flex; - align-items: center; - margin-right: 25px; -} - -.ingredientItem > svg { - margin-right: 5px; -} - -.ingredientItem > div { - max-width: 536px; - width: 536px; -} - -.submitSection { - display: flex; - justify-content: flex-end; - align-items: center; - margin-right: 30px; -} - -.submitSection button { - cursor: pointer; -} - -.submitSection button:disabled { - box-shadow: none; - filter: none; - opacity: 0.2; - cursor: default; -} - -.totalPrice { - display: flex; - align-items: center; -} - -.totalPrice > svg { - width: 33px; - height: 33px; -} diff --git a/src/components/burger-ingredients/burger-ingredients.jsx b/src/components/burger-ingredients/burger-ingredients.jsx index 4174bf1..1b5693b 100644 --- a/src/components/burger-ingredients/burger-ingredients.jsx +++ b/src/components/burger-ingredients/burger-ingredients.jsx @@ -1,117 +1,31 @@ -import { useState, useCallback, useMemo, useContext, memo } from 'react'; -import { Tab } from '@ya.praktikum/react-developer-burger-ui-components'; +import { useState } from 'react'; import s from './burger-ingredients.module.css'; -import { AppContext } from '../../services/app-context'; -import { ModalType } from '../../utils/constants'; -import { - SET_INGREDIENT, - ADD_BUN_INGREDIENT, - ADD_MAIN_INGREDIENT, -} from '../../services/modules/app'; +import { IngredientType } from '../../utils/constants'; // Components -import IngredientsList from '../ingredients-list/ingredients-list'; - -const IngredientType = { - BUN: 'bun', - SAUCE: 'sauce', - MAIN: 'main', -}; - -const tabItems = [ - { - name: 'Булки', - type: IngredientType.BUN, - }, - { - name: 'Соусы', - type: IngredientType.SAUCE, - }, - { - name: 'Начинки', - type: IngredientType.MAIN, - }, -]; +import IngredientsTabs from '../ingredients-tabs/ingredients-tabs'; +import IngredientsListContainer from '../ingredients-list-container/ingredients-list-container'; const BurgerIngredients = () => { - const [current, setCurrent] = useState(IngredientType.BUN); - const { - ingredients, - burgerData: { bunIngredient, mainIngredients }, - dispatch, - } = useContext(AppContext); - - const burgerIngredients = useMemo( - () => - bunIngredient - ? [bunIngredient, ...mainIngredients] - : [...mainIngredients], - [bunIngredient, mainIngredients] - ); - - const filteredIngredients = useMemo( - () => - ingredients.reduce((acc, ingredient) => { - const { type } = ingredient; - if (!acc[type]) { - acc[type] = [ingredient]; - return acc; - } - - acc[type].push(ingredient); - return acc; - }, {}), - [ingredients] - ); - - const handleClickIngredientItem = useCallback( - (ingredient) => { - dispatch({ - type: SET_INGREDIENT, - payload: { ingredient, modalType: ModalType.INGREDIENT }, - }); - - if (ingredient.type === IngredientType.BUN) { - return dispatch({ type: ADD_BUN_INGREDIENT, payload: ingredient }); - } - - dispatch({ type: ADD_MAIN_INGREDIENT, payload: ingredient }); - }, - [dispatch] - ); + const [activeTab, setActiveTab] = useState(IngredientType.BUN); + const [isScrollMethod, setIsScrollMethod] = useState(false); return (

Соберите бургер

-
- {useMemo( - () => - tabItems.map(({ name, type }) => ( - setCurrent(type)} - > - {name} - - )), - [current] - )} -
-
- {tabItems.map(({ name, type }) => ( - - ))} -
+ +
); }; -export default memo(BurgerIngredients); +export default BurgerIngredients; diff --git a/src/components/burger-ingredients/burger-ingredients.module.css b/src/components/burger-ingredients/burger-ingredients.module.css index d5bb249..b647815 100644 --- a/src/components/burger-ingredients/burger-ingredients.module.css +++ b/src/components/burger-ingredients/burger-ingredients.module.css @@ -2,28 +2,3 @@ max-width: 600px; width: 100%; } - -.tabsBlock { - display: flex; -} - -.tabsBlock > div { - flex-grow: 1; -} - -.ingredientsContainer { - max-height: 756px; - overflow-y: auto; -} - -.ingredientsContainer::-webkit-scrollbar { - width: 8px; -} - -.ingredientsContainer::-webkit-scrollbar-track { - background: #2f2f37; -} - -.ingredientsContainer::-webkit-scrollbar-thumb { - background: #8585ad; -} diff --git a/src/components/hocs/with-modal.js b/src/components/hocs/with-modal.js index 153e955..5780ebc 100644 --- a/src/components/hocs/with-modal.js +++ b/src/components/hocs/with-modal.js @@ -1,7 +1,9 @@ -import { useContext } from 'react'; +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import pt from 'prop-types'; import { ModalType } from '../../utils/constants'; -import { AppContext } from '../../services/app-context'; -import { CLOSE_MODAL } from '../../services/modules/app'; +import { setIngredient } from '../../services/ducks/ingredient'; +import { getAppState, closeModal } from '../../services/ducks/app'; // Components import Modal from '../modal/modal'; @@ -10,20 +12,22 @@ import IngredientDetails from '../ingredient-details/ingredient-details'; const withModal = (WrappedComponent) => { const WithModal = (props) => { - const { modalType, orderNumber, ingredient, dispatch } = useContext( - AppContext - ); + const dispatch = useDispatch(); + const { isModalOpen, modalType } = useSelector(getAppState); - const handleCloseModal = () => { - dispatch({ type: CLOSE_MODAL }); - }; + const handleCloseModal = useCallback(() => { + dispatch(closeModal()); + if (modalType === ModalType.INGREDIENT) { + dispatch(setIngredient(null)); + } + }, [modalType, dispatch]); const getComponent = () => { switch (modalType) { case ModalType.ORDER: - return ; + return ; case ModalType.INGREDIENT: - return ; + return ; default: return; } @@ -32,7 +36,7 @@ const withModal = (WrappedComponent) => { return ( <> - {modalType && ( + {isModalOpen && ( { ); }; + WithModal.propTypes = { + modalType: pt.string.isRequired, + }; + return WithModal; }; diff --git a/src/components/ingredient-details/ingredient-details.jsx b/src/components/ingredient-details/ingredient-details.jsx index 6d252e5..35590f1 100644 --- a/src/components/ingredient-details/ingredient-details.jsx +++ b/src/components/ingredient-details/ingredient-details.jsx @@ -1,7 +1,10 @@ -import { PropValidator } from '../../utils/prop-validator'; +import { useSelector } from 'react-redux'; +import { getIngredient } from '../../services/ducks/ingredient'; import s from './ingredient-details.module.css'; -const IngredientDetails = ({ ingredient }) => { +const IngredientDetails = () => { + const { ingredient } = useSelector(getIngredient); + const { image_large, name, @@ -46,8 +49,4 @@ const IngredientDetails = ({ ingredient }) => { ); }; -IngredientDetails.propTypes = { - ingredient: PropValidator.INGREDIENT.isRequired, -}; - export default IngredientDetails; diff --git a/src/components/ingredient-item/ingredient-item.jsx b/src/components/ingredient-item/ingredient-item.jsx index 2cd88cb..9e965f4 100644 --- a/src/components/ingredient-item/ingredient-item.jsx +++ b/src/components/ingredient-item/ingredient-item.jsx @@ -1,5 +1,6 @@ import { memo } from 'react'; import pt from 'prop-types'; +import { useDrag } from 'react-dnd'; import { CurrencyIcon, Counter, @@ -12,11 +13,19 @@ const IngredientItem = ({ quantity, handleClickIngredientItem, }) => { + const [{ isDrag }, dragRef] = useDrag({ + type: 'ingredient', + item: ingredient, + collect: (monitor) => ({ + isDrag: monitor.isDragging(), + }), + }); const { name, image, price } = ingredient; return (
  • handleClickIngredientItem(ingredient)} > {name} diff --git a/src/components/ingredient-item/ingredient-item.module.css b/src/components/ingredient-item/ingredient-item.module.css index eab10d1..2e323cc 100644 --- a/src/components/ingredient-item/ingredient-item.module.css +++ b/src/components/ingredient-item/ingredient-item.module.css @@ -9,6 +9,11 @@ transition: all 0.2s linear; } +.draggableIngredientItem { + composes: ingredientItem; + opacity: 0.2; +} + .ingredientItem:hover { transform: scale(1.05); } diff --git a/src/components/ingredients-list-container/ingredients-list-container.jsx b/src/components/ingredients-list-container/ingredients-list-container.jsx new file mode 100644 index 0000000..0ba0fdb --- /dev/null +++ b/src/components/ingredients-list-container/ingredients-list-container.jsx @@ -0,0 +1,78 @@ +import { useRef, useMemo, useEffect, createRef } from 'react'; +import { useSelector } from 'react-redux'; +import pt from 'prop-types'; +import s from './ingredients-list-container.module.css'; +import { TAB_ITEMS } from '../../utils/constants'; +import { getFilteredIngredients } from '../../services/ducks/ingredients'; + +// Components +import IngredientsList from '../ingredients-list/ingredients-list'; + +const IngredientsListContainer = ({ + isScrollMethod, + activeTab, + setIsScrollMethod, + setActiveTab, +}) => { + const filteredIngredients = useSelector(getFilteredIngredients); + const containerRef = useRef(); + const ingredientsTitleRef = useMemo(() => { + return TAB_ITEMS.reduce((acc, { type }) => { + acc[type] = createRef(null); + return acc; + }, {}); + }, []); + + const handleScroll = () => { + const [nearestTitle] = TAB_ITEMS.reduce((acc, { type }) => { + acc.push({ + type, + distance: Math.abs( + containerRef.current.getBoundingClientRect().top - + ingredientsTitleRef[type].current.getBoundingClientRect().top + ), + }); + return acc; + }, []).sort((a, b) => a.distance - b.distance); + + if (activeTab !== nearestTitle.type) { + setActiveTab(nearestTitle.type); + setIsScrollMethod(true); + } + }; + + useEffect(() => { + if (!isScrollMethod) { + ingredientsTitleRef[activeTab].current.scrollIntoView(); + } + }, [activeTab, ingredientsTitleRef, isScrollMethod]); + + return ( +
    + {useMemo( + () => + TAB_ITEMS.map(({ name, type }) => ( + + )), + [filteredIngredients, ingredientsTitleRef] + )} +
    + ); +}; +IngredientsListContainer.propTypes = { + isScrollMethod: pt.bool.isRequired, + activeTab: pt.string.isRequired, + setIsScrollMethod: pt.func.isRequired, + setActiveTab: pt.func.isRequired, +}; + +export default IngredientsListContainer; diff --git a/src/components/ingredients-list-container/ingredients-list-container.module.css b/src/components/ingredients-list-container/ingredients-list-container.module.css new file mode 100644 index 0000000..28a46ae --- /dev/null +++ b/src/components/ingredients-list-container/ingredients-list-container.module.css @@ -0,0 +1,16 @@ +.ingredientsContainer { + max-height: 756px; + overflow-y: auto; +} + +.ingredientsContainer::-webkit-scrollbar { + width: 8px; +} + +.ingredientsContainer::-webkit-scrollbar-track { + background: #2f2f37; +} + +.ingredientsContainer::-webkit-scrollbar-thumb { + background: #8585ad; +} diff --git a/src/components/ingredients-list/ingredients-list.jsx b/src/components/ingredients-list/ingredients-list.jsx index 3eefb5d..9df9ab3 100644 --- a/src/components/ingredients-list/ingredients-list.jsx +++ b/src/components/ingredients-list/ingredients-list.jsx @@ -1,42 +1,50 @@ -import { memo } from 'react'; +import { useCallback, forwardRef } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import pt from 'prop-types'; import { PropValidator } from '../../utils/prop-validator'; import s from './ingredients-list.module.css'; +import { ModalType } from '../../utils/constants'; +import { getBurgerIgredientsIdsCount } from '../../services/ducks/burger-ingredients'; +import { setIngredient } from '../../services/ducks/ingredient'; +import { openModal } from '../../services/ducks/app'; // Components import IngredientItem from '../ingredient-item/ingredient-item'; -const IngredientsList = ({ - title, - ingredients, - burgerIngredients, - handleClickIngredientItem, -}) => { - const countIngredients = (id) => - burgerIngredients.filter((ingredient) => ingredient._id === id).length; +const IngredientsList = forwardRef(({ title, ingredients }, ref) => { + const burgerIngredientId = useSelector(getBurgerIgredientsIdsCount); + const dispatch = useDispatch(); + + const handleClickIngredientItem = useCallback( + (ingredient) => { + dispatch(setIngredient(ingredient)); + dispatch(openModal(ModalType.INGREDIENT)); + }, + [dispatch] + ); return ( <> -

    {title}

    +

    + {title} +

      {ingredients.map((ingredient) => ( ))}
    ); -}; +}); IngredientsList.propTypes = { title: pt.string.isRequired, ingredients: pt.arrayOf(PropValidator.INGREDIENT.isRequired).isRequired, - burgerIngredients: pt.arrayOf(PropValidator.INGREDIENT).isRequired, - handleClickIngredientItem: pt.func.isRequired, }; -export default memo(IngredientsList); +export default IngredientsList; diff --git a/src/components/ingredients-tabs/ingredients-tabs.jsx b/src/components/ingredients-tabs/ingredients-tabs.jsx new file mode 100644 index 0000000..1a033f8 --- /dev/null +++ b/src/components/ingredients-tabs/ingredients-tabs.jsx @@ -0,0 +1,34 @@ +import pt from 'prop-types'; +import s from './ingredients-tabs.module.css'; +import { TAB_ITEMS } from '../../utils/constants'; + +// Components +import { Tab } from '@ya.praktikum/react-developer-burger-ui-components'; + +const IngredientsTabs = ({ activeTab, setIsScrollMethod, setActiveTab }) => { + return ( +
    + {TAB_ITEMS.map(({ name, type }) => ( + { + setActiveTab(type); + setIsScrollMethod(false); + }} + > + {name} + + ))} +
    + ); +}; + +IngredientsTabs.propTypes = { + activeTab: pt.string.isRequired, + setIsScrollMethod: pt.func.isRequired, + setActiveTab: pt.func.isRequired, +}; + +export default IngredientsTabs; diff --git a/src/components/ingredients-tabs/ingredients-tabs.module.css b/src/components/ingredients-tabs/ingredients-tabs.module.css new file mode 100644 index 0000000..557b73b --- /dev/null +++ b/src/components/ingredients-tabs/ingredients-tabs.module.css @@ -0,0 +1,7 @@ +.tabsBlock { + display: flex; +} + +.tabsBlock > div { + flex-grow: 1; +} diff --git a/src/components/main-ingredient/main-ingredient.jsx b/src/components/main-ingredient/main-ingredient.jsx new file mode 100644 index 0000000..77d85d9 --- /dev/null +++ b/src/components/main-ingredient/main-ingredient.jsx @@ -0,0 +1,81 @@ +import { memo, useEffect, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import { useDrag, useDrop } from 'react-dnd'; +import pt from 'prop-types'; +import { PropValidator } from '../../utils/prop-validator'; +import { + DragIcon, + ConstructorElement, +} from '@ya.praktikum/react-developer-burger-ui-components'; +import s from './main-ingredient.module.css'; +import { + removeIngredient, + sortMainIngredients, +} from '../../services/ducks/burger-ingredients'; + +const MainIngredient = ({ ingredient, ingredientIndex }) => { + const dispatch = useDispatch(); + const dragDropRef = useRef(null); + const { name, price, image_mobile } = ingredient; + + const [{ isDrag, draggingItem }, dragRef] = useDrag({ + type: 'main-ingredient', + item: { draggingItemIndex: ingredientIndex }, + collect: (monitor) => ({ + isDrag: monitor.isDragging(), + draggingItem: monitor.getItem(), + }), + }); + + const [{ isHover }, dropRef] = useDrop({ + accept: 'main-ingredient', + drop() { + dispatch( + sortMainIngredients({ + startIndex: draggingItem.draggingItemIndex, + moveToIndex: ingredientIndex, + }) + ); + }, + collect: (monitor) => ({ + isHover: monitor.isOver(), + }), + }); + + const isHoveredSameIngredient = + draggingItem?.draggingItemIndex === ingredientIndex; + + useEffect(() => { + dragRef(dragDropRef); + dropRef(dragDropRef); + }, [dragRef, dropRef]); + + const handleDeleteIngredient = (ingredientIndex) => { + dispatch(removeIngredient(ingredientIndex)); + }; + + return ( +
  • + + handleDeleteIngredient(ingredientIndex)} + /> +
  • + ); +}; + +MainIngredient.propTypes = { + ingredient: PropValidator.INGREDIENT.isRequired, + ingredientIndex: pt.number.isRequired, +}; + +export default memo(MainIngredient); diff --git a/src/components/main-ingredient/main-ingredient.module.css b/src/components/main-ingredient/main-ingredient.module.css new file mode 100644 index 0000000..ced0e8d --- /dev/null +++ b/src/components/main-ingredient/main-ingredient.module.css @@ -0,0 +1,34 @@ +.ingredientItem { + display: flex; + align-items: center; + margin-right: 25px; +} + +.ingredientItem > svg { + margin-right: 5px; +} + +.ingredientItem > div { + max-width: 536px; + width: 536px; +} + +.ingredientItemDragging { + opacity: 0.2; +} + +@keyframes color_change { + from { + background-color: #2f2f37; + } + to { + background-color: #4c4cff; + } +} + +.ingredientItemHovered > div { + animation-name: color_change; + animation-duration: 0.5s; + animation-iteration-count: infinite; + animation-direction: alternate; +} diff --git a/src/components/main-ingredients-list/main-ingredients-list.jsx b/src/components/main-ingredients-list/main-ingredients-list.jsx new file mode 100644 index 0000000..28dcdb3 --- /dev/null +++ b/src/components/main-ingredients-list/main-ingredients-list.jsx @@ -0,0 +1,31 @@ +import { memo } from 'react'; +import pt from 'prop-types'; +import { PropValidator } from '../../utils/prop-validator'; +import s from './main-ingredients-list.module.css'; + +// Components +import NoIngredient from '../no-ingredient/no-ingredient'; +import MainIngredient from '../main-ingredient/main-ingredient'; + +const MainIngredientsList = ({ ingredients }) => { + if (!ingredients.length) + return Выберите основные ингредиенты; + + return ( +
      + {ingredients.map((ingredient, index) => ( + + ))} +
    + ); +}; + +MainIngredientsList.propTypes = { + ingredients: pt.arrayOf(PropValidator.INGREDIENT.isRequired).isRequired, +}; + +export default memo(MainIngredientsList); diff --git a/src/components/main-ingredients-list/main-ingredients-list.module.css b/src/components/main-ingredients-list/main-ingredients-list.module.css new file mode 100644 index 0000000..4654909 --- /dev/null +++ b/src/components/main-ingredients-list/main-ingredients-list.module.css @@ -0,0 +1,20 @@ +.ingredientsList { + display: flex; + align-items: center; + flex-flow: column; + gap: 16px; + max-height: 465px; + overflow-y: auto; +} + +.ingredientsList::-webkit-scrollbar { + width: 8px; +} + +.ingredientsList::-webkit-scrollbar-track { + background: #2f2f37; +} + +.ingredientsList::-webkit-scrollbar-thumb { + background: #8585ad; +} diff --git a/src/components/no-ingredients/no-ingredients.jsx b/src/components/no-ingredients/no-ingredients.jsx new file mode 100644 index 0000000..a91eb1f --- /dev/null +++ b/src/components/no-ingredients/no-ingredients.jsx @@ -0,0 +1,17 @@ +import { memo } from 'react'; +import { BurgerIcon } from '@ya.praktikum/react-developer-burger-ui-components'; +import s from './no-ingredients.module.css'; + +const NoIngredients = () => { + return ( +
    + +

    Собери свой бургер

    +

    + Перетащите ингредиенты в конструктор +

    +
    + ); +}; + +export default memo(NoIngredients); diff --git a/src/components/no-ingredients/no-ingredients.module.css b/src/components/no-ingredients/no-ingredients.module.css new file mode 100644 index 0000000..fc3b63b --- /dev/null +++ b/src/components/no-ingredients/no-ingredients.module.css @@ -0,0 +1,16 @@ +.noIngredientsBlock { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + display: flex; + flex-flow: column; + align-items: center; +} + +.noIngredientsBlock svg { + width: 30%; + height: 30%; + margin-bottom: 25px; +} diff --git a/src/components/order-details/order-details.jsx b/src/components/order-details/order-details.jsx index 977f1a2..89f8867 100644 --- a/src/components/order-details/order-details.jsx +++ b/src/components/order-details/order-details.jsx @@ -1,8 +1,11 @@ -import pt from 'prop-types'; +import { useSelector } from 'react-redux'; import OrderAccepted from '../../images/done.gif'; import s from './order-details.module.css'; +import { getOrderState } from '../../services/ducks/order'; + +const OrderDetails = () => { + const { orderNumber } = useSelector(getOrderState); -const OrderDetails = ({ orderNumber }) => { return (

    {orderNumber}

    @@ -18,8 +21,4 @@ const OrderDetails = ({ orderNumber }) => { ); }; -OrderDetails.propTypes = { - orderNumber: pt.number.isRequired, -}; - export default OrderDetails; diff --git a/src/components/submit-section/submit-section.jsx b/src/components/submit-section/submit-section.jsx new file mode 100644 index 0000000..a440601 --- /dev/null +++ b/src/components/submit-section/submit-section.jsx @@ -0,0 +1,34 @@ +import { memo } from 'react'; +import pt from 'prop-types'; +import { + Button, + CurrencyIcon, +} from '@ya.praktikum/react-developer-burger-ui-components'; +import s from './submit-section.module.css'; + +const SubmitSection = ({ totalPrice, isDisabled, handleSendOrder }) => { + return ( +
    +

    + {totalPrice}  + +

    + +
    + ); +}; + +SubmitSection.propTypes = { + totalPrice: pt.number.isRequired, + isDisabled: pt.bool.isRequired, + handleSendOrder: pt.func.isRequired, +}; + +export default memo(SubmitSection); diff --git a/src/components/submit-section/submit-section.module.css b/src/components/submit-section/submit-section.module.css new file mode 100644 index 0000000..7eed5c9 --- /dev/null +++ b/src/components/submit-section/submit-section.module.css @@ -0,0 +1,27 @@ +.submitSection { + display: flex; + justify-content: flex-end; + align-items: center; + margin-right: 30px; +} + +.submitSection button { + cursor: pointer; +} + +.submitSection button:disabled { + box-shadow: none; + filter: none; + opacity: 0.2; + cursor: default; +} + +.totalPrice { + display: flex; + align-items: center; +} + +.totalPrice > svg { + width: 33px; + height: 33px; +} diff --git a/src/index.js b/src/index.js index 10ba03f..0ddbee3 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import { store } from './services/store'; + +// Components import App from './components/app/app'; ReactDOM.render( - + + + , document.getElementById('root') ); diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 0000000..f47d0ba --- /dev/null +++ b/src/services/api.js @@ -0,0 +1,18 @@ +export const ENDPOINT = 'https://norma.nomoreparties.space/api'; + +const request = async (url, options = null) => { + try { + const response = await fetch(`${ENDPOINT}/${url}`, options); + + if (response.ok) { + const data = await response.json(); + return data; + } + + throw new Error(); + } catch (err) { + throw new Error(err); + } +}; + +export default request; diff --git a/src/services/app-context.js b/src/services/app-context.js deleted file mode 100644 index e9220de..0000000 --- a/src/services/app-context.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react'; - -export const AppContext = createContext(); diff --git a/src/services/ducks/app.js b/src/services/ducks/app.js new file mode 100644 index 0000000..8e8f324 --- /dev/null +++ b/src/services/ducks/app.js @@ -0,0 +1,34 @@ +import { createAction, createReducer } from '@reduxjs/toolkit'; +import { ActionPrefix } from '../../utils/constants'; + +// Actions +export const setError = createAction(`${ActionPrefix.APP}/setError`); +export const openModal = createAction(`${ActionPrefix.APP}/openModal`); +export const closeModal = createAction(`${ActionPrefix.APP}/closeModal`); + +// Reducer +const initialState = { + isError: false, + isModalOpen: false, + modalType: null, +}; +const appReducer = createReducer(initialState, (builder) => { + builder + .addCase(setError, (state) => { + state.isError = true; + }) + .addCase(openModal, (state, { payload }) => { + state.isModalOpen = true; + state.modalType = payload; + }) + .addCase(closeModal, (state) => { + state.isModalOpen = false; + state.modalType = null; + }) + .addDefaultCase((state) => state); +}); + +// Selectors +export const getAppState = ({ app }) => app; + +export default appReducer; diff --git a/src/services/ducks/burger-ingredients.js b/src/services/ducks/burger-ingredients.js new file mode 100644 index 0000000..45412eb --- /dev/null +++ b/src/services/ducks/burger-ingredients.js @@ -0,0 +1,85 @@ +import { createReducer, createAction, createSelector } from '@reduxjs/toolkit'; +import { IngredientType } from '../../utils/constants'; +import { ActionPrefix } from '../../utils/constants'; + +// Actions +export const addIngredient = createAction( + `${ActionPrefix.BURGER_INGREDIENTS}/addIngredient` +); +export const removeIngredient = createAction( + `${ActionPrefix.BURGER_INGREDIENTS}/removeIngredient` +); +export const sortMainIngredients = createAction( + `${ActionPrefix.BURGER_INGREDIENTS}/sortMainIngredients` +); + +// Reducer +const initialState = { + burgerData: { + bunIngredient: null, + mainIngredients: [], + }, +}; + +const reducer = createReducer(initialState, (builder) => { + builder + .addCase(addIngredient, (state, { payload: ingredient }) => { + if (ingredient.type === IngredientType.BUN) { + state.burgerData.bunIngredient = ingredient; + return; + } + state.burgerData.mainIngredients.push(ingredient); + }) + .addCase(sortMainIngredients, (state, { payload }) => { + const { startIndex, moveToIndex } = payload; + const ingredient = state.burgerData.mainIngredients[startIndex]; + const mainIngredients = [ + ...state.burgerData.mainIngredients.slice(0, startIndex), + ...state.burgerData.mainIngredients.slice(startIndex + 1), + ]; + + mainIngredients.splice(moveToIndex, 0, ingredient); + state.burgerData.mainIngredients = mainIngredients; + }) + .addCase(removeIngredient, (state, { payload: ingredientIndex }) => { + state.burgerData.mainIngredients.splice(ingredientIndex, 1); + }) + .addDefaultCase((state) => state); +}); + +// Selectors +export const getBurgerIngredients = ({ burgerIngredients }) => + burgerIngredients.burgerData; + +export const getTotalPrice = createSelector( + getBurgerIngredients, + (burgerIngredients) => { + const { bunIngredient, mainIngredients } = burgerIngredients; + const bunPrice = bunIngredient?.price * 2 || 0; + + const totalPrice = + mainIngredients.reduce( + (totalPrice, { price }) => (totalPrice += price), + 0 + ) + bunPrice; + + return totalPrice; + } +); + +export const getBurgerIgredientsIdsCount = createSelector( + getBurgerIngredients, + (burgerIngredients) => { + const { bunIngredient, mainIngredients } = burgerIngredients; + const ingredients = bunIngredient + ? [bunIngredient, ...mainIngredients] + : mainIngredients; + + return ingredients.reduce((acc, { _id }) => { + !acc[_id] ? (acc[_id] = 1) : (acc[_id] += 1); + return acc; + }, {}); + } +); + +export default reducer; diff --git a/src/services/ducks/index.js b/src/services/ducks/index.js new file mode 100644 index 0000000..d5f529e --- /dev/null +++ b/src/services/ducks/index.js @@ -0,0 +1,15 @@ +import appReducer from './app'; +import ingredientsReducer from './ingredients'; +import burderIngredientsReducer from './burger-ingredients'; +import ingredientReducer from './ingredient'; +import orderReducer from './order'; + +const rootReducer = { + app: appReducer, + ingredients: ingredientsReducer, + burgerIngredients: burderIngredientsReducer, + ingredient: ingredientReducer, + order: orderReducer, +}; + +export default rootReducer; diff --git a/src/services/ducks/ingredient.js b/src/services/ducks/ingredient.js new file mode 100644 index 0000000..5aae349 --- /dev/null +++ b/src/services/ducks/ingredient.js @@ -0,0 +1,25 @@ +import { createAction, createReducer } from '@reduxjs/toolkit'; +import { ActionPrefix } from '../../utils/constants'; + +// Actions +export const setIngredient = createAction( + `${ActionPrefix.INGREDIENT}/setIngredient` +); + +// Reducer +const initialState = { + ingredient: null, +}; + +const reducer = createReducer(initialState, (builder) => { + builder + .addCase(setIngredient, (state, { payload }) => { + state.ingredient = payload; + }) + .addDefaultCase((state) => state); +}); + +// Selectors +export const getIngredient = ({ ingredient }) => ingredient; + +export default reducer; diff --git a/src/services/ducks/ingredients.js b/src/services/ducks/ingredients.js new file mode 100644 index 0000000..3da71fc --- /dev/null +++ b/src/services/ducks/ingredients.js @@ -0,0 +1,56 @@ +import { + createReducer, + createAsyncThunk, + createSelector, +} from '@reduxjs/toolkit'; +import { ApiRoute } from '../../utils/constants'; +import { setError } from './app'; +import { ActionPrefix } from '../../utils/constants'; + +// Actions +export const fetchAllIngredients = createAsyncThunk( + `${ActionPrefix.INGREDIENTS}/fetchAllIngredients`, + async (_, { dispatch, rejectWithValue, extra: request }) => { + try { + const { data: ingredients } = await request(ApiRoute.INGREDIENTS); + return ingredients; + } catch { + dispatch(setError()); + return rejectWithValue(); + } + } +); + +// Reducer +const initialState = { + ingredientsList: [], + isLoading: true, +}; + +const reducer = createReducer(initialState, (builder) => { + builder + .addCase(fetchAllIngredients.fulfilled, (state, { payload }) => { + state.ingredientsList = payload; + state.isLoading = false; + }) + .addCase(fetchAllIngredients.rejected, (state) => { + state.isLoading = false; + }) + .addDefaultCase((state) => state); +}); + +// Selectors +export const getIngredientsState = ({ ingredients }) => ingredients; +const getAllIngredients = ({ ingredients }) => ingredients.ingredientsList; + +export const getFilteredIngredients = createSelector( + getAllIngredients, + (ingredients) => + ingredients.reduce((acc, ingredient) => { + const { type } = ingredient; + !acc[type] ? (acc[type] = [ingredient]) : acc[type].push(ingredient); + return acc; + }, {}) +); + +export default reducer; diff --git a/src/services/ducks/order.js b/src/services/ducks/order.js new file mode 100644 index 0000000..0bada5b --- /dev/null +++ b/src/services/ducks/order.js @@ -0,0 +1,50 @@ +import { createReducer, createAsyncThunk } from '@reduxjs/toolkit'; +import { ApiRoute } from '../../utils/constants'; +import { setError } from './app'; +import { ActionPrefix } from '../../utils/constants'; + +// Actions +export const sendOrder = createAsyncThunk( + `${ActionPrefix.ORDER}/sendOrder`, + async (ingredientsIds, { dispatch, rejectWithValue, extra: request }) => { + try { + const { + order: { number }, + } = await request(ApiRoute.ORDERS, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ingredients: ingredientsIds }), + }); + return number; + } catch { + dispatch(setError()); + return rejectWithValue(); + } + } +); + +// Reducer +const initialState = { + orderNumber: null, + isLoading: false, +}; + +const reducer = createReducer(initialState, (builder) => { + builder + .addCase(sendOrder.pending, (state) => { + state.isLoading = true; + }) + .addCase(sendOrder.fulfilled, (state, { payload }) => { + state.orderNumber = payload; + state.isLoading = false; + }) + .addCase(sendOrder.rejected, (state) => { + state.isLoading = false; + }) + .addDefaultCase((state) => state); +}); + +// Selectors +export const getOrderState = ({ order }) => order; + +export default reducer; diff --git a/src/services/modules/app.js b/src/services/modules/app.js deleted file mode 100644 index 557fa8a..0000000 --- a/src/services/modules/app.js +++ /dev/null @@ -1,80 +0,0 @@ -// Action types -export const FETCH_INGREDIENTS = 'FETCH_INGREDIENTS'; -export const FETCH_INGREDIENTS_ERROR = 'FETCH_INGREDIENTS_ERROR'; -export const SEND_ORDER_ERROR = 'SEND_ORDER_ERROR'; -export const ADD_BUN_INGREDIENT = 'ADD_BUN_INGREDIENT'; -export const ADD_MAIN_INGREDIENT = 'ADD_MAIN_INGREDIENT'; -export const SET_INGREDIENT = 'SET_INGREDIENT'; -export const SET_ORDER_NUMBER = 'SET_ORDER_NUMBER'; -export const SET_TOTAL_PRICE = 'SET_TOTAL_PRICE'; -export const DELETE_INGREDIENT = 'DELETE_INGREDIENT'; -export const CLOSE_MODAL = 'CLOSE_MODAL'; - -// Reducer -const appReducer = (state, { type, payload = null }) => { - switch (type) { - case 'FETCH_INGREDIENTS': - return { - ...state, - ingredients: payload, - loading: false, - }; - case 'FETCH_INGREDIENTS_ERROR': - case 'SEND_ORDER_ERROR': - return { - ...state, - loading: false, - error: true, - }; - case 'ADD_BUN_INGREDIENT': - return { - ...state, - burgerData: { - ...state.burgerData, - bunIngredient: payload, - }, - }; - case 'ADD_MAIN_INGREDIENT': - return { - ...state, - burgerData: { - ...state.burgerData, - mainIngredients: [...state.burgerData.mainIngredients, payload], - }, - }; - case 'SET_INGREDIENT': - return { - ...state, - ingredient: payload.ingredient, - modalType: payload.modalType, - }; - case 'SET_ORDER_NUMBER': - return { - ...state, - orderNumber: payload.orderNumber, - modalType: payload.modalType, - }; - case 'SET_TOTAL_PRICE': - return { - ...state, - totalPrice: payload, - }; - case 'DELETE_INGREDIENT': - return { - ...state, - burgerData: { - ...state.burgerData, - mainIngredients: payload, - }, - }; - case 'CLOSE_MODAL': - return { - ...state, - modalType: null, - }; - default: - return state; - } -}; - -export default appReducer; diff --git a/src/services/store.js b/src/services/store.js new file mode 100644 index 0000000..463a071 --- /dev/null +++ b/src/services/store.js @@ -0,0 +1,14 @@ +import { configureStore } from '@reduxjs/toolkit'; +import rootReducer from './ducks'; +import request from './api'; + +export const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + thunk: { + extraArgument: request, + }, + }), + devTools: process.env.NODE_ENV !== 'production', +}); diff --git a/src/utils/constants.js b/src/utils/constants.js index fa5a793..2ad9b45 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -3,8 +3,41 @@ export const ModalType = { ORDER: 'order', }; -export const ENDPOINT = 'https://norma.nomoreparties.space/api'; export const ApiRoute = { INGREDIENTS: 'ingredients', ORDERS: 'orders', }; + +export const BunPosition = { + TOP: 'top', + BOTTOM: 'bottom', +}; + +export const IngredientType = { + BUN: 'bun', + SAUCE: 'sauce', + MAIN: 'main', +}; + +export const TAB_ITEMS = [ + { + name: 'Булки', + type: IngredientType.BUN, + }, + { + name: 'Соусы', + type: IngredientType.SAUCE, + }, + { + name: 'Начинки', + type: IngredientType.MAIN, + }, +]; + +export const ActionPrefix = { + APP: 'app', + ORDER: 'app/order', + INGREDIENTS: 'app/ingredients', + INGREDIENT: 'app/ingredient', + BURGER_INGREDIENTS: 'app/burgerIngredients', +}; From 30e1b74fcd1877d85374d7604356c95973206419 Mon Sep 17 00:00:00 2001 From: maXimalist Date: Tue, 7 Dec 2021 20:09:20 +0300 Subject: [PATCH 2/2] sprint-2/step-2 refactoring --- src/components/app/app.jsx | 11 +--- .../burger-constructor/burger-constructor.jsx | 29 +++++++--- .../burger-ingredients/burger-ingredients.jsx | 50 +++++++++++----- src/components/hocs/with-modal.js | 58 ------------------- .../ingredient-details/ingredient-details.jsx | 11 ++-- .../ingredients-list/ingredients-list.jsx | 7 +-- .../order-details/order-details.jsx | 11 ++-- src/services/ducks/app.js | 12 ---- src/services/ducks/burger-ingredients.js | 4 ++ .../{ingredient.js => current-ingredient.js} | 9 +-- src/services/ducks/index.js | 4 +- src/services/ducks/ingredients.js | 1 + src/services/ducks/order.js | 13 ++++- src/services/store.js | 2 +- src/{services => utils}/api.js | 0 src/utils/constants.js | 7 +-- 16 files changed, 97 insertions(+), 132 deletions(-) delete mode 100644 src/components/hocs/with-modal.js rename src/services/ducks/{ingredient.js => current-ingredient.js} (59%) rename src/{services => utils}/api.js (100%) diff --git a/src/components/app/app.jsx b/src/components/app/app.jsx index 76e8556..4626ecc 100644 --- a/src/components/app/app.jsx +++ b/src/components/app/app.jsx @@ -3,7 +3,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import s from './app.module.css'; -import { ModalType } from '../../utils/constants'; import { getIngredientsState, fetchAllIngredients, @@ -17,12 +16,6 @@ import BurgerConstructor from '../burger-constructor/burger-constructor'; import Loader from '../loader/loader'; import Error from '../error/error'; -// HOC -import withModal from '../hocs/with-modal'; - -const WithModalBurgerConstructor = withModal(BurgerConstructor); -const WithModalBurgerIngredients = withModal(BurgerIngredients); - const App = () => { const dispatch = useDispatch(); const { isLoading } = useSelector(getIngredientsState); @@ -45,8 +38,8 @@ const App = () => { ) : (
    - - + +
    )} diff --git a/src/components/burger-constructor/burger-constructor.jsx b/src/components/burger-constructor/burger-constructor.jsx index 75a638a..6246c78 100644 --- a/src/components/burger-constructor/burger-constructor.jsx +++ b/src/components/burger-constructor/burger-constructor.jsx @@ -6,16 +6,22 @@ import { getBurgerIngredients, getTotalPrice, addIngredient, + resetBurgerIngredients, } from '../../services/ducks/burger-ingredients'; -import { sendOrder, getOrderState } from '../../services/ducks/order'; -import { openModal } from '../../services/ducks/app'; -import { BunPosition, ModalType } from '../../utils/constants'; +import { + sendOrder, + resetOrder, + getOrderState, +} from '../../services/ducks/order'; +import { BunPosition } from '../../utils/constants'; // Components import BunIngredient from '../bun-ingredient/bun-ingredient'; import MainIngredientsList from '../main-ingredients-list/main-ingredients-list'; import SubmitSection from '../submit-section/submit-section'; import NoIngredients from '../no-ingredients/no-ingredients'; +import Modal from '../modal/modal'; +import OrderDetails from '../order-details/order-details'; const BurgerConstructor = () => { const dispatch = useDispatch(); @@ -29,16 +35,15 @@ const BurgerConstructor = () => { }), }); const { bunIngredient, mainIngredients } = useSelector(getBurgerIngredients); - const { isLoading } = useSelector(getOrderState); + const { orderNumber, isLoading } = useSelector(getOrderState); const totalPrice = useSelector(getTotalPrice); const isIngredientsExist = bunIngredient || mainIngredients.length > 0; const handleSendOrder = useCallback(async () => { - const ingredientIds = [bunIngredient, ...mainIngredients].map( + const ingredientsIds = [bunIngredient, ...mainIngredients].map( ({ _id }) => _id ); - await dispatch(sendOrder(ingredientIds)); - dispatch(openModal(ModalType.ORDER)); + await dispatch(sendOrder(ingredientsIds)); }, [bunIngredient, mainIngredients, dispatch]); const handleDrop = useCallback( @@ -48,6 +53,11 @@ const BurgerConstructor = () => { [dispatch] ); + const handleCloseModal = useCallback(() => { + dispatch(resetOrder()); + dispatch(resetBurgerIngredients()); + }, [dispatch]); + return (
    { isDisabled={!bunIngredient || isLoading} handleSendOrder={handleSendOrder} /> + {orderNumber && ( + + + + )} )}
    diff --git a/src/components/burger-ingredients/burger-ingredients.jsx b/src/components/burger-ingredients/burger-ingredients.jsx index 1b5693b..d9ee707 100644 --- a/src/components/burger-ingredients/burger-ingredients.jsx +++ b/src/components/burger-ingredients/burger-ingredients.jsx @@ -1,30 +1,50 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import s from './burger-ingredients.module.css'; import { IngredientType } from '../../utils/constants'; +import { + getCurrentIngredient, + setCurrentIngredient, +} from '../../services/ducks/current-ingredient'; // Components import IngredientsTabs from '../ingredients-tabs/ingredients-tabs'; import IngredientsListContainer from '../ingredients-list-container/ingredients-list-container'; +import Modal from '../modal/modal'; +import IngredientDetails from '../ingredient-details/ingredient-details'; const BurgerIngredients = () => { + const dispatch = useDispatch(); + const currentIngredient = useSelector(getCurrentIngredient); const [activeTab, setActiveTab] = useState(IngredientType.BUN); const [isScrollMethod, setIsScrollMethod] = useState(false); + const handleCloseModal = useCallback(() => { + dispatch(setCurrentIngredient(null)); + }, [dispatch]); + return ( -
    -

    Соберите бургер

    - - -
    + <> +
    +

    Соберите бургер

    + + +
    + {currentIngredient && ( + + + + )} + ); }; diff --git a/src/components/hocs/with-modal.js b/src/components/hocs/with-modal.js deleted file mode 100644 index 5780ebc..0000000 --- a/src/components/hocs/with-modal.js +++ /dev/null @@ -1,58 +0,0 @@ -import { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import pt from 'prop-types'; -import { ModalType } from '../../utils/constants'; -import { setIngredient } from '../../services/ducks/ingredient'; -import { getAppState, closeModal } from '../../services/ducks/app'; - -// Components -import Modal from '../modal/modal'; -import OrderDetails from '../order-details/order-details'; -import IngredientDetails from '../ingredient-details/ingredient-details'; - -const withModal = (WrappedComponent) => { - const WithModal = (props) => { - const dispatch = useDispatch(); - const { isModalOpen, modalType } = useSelector(getAppState); - - const handleCloseModal = useCallback(() => { - dispatch(closeModal()); - if (modalType === ModalType.INGREDIENT) { - dispatch(setIngredient(null)); - } - }, [modalType, dispatch]); - - const getComponent = () => { - switch (modalType) { - case ModalType.ORDER: - return ; - case ModalType.INGREDIENT: - return ; - default: - return; - } - }; - - return ( - <> - - {isModalOpen && ( - - {getComponent()} - - )} - - ); - }; - - WithModal.propTypes = { - modalType: pt.string.isRequired, - }; - - return WithModal; -}; - -export default withModal; diff --git a/src/components/ingredient-details/ingredient-details.jsx b/src/components/ingredient-details/ingredient-details.jsx index 35590f1..6d252e5 100644 --- a/src/components/ingredient-details/ingredient-details.jsx +++ b/src/components/ingredient-details/ingredient-details.jsx @@ -1,10 +1,7 @@ -import { useSelector } from 'react-redux'; -import { getIngredient } from '../../services/ducks/ingredient'; +import { PropValidator } from '../../utils/prop-validator'; import s from './ingredient-details.module.css'; -const IngredientDetails = () => { - const { ingredient } = useSelector(getIngredient); - +const IngredientDetails = ({ ingredient }) => { const { image_large, name, @@ -49,4 +46,8 @@ const IngredientDetails = () => { ); }; +IngredientDetails.propTypes = { + ingredient: PropValidator.INGREDIENT.isRequired, +}; + export default IngredientDetails; diff --git a/src/components/ingredients-list/ingredients-list.jsx b/src/components/ingredients-list/ingredients-list.jsx index 9df9ab3..8c7c645 100644 --- a/src/components/ingredients-list/ingredients-list.jsx +++ b/src/components/ingredients-list/ingredients-list.jsx @@ -3,10 +3,8 @@ import { useSelector, useDispatch } from 'react-redux'; import pt from 'prop-types'; import { PropValidator } from '../../utils/prop-validator'; import s from './ingredients-list.module.css'; -import { ModalType } from '../../utils/constants'; import { getBurgerIgredientsIdsCount } from '../../services/ducks/burger-ingredients'; -import { setIngredient } from '../../services/ducks/ingredient'; -import { openModal } from '../../services/ducks/app'; +import { setCurrentIngredient } from '../../services/ducks/current-ingredient'; // Components import IngredientItem from '../ingredient-item/ingredient-item'; @@ -17,8 +15,7 @@ const IngredientsList = forwardRef(({ title, ingredients }, ref) => { const handleClickIngredientItem = useCallback( (ingredient) => { - dispatch(setIngredient(ingredient)); - dispatch(openModal(ModalType.INGREDIENT)); + dispatch(setCurrentIngredient(ingredient)); }, [dispatch] ); diff --git a/src/components/order-details/order-details.jsx b/src/components/order-details/order-details.jsx index 89f8867..977f1a2 100644 --- a/src/components/order-details/order-details.jsx +++ b/src/components/order-details/order-details.jsx @@ -1,11 +1,8 @@ -import { useSelector } from 'react-redux'; +import pt from 'prop-types'; import OrderAccepted from '../../images/done.gif'; import s from './order-details.module.css'; -import { getOrderState } from '../../services/ducks/order'; - -const OrderDetails = () => { - const { orderNumber } = useSelector(getOrderState); +const OrderDetails = ({ orderNumber }) => { return (

    {orderNumber}

    @@ -21,4 +18,8 @@ const OrderDetails = () => { ); }; +OrderDetails.propTypes = { + orderNumber: pt.number.isRequired, +}; + export default OrderDetails; diff --git a/src/services/ducks/app.js b/src/services/ducks/app.js index 8e8f324..6510c66 100644 --- a/src/services/ducks/app.js +++ b/src/services/ducks/app.js @@ -3,28 +3,16 @@ import { ActionPrefix } from '../../utils/constants'; // Actions export const setError = createAction(`${ActionPrefix.APP}/setError`); -export const openModal = createAction(`${ActionPrefix.APP}/openModal`); -export const closeModal = createAction(`${ActionPrefix.APP}/closeModal`); // Reducer const initialState = { isError: false, - isModalOpen: false, - modalType: null, }; const appReducer = createReducer(initialState, (builder) => { builder .addCase(setError, (state) => { state.isError = true; }) - .addCase(openModal, (state, { payload }) => { - state.isModalOpen = true; - state.modalType = payload; - }) - .addCase(closeModal, (state) => { - state.isModalOpen = false; - state.modalType = null; - }) .addDefaultCase((state) => state); }); diff --git a/src/services/ducks/burger-ingredients.js b/src/services/ducks/burger-ingredients.js index 45412eb..e90c96d 100644 --- a/src/services/ducks/burger-ingredients.js +++ b/src/services/ducks/burger-ingredients.js @@ -12,6 +12,9 @@ export const removeIngredient = createAction( export const sortMainIngredients = createAction( `${ActionPrefix.BURGER_INGREDIENTS}/sortMainIngredients` ); +export const resetBurgerIngredients = createAction( + `${ActionPrefix.BURGER_INGREDIENTS}/resetBurgerIngredients` +); // Reducer const initialState = { @@ -44,6 +47,7 @@ const reducer = createReducer(initialState, (builder) => { .addCase(removeIngredient, (state, { payload: ingredientIndex }) => { state.burgerData.mainIngredients.splice(ingredientIndex, 1); }) + .addCase(resetBurgerIngredients, () => initialState) .addDefaultCase((state) => state); }); diff --git a/src/services/ducks/ingredient.js b/src/services/ducks/current-ingredient.js similarity index 59% rename from src/services/ducks/ingredient.js rename to src/services/ducks/current-ingredient.js index 5aae349..11c2341 100644 --- a/src/services/ducks/ingredient.js +++ b/src/services/ducks/current-ingredient.js @@ -2,8 +2,8 @@ import { createAction, createReducer } from '@reduxjs/toolkit'; import { ActionPrefix } from '../../utils/constants'; // Actions -export const setIngredient = createAction( - `${ActionPrefix.INGREDIENT}/setIngredient` +export const setCurrentIngredient = createAction( + `${ActionPrefix.CURRENT_INGREDIENT}/setCurrentIngredient` ); // Reducer @@ -13,13 +13,14 @@ const initialState = { const reducer = createReducer(initialState, (builder) => { builder - .addCase(setIngredient, (state, { payload }) => { + .addCase(setCurrentIngredient, (state, { payload }) => { state.ingredient = payload; }) .addDefaultCase((state) => state); }); // Selectors -export const getIngredient = ({ ingredient }) => ingredient; +export const getCurrentIngredient = ({ currentIngredient }) => + currentIngredient.ingredient; export default reducer; diff --git a/src/services/ducks/index.js b/src/services/ducks/index.js index d5f529e..469b478 100644 --- a/src/services/ducks/index.js +++ b/src/services/ducks/index.js @@ -1,14 +1,14 @@ import appReducer from './app'; import ingredientsReducer from './ingredients'; import burderIngredientsReducer from './burger-ingredients'; -import ingredientReducer from './ingredient'; +import ingredientReducer from './current-ingredient'; import orderReducer from './order'; const rootReducer = { app: appReducer, ingredients: ingredientsReducer, burgerIngredients: burderIngredientsReducer, - ingredient: ingredientReducer, + currentIngredient: ingredientReducer, order: orderReducer, }; diff --git a/src/services/ducks/ingredients.js b/src/services/ducks/ingredients.js index 3da71fc..45a05b0 100644 --- a/src/services/ducks/ingredients.js +++ b/src/services/ducks/ingredients.js @@ -34,6 +34,7 @@ const reducer = createReducer(initialState, (builder) => { state.isLoading = false; }) .addCase(fetchAllIngredients.rejected, (state) => { + state.ingredientsList = []; state.isLoading = false; }) .addDefaultCase((state) => state); diff --git a/src/services/ducks/order.js b/src/services/ducks/order.js index 0bada5b..dd89886 100644 --- a/src/services/ducks/order.js +++ b/src/services/ducks/order.js @@ -1,9 +1,15 @@ -import { createReducer, createAsyncThunk } from '@reduxjs/toolkit'; +import { + createReducer, + createAsyncThunk, + createAction, +} from '@reduxjs/toolkit'; import { ApiRoute } from '../../utils/constants'; import { setError } from './app'; import { ActionPrefix } from '../../utils/constants'; // Actions +export const resetOrder = createAction(`${ActionPrefix.ORDER}/resetOrder`); + export const sendOrder = createAsyncThunk( `${ActionPrefix.ORDER}/sendOrder`, async (ingredientsIds, { dispatch, rejectWithValue, extra: request }) => { @@ -38,8 +44,9 @@ const reducer = createReducer(initialState, (builder) => { state.orderNumber = payload; state.isLoading = false; }) - .addCase(sendOrder.rejected, (state) => { - state.isLoading = false; + .addCase(sendOrder.rejected, () => initialState) + .addCase(resetOrder, (state) => { + state.orderNumber = null; }) .addDefaultCase((state) => state); }); diff --git a/src/services/store.js b/src/services/store.js index 463a071..b63a231 100644 --- a/src/services/store.js +++ b/src/services/store.js @@ -1,6 +1,6 @@ import { configureStore } from '@reduxjs/toolkit'; import rootReducer from './ducks'; -import request from './api'; +import request from '../utils/api'; export const store = configureStore({ reducer: rootReducer, diff --git a/src/services/api.js b/src/utils/api.js similarity index 100% rename from src/services/api.js rename to src/utils/api.js diff --git a/src/utils/constants.js b/src/utils/constants.js index 2ad9b45..118f47a 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -1,8 +1,3 @@ -export const ModalType = { - INGREDIENT: 'ingredient', - ORDER: 'order', -}; - export const ApiRoute = { INGREDIENTS: 'ingredients', ORDERS: 'orders', @@ -38,6 +33,6 @@ export const ActionPrefix = { APP: 'app', ORDER: 'app/order', INGREDIENTS: 'app/ingredients', - INGREDIENT: 'app/ingredient', + CURRENT_INGREDIENT: 'app/currentIngredient', BURGER_INGREDIENTS: 'app/burgerIngredients', };