-
- {totalPrice}
-
-
-
+
+
+
+
+
+ {orderNumber && (
+
+
+
+ )}
>
)}
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..d9ee707 100644
--- a/src/components/burger-ingredients/burger-ingredients.jsx
+++ b/src/components/burger-ingredients/burger-ingredients.jsx
@@ -1,117 +1,51 @@
-import { useState, useCallback, useMemo, useContext, memo } from 'react';
-import { Tab } from '@ya.praktikum/react-developer-burger-ui-components';
+import { useState, useCallback } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
import s from './burger-ingredients.module.css';
-import { AppContext } from '../../services/app-context';
-import { ModalType } from '../../utils/constants';
+import { IngredientType } from '../../utils/constants';
import {
- SET_INGREDIENT,
- ADD_BUN_INGREDIENT,
- ADD_MAIN_INGREDIENT,
-} from '../../services/modules/app';
+ getCurrentIngredient,
+ setCurrentIngredient,
+} from '../../services/ducks/current-ingredient';
// 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';
+import Modal from '../modal/modal';
+import IngredientDetails from '../ingredient-details/ingredient-details';
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;
- }
+ const dispatch = useDispatch();
+ const currentIngredient = useSelector(getCurrentIngredient);
+ const [activeTab, setActiveTab] = useState(IngredientType.BUN);
+ const [isScrollMethod, setIsScrollMethod] = useState(false);
- 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 handleCloseModal = useCallback(() => {
+ dispatch(setCurrentIngredient(null));
+ }, [dispatch]);
return (
-
- Соберите бургер
-
- {useMemo(
- () =>
- tabItems.map(({ name, type }) => (
- setCurrent(type)}
- >
- {name}
-
- )),
- [current]
- )}
-
-
- {tabItems.map(({ name, type }) => (
-
- ))}
-
-
+ <>
+
+ {currentIngredient && (
+
+
+
+ )}
+ >
);
};
-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
deleted file mode 100644
index 153e955..0000000
--- a/src/components/hocs/with-modal.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { useContext } from 'react';
-import { ModalType } from '../../utils/constants';
-import { AppContext } from '../../services/app-context';
-import { CLOSE_MODAL } from '../../services/modules/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 { modalType, orderNumber, ingredient, dispatch } = useContext(
- AppContext
- );
-
- const handleCloseModal = () => {
- dispatch({ type: CLOSE_MODAL });
- };
-
- const getComponent = () => {
- switch (modalType) {
- case ModalType.ORDER:
- return
;
- case ModalType.INGREDIENT:
- return
;
- default:
- return;
- }
- };
-
- return (
- <>
-
- {modalType && (
-
- {getComponent()}
-
- )}
- >
- );
- };
-
- return WithModal;
-};
-
-export default withModal;
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)}
>
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..8c7c645 100644
--- a/src/components/ingredients-list/ingredients-list.jsx
+++ b/src/components/ingredients-list/ingredients-list.jsx
@@ -1,42 +1,47 @@
-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 { getBurgerIgredientsIdsCount } from '../../services/ducks/burger-ingredients';
+import { setCurrentIngredient } from '../../services/ducks/current-ingredient';
// 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(setCurrentIngredient(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/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/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..6510c66
--- /dev/null
+++ b/src/services/ducks/app.js
@@ -0,0 +1,22 @@
+import { createAction, createReducer } from '@reduxjs/toolkit';
+import { ActionPrefix } from '../../utils/constants';
+
+// Actions
+export const setError = createAction(`${ActionPrefix.APP}/setError`);
+
+// Reducer
+const initialState = {
+ isError: false,
+};
+const appReducer = createReducer(initialState, (builder) => {
+ builder
+ .addCase(setError, (state) => {
+ state.isError = true;
+ })
+ .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..e90c96d
--- /dev/null
+++ b/src/services/ducks/burger-ingredients.js
@@ -0,0 +1,89 @@
+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`
+);
+export const resetBurgerIngredients = createAction(
+ `${ActionPrefix.BURGER_INGREDIENTS}/resetBurgerIngredients`
+);
+
+// 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);
+ })
+ .addCase(resetBurgerIngredients, () => initialState)
+ .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/current-ingredient.js b/src/services/ducks/current-ingredient.js
new file mode 100644
index 0000000..11c2341
--- /dev/null
+++ b/src/services/ducks/current-ingredient.js
@@ -0,0 +1,26 @@
+import { createAction, createReducer } from '@reduxjs/toolkit';
+import { ActionPrefix } from '../../utils/constants';
+
+// Actions
+export const setCurrentIngredient = createAction(
+ `${ActionPrefix.CURRENT_INGREDIENT}/setCurrentIngredient`
+);
+
+// Reducer
+const initialState = {
+ ingredient: null,
+};
+
+const reducer = createReducer(initialState, (builder) => {
+ builder
+ .addCase(setCurrentIngredient, (state, { payload }) => {
+ state.ingredient = payload;
+ })
+ .addDefaultCase((state) => state);
+});
+
+// Selectors
+export const getCurrentIngredient = ({ currentIngredient }) =>
+ currentIngredient.ingredient;
+
+export default reducer;
diff --git a/src/services/ducks/index.js b/src/services/ducks/index.js
new file mode 100644
index 0000000..469b478
--- /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 './current-ingredient';
+import orderReducer from './order';
+
+const rootReducer = {
+ app: appReducer,
+ ingredients: ingredientsReducer,
+ burgerIngredients: burderIngredientsReducer,
+ currentIngredient: ingredientReducer,
+ order: orderReducer,
+};
+
+export default rootReducer;
diff --git a/src/services/ducks/ingredients.js b/src/services/ducks/ingredients.js
new file mode 100644
index 0000000..45a05b0
--- /dev/null
+++ b/src/services/ducks/ingredients.js
@@ -0,0 +1,57 @@
+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.ingredientsList = [];
+ 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..dd89886
--- /dev/null
+++ b/src/services/ducks/order.js
@@ -0,0 +1,57 @@
+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 }) => {
+ 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, () => initialState)
+ .addCase(resetOrder, (state) => {
+ state.orderNumber = null;
+ })
+ .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..b63a231
--- /dev/null
+++ b/src/services/store.js
@@ -0,0 +1,14 @@
+import { configureStore } from '@reduxjs/toolkit';
+import rootReducer from './ducks';
+import request from '../utils/api';
+
+export const store = configureStore({
+ reducer: rootReducer,
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware({
+ thunk: {
+ extraArgument: request,
+ },
+ }),
+ devTools: process.env.NODE_ENV !== 'production',
+});
diff --git a/src/utils/api.js b/src/utils/api.js
new file mode 100644
index 0000000..f47d0ba
--- /dev/null
+++ b/src/utils/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/utils/constants.js b/src/utils/constants.js
index fa5a793..118f47a 100644
--- a/src/utils/constants.js
+++ b/src/utils/constants.js
@@ -1,10 +1,38 @@
-export const ModalType = {
- INGREDIENT: 'ingredient',
- 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',
+ CURRENT_INGREDIENT: 'app/currentIngredient',
+ BURGER_INGREDIENTS: 'app/burgerIngredients',
+};