Repository: AsadovEZO/react-burger
Pull request #4: The conditions of sprint-2 are met
Author: login: AsadovEZO
Created: 2025-05-02T10:50:03Z | Merged: 2025-05-18T06:20:10Z

Требования реализованы. 
В ряде мест использовал TypeScript (изучал, как он работает). 


Diff:
diff --git a/package-lock.json b/package-lock.json
index 9346cc2..6729d82 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
       "name": "react-burger",
       "version": "0.1.0",
       "dependencies": {
+        "@reduxjs/toolkit": "^2.7.0",
         "@testing-library/dom": "^10.4.0",
         "@testing-library/jest-dom": "^6.6.3",
         "@testing-library/react": "^16.3.0",
@@ -19,8 +20,13 @@
         "@ya.praktikum/react-developer-burger-ui-components": "^1.14.2",
         "prop-types": "^15.8.1",
         "react": "^18.3.1",
+        "react-dnd": "^16.0.1",
+        "react-dnd-html5-backend": "^16.0.1",
         "react-dom": "^18.3.1",
+        "react-redux": "^9.2.0",
         "react-scripts": "^5.0.1",
+        "redux": "^5.0.1",
+        "redux-thunk": "^3.1.0",
         "typescript": "^4.9.5",
         "web-vitals": "^2.1.4"
       },
@@ -3100,6 +3106,60 @@
         }
       }
     },
+    "node_modules/@react-dnd/asap": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
+      "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==",
+      "license": "MIT"
+    },
+    "node_modules/@react-dnd/invariant": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
+      "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==",
+      "license": "MIT"
+    },
+    "node_modules/@react-dnd/shallowequal": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
+      "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
+      "license": "MIT"
+    },
+    "node_modules/@reduxjs/toolkit": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.7.0.tgz",
+      "integrity": "sha512-XVwolG6eTqwV0N8z/oDlN93ITCIGIop6leXlGJI/4EKy+0POYkR+ABHRSdGXY+0MQvJBP8yAzh+EYFxTuvmBiQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.0.0",
+        "@standard-schema/utils": "^0.3.0",
+        "immer": "^10.0.3",
+        "redux": "^5.0.1",
+        "redux-thunk": "^3.1.0",
+        "reselect": "^5.1.0"
+      },
+      "peerDependencies": {
+        "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+        "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "react": {
+          "optional": true
+        },
+        "react-redux": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@reduxjs/toolkit/node_modules/immer": {
+      "version": "10.1.1",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+      "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/immer"
+      }
+    },
     "node_modules/@rollup/plugin-babel": {
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3215,6 +3275,18 @@
         "@sinonjs/commons": "^1.7.0"
       }
     },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+      "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+      "license": "MIT"
+    },
+    "node_modules/@standard-schema/utils": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+      "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+      "license": "MIT"
+    },
     "node_modules/@surma/rollup-plugin-off-main-thread": {
       "version": "2.2.3",
       "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@@ -3938,6 +4010,12 @@
       "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
       "license": "MIT"
     },
+    "node_modules/@types/use-sync-external-store": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+      "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+      "license": "MIT"
+    },
     "node_modules/@types/ws": {
       "version": "8.18.1",
       "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -6714,6 +6792,26 @@
       "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
       "license": "MIT"
     },
+    "node_modules/dnd-core": {
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
+      "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
+      "license": "MIT",
+      "dependencies": {
+        "@react-dnd/asap": "^5.0.1",
+        "@react-dnd/invariant": "^4.0.1",
+        "redux": "^4.2.0"
+      }
+    },
+    "node_modules/dnd-core/node_modules/redux": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+      "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.9.2"
+      }
+    },
     "node_modules/dns-packet": {
       "version": "5.6.1",
       "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
@@ -8915,6 +9013,21 @@
         "he": "bin/he"
       }
     },
+    "node_modules/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==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "react-is": "^16.7.0"
+      }
+    },
+    "node_modules/hoist-non-react-statics/node_modules/react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+      "license": "MIT"
+    },
     "node_modules/hoopy": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -13973,6 +14086,45 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/react-dnd": {
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
+      "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@react-dnd/invariant": "^4.0.1",
+        "@react-dnd/shallowequal": "^4.0.1",
+        "dnd-core": "^16.0.1",
+        "fast-deep-equal": "^3.1.3",
+        "hoist-non-react-statics": "^3.3.2"
+      },
+      "peerDependencies": {
+        "@types/hoist-non-react-statics": ">= 3.3.1",
+        "@types/node": ">= 12",
+        "@types/react": ">= 16",
+        "react": ">= 16.14"
+      },
+      "peerDependenciesMeta": {
+        "@types/hoist-non-react-statics": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-dnd-html5-backend": {
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
+      "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
+      "license": "MIT",
+      "dependencies": {
+        "dnd-core": "^16.0.1"
+      }
+    },
     "node_modules/react-dom": {
       "version": "18.3.1",
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -13998,6 +14150,29 @@
       "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
       "license": "MIT"
     },
+    "node_modules/react-redux": {
+      "version": "9.2.0",
+      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+      "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/use-sync-external-store": "^0.0.6",
+        "use-sync-external-store": "^1.4.0"
+      },
+      "peerDependencies": {
+        "@types/react": "^18.2.25 || ^19",
+        "react": "^18.0 || ^19",
+        "redux": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "redux": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/react-refresh": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -14140,6 +14315,21 @@
         "node": ">=8"
       }
     },
+    "node_modules/redux": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+      "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+      "license": "MIT"
+    },
+    "node_modules/redux-thunk": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+      "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "redux": "^5.0.0"
+      }
+    },
     "node_modules/reflect.getprototypeof": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -14314,6 +14504,12 @@
       "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
       "license": "MIT"
     },
+    "node_modules/reselect": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+      "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+      "license": "MIT"
+    },
     "node_modules/resolve": {
       "version": "1.22.10",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -16604,6 +16800,15 @@
         "requires-port": "^1.0.0"
       }
     },
+    "node_modules/use-sync-external-store": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+      "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index f597c7b..953f935 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@reduxjs/toolkit": "^2.7.0",
     "@testing-library/dom": "^10.4.0",
     "@testing-library/jest-dom": "^6.6.3",
     "@testing-library/react": "^16.3.0",
@@ -14,8 +15,13 @@
     "@ya.praktikum/react-developer-burger-ui-components": "^1.14.2",
     "prop-types": "^15.8.1",
     "react": "^18.3.1",
+    "react-dnd": "^16.0.1",
+    "react-dnd-html5-backend": "^16.0.1",
     "react-dom": "^18.3.1",
+    "react-redux": "^9.2.0",
     "react-scripts": "^5.0.1",
+    "redux": "^5.0.1",
+    "redux-thunk": "^3.1.0",
     "typescript": "^4.9.5",
     "web-vitals": "^2.1.4"
   },
diff --git a/src/App.jsx b/src/App.jsx
index 5df62b4..1fed411 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,58 +1,23 @@
-import { useState, useEffect } from "react";
+import { useEffect } from "react";
 import styles from "./App.module.css";
+import { useDispatch, useSelector } from "react-redux";
+import { DndProvider } from "react-dnd";
+import { HTML5Backend } from "react-dnd-html5-backend";
 
 import AppHeader from "./components/app-header/app-header";
 import BurgerIngredients from "./components/burger-ingredients/burger-ingredients";
 import BurgerConstructor from "./components/burger-constructor/burger-constructor";
-import { useUserBurger } from "./hooks/use-user-burger";
+import { fetchIngredients } from "./services/burger-ingredients-slice";
 
 function App() {
-  // Кастомный хук для упрощения кода в App.jsx
-  const { selectedIngredients, handleAdd, handleRemove } = useUserBurger();
-
-  const [state, setState] = useState({
-    isLoading: false,
-    hasError: false,
-    data: [],
-  });
-
-  // Получение данных
-  const url = "https://norma.nomoreparties.space/api/ingredients";
-
-  const getData = () => {
-    setState({ ...state, hasError: false, isLoading: true });
-    fetch(url)
-      .then((res) => {
-        if (res.ok) {
-          return res.json();
-        }
-        return Promise.reject(
-          new Error(`Ошибка ${res.status}: ${res.statusText}`)
-        );
-      })
-      .then((data) => {
-        if (!data.success) {
-          throw new Error("Данные с сервера не содержат success: true");
-        }
-        setState({
-          ...state,
-          data: data.data,
-          hasError: !data.success,
-          isLoading: false,
-        });
-      })
-      .catch((e) => {
-        console.log("Ошибка при загрузке данных:", e.message);
-        setState({ ...state, hasError: true, isLoading: false });
-      });
-  };
+  const dispatch = useDispatch();
+  const hasError = useSelector((state) => state.burgerIngredients.hasError);
 
   useEffect(() => {
-    getData();
-  }, []);
+    dispatch(fetchIngredients());
+  }, [dispatch]);
 
-  // Если на каком-то этапе возникла ошибка (получили флаг в state.hasError) - возвращаем уведомлеие для пользователей
-  if (state.hasError) {
+  if (hasError) {
     return (
       <section className={styles.app}>
         <h1 className="text text_type_main-large">Упс! Куда все подевалось?</h1>
@@ -68,19 +33,14 @@ function App() {
     <div className={styles.app}>
       <AppHeader />
       <main className={styles.main}>
-        <section className={styles.leftBlock}>
-          <BurgerIngredients
-            ingredients={state.data}
-            onAdd={handleAdd}
-            selectedIngredients={selectedIngredients}
-          />
-        </section>
-        <section className={styles.rightBlock}>
-          <BurgerConstructor
-            selectedIngredients={selectedIngredients}
-            onRemove={handleRemove}
-          />
-        </section>
+        <DndProvider backend={HTML5Backend}>
+          <section className={styles.leftBlock}>
+            <BurgerIngredients />
+          </section>
+          <section className={styles.rightBlock}>
+            <BurgerConstructor />
+          </section>
+        </DndProvider>
       </main>
     </div>
   );
diff --git a/src/components/burger-constructor/bun-element.jsx b/src/components/burger-constructor/bun-element.jsx
new file mode 100644
index 0000000..dd88e1c
--- /dev/null
+++ b/src/components/burger-constructor/bun-element.jsx
@@ -0,0 +1,37 @@
+import PropTypes from "prop-types";
+
+import { IngredientType } from "../../utils/types.js";
+import { ConstructorElement } from "@ya.praktikum/react-developer-burger-ui-components";
+import styles from "./burger-constructor.module.css";
+
+function BunElement({ bun, position }) {
+  if (!bun) {
+    return (
+      <section className={styles.bunPlaceholder}>
+        <p className="text text_type_main-medium">Выберите булку</p>
+      </section>
+    );
+  }
+
+  const type = position === "top" ? "top" : "bottom";
+  const label = position === "top" ? "(верх)" : "(низ)";
+
+  return (
+    <section className={styles.bun}>
+      <ConstructorElement
+        type={type}
+        isLocked
+        text={`${bun.name} ${label}`}
+        price={bun.price}
+        thumbnail={bun.image}
+      />
+    </section>
+  );
+}
+
+BunElement.propTypes = {
+  bun: IngredientType,
+  position: PropTypes.oneOf(["top", "bottom"]),
+};
+
+export default BunElement;
diff --git a/src/components/burger-constructor/burger-constructor.jsx b/src/components/burger-constructor/burger-constructor.jsx
index 48f82b2..a31e072 100644
--- a/src/components/burger-constructor/burger-constructor.jsx
+++ b/src/components/burger-constructor/burger-constructor.jsx
@@ -1,105 +1,68 @@
-import PropTypes from "prop-types";
-import {
-  Button,
-  CurrencyIcon,
-  ConstructorElement,
-  DragIcon,
-} from "@ya.praktikum/react-developer-burger-ui-components";
-import constructorStyles from "./burger-constructor.module.css";
-import { IngredientType } from "../../utils/types.js";
-import { useModal } from "../../hooks/show-modal";
+import { useDispatch, useSelector } from "react-redux";
+import { useDrop } from "react-dnd";
+
 import Modal from "../modal/modal";
+import BunElement from "./bun-element";
+import IngredientsList from "./ingredients-list";
+import TotalSection from "./total-section";
+import constructorStyles from "./burger-constructor.module.css";
 import OrderDetails from "../order-details/order-details";
+import { postOrder, hideOrderModal } from "../../services/order-details-slice";
+import { handleAdd } from "../../services/burger-constructor-slice";
 
-function BurgerConstructor({ selectedIngredients, onRemove }) {
-  const [isShowingModal, toggleModal] = useModal();
+function BurgerConstructor() {
+  const selectedIngredients = useSelector((state) => state.burgerConstructor);
   const bun = selectedIngredients.find((item) => item.type === "bun");
   const otherIngredients = selectedIngredients.filter(
     (item) => item.type !== "bun"
   );
 
+  const isShowingOrderModal = useSelector(
+    (state) => state.orderDetails.isShowingModal
+  );
+
+  const dispatch = useDispatch();
+
+  const handleOrderClick = () => {
+    dispatch(
+      postOrder(selectedIngredients.map((ingredient) => ingredient._id))
+    );
+  };
+
+  const [{ isHover }, dropNew] = useDrop({
+    accept: "addIngredient",
+    collect: (monitor) => ({
+      isHover: monitor.isOver(),
+    }),
+    drop({ ingredient }) {
+      dispatch(handleAdd(ingredient));
+    },
+  });
+
   const totalPrice = selectedIngredients.reduce((acc, item) => {
-    const price = item.type === "bun" ? item.price * 2 : item.price;
+    const price = item
+      ? item.type === "bun"
+        ? item.price * 2
+        : item.price
+      : 0;
     return acc + price;
   }, 0);
 
   return (
-    <div className={constructorStyles.constructor}>
-      {bun ? (
-        <section className={constructorStyles.bun}>
-          <ConstructorElement
-            type="top"
-            isLocked={true}
-            text={`${bun.name} (верх)`}
-            price={bun.price}
-            thumbnail={bun.image}
-          />
-        </section>
-      ) : (
-        <section className={constructorStyles.bunPlaceholder}>
-          <p className="text text_type_main-medium">Выберите булку</p>
-        </section>
-      )}
+    <div
+      className={constructorStyles.constructor}
+      ref={dropNew}
+      style={{ isHover }}
+    >
+      <BunElement bun={bun} position="top" />
+      <IngredientsList ingredients={otherIngredients} />
+      <BunElement bun={bun} position="bottom" />
 
-      <section className={constructorStyles.ingredientsList}>
-        {otherIngredients.length > 0 ? (
-          otherIngredients.map((ingredient, index) => (
-            <div
-              key={ingredient.uniqueId}
-              className={constructorStyles.ingredientItem}
-            >
-              <DragIcon type="primary" />
-              <ConstructorElement
-                text={ingredient.name}
-                price={ingredient.price}
-                thumbnail={ingredient.image}
-                handleClose={() => onRemove(ingredient)}
-              />
-            </div>
-          ))
-        ) : (
-          <p
-            className="text text_type_main-medium"
-            style={{ textAlign: "center" }}
-          >
-            Добавьте ингредиенты
-          </p>
-        )}
-      </section>
+      <TotalSection totalPrice={totalPrice} onOrder={handleOrderClick} />
 
-      {bun ? (
-        <section className={constructorStyles.bun}>
-          <ConstructorElement
-            type="bottom"
-            isLocked={true}
-            text={`${bun.name} (низ)`}
-            price={bun.price}
-            thumbnail={bun.image}
-          />
-        </section>
-      ) : (
-        <section className={constructorStyles.bunPlaceholder}>
-          <p className="text text_type_main-medium">Выберите булку</p>
-        </section>
-      )}
-
-      <section className={constructorStyles.total}>
-        <section className={constructorStyles.price}>
-          <span className="text text_type_digits-medium">{totalPrice}</span>
-          <CurrencyIcon type="primary" />
-        </section>
-        <Button
-          htmlType="button"
-          type="primary"
-          size="medium"
-          onClick={toggleModal}
-        >
-          Оформить заказ
-        </Button>
-      </section>
-      {isShowingModal && (
+      {isShowingOrderModal && (
         <Modal
-          onCloseButtonClick={toggleModal}
+          onCloseButtonClick={() => dispatch(hideOrderModal())}
           type="order"
           children={<OrderDetails />}
         />
@@ -108,9 +71,4 @@ function BurgerConstructor({ selectedIngredients, onRemove }) {
   );
 }
 
-BurgerConstructor.propTypes = {
-  selectedIngredients: PropTypes.arrayOf(IngredientType).isRequired,
-  onRemove: PropTypes.func.isRequired,
-};
-
 export default BurgerConstructor;
diff --git a/src/components/burger-constructor/ingredient-element.jsx b/src/components/burger-constructor/ingredient-element.jsx
new file mode 100644
index 0000000..9c50844
--- /dev/null
+++ b/src/components/burger-constructor/ingredient-element.jsx
@@ -0,0 +1,70 @@
+import { useRef } from "react";
+import { useDispatch } from "react-redux";
+import { useDrag, useDrop } from "react-dnd";
+
+import { IngredientType } from "../../utils/types.js";
+import {
+  handleMove,
+  handleRemove,
+} from "../../services/burger-constructor-slice";
+import styles from "./burger-constructor.module.css";
+import {
+  ConstructorElement,
+  DragIcon,
+} from "@ya.praktikum/react-developer-burger-ui-components";
+import PropTypes from "prop-types";
+
+function IngredientElement({ ingredient, newIndex }) {
+  const dispatch = useDispatch();
+  const ref = useRef(null);
+
+  const [{ opacity }, drag] = useDrag({
+    type: "moveIngredient",
+    item: { ingredient, index: newIndex },
+    collect: (monitor) => ({
+      opacity: monitor.isDragging() ? 0.5 : 1,
+    }),
+  });
+
+  const [, drop] = useDrop({
+    accept: "moveIngredient",
+    collect: (monitor) => ({
+      isHover: monitor.isOver(),
+    }),
+    hover(draggedItem) {
+      dispatch(
+        handleMove({
+          ingredient: draggedItem.ingredient,
+          toIndex: newIndex,
+        })
+      );
+    },
+  });
+
+  drag(drop(ref));
+
+  if (!ingredient) {
+    return null;
+  }
+
+  return (
+    <section className={styles.ingredientsList} style={{ opacity }} ref={ref}>
+      <div key={ingredient.uniqueId} className={styles.ingredientItem}>
+        <DragIcon type="primary" />
+        <ConstructorElement
+          text={ingredient.name}
+          price={ingredient.price}
+          thumbnail={ingredient.image}
+          handleClose={() => dispatch(handleRemove(ingredient))}
+        />
+      </div>
+    </section>
+  );
+}
+
+IngredientElement.propTypes = {
+  ingredient: IngredientType,
+  newIndex: PropTypes.number.isRequired,
+};
+
+export default IngredientElement;
diff --git a/src/components/burger-constructor/ingredients-list.jsx b/src/components/burger-constructor/ingredients-list.jsx
new file mode 100644
index 0000000..52324aa
--- /dev/null
+++ b/src/components/burger-constructor/ingredients-list.jsx
@@ -0,0 +1,34 @@
+import PropTypes from "prop-types";
+
+import { IngredientType } from "../../utils/types.js";
+
+import styles from "./burger-constructor.module.css";
+import IngredientElement from "./ingredient-element.jsx";
+
+function IngredientsList({ ingredients }) {
+  if (!ingredients || !Array.isArray(ingredients) || ingredients.length === 0) {
+    return (
+      <p className="text text_type_main-medium" style={{ textAlign: "center" }}>
+        Добавьте ингредиенты
+      </p>
+    );
+  }
+
+  return (
+    <section className={styles.ingredientsList}>
+      {ingredients.map((ingredient, index) => (
+        <IngredientElement
+          key={ingredient.uniqueId || `${ingredient._id}-${index}`}
+          ingredient={ingredient}
+          newIndex={index}
+        />
+      ))}
+    </section>
+  );
+}
+
+IngredientsList.propTypes = {
+  ingredients: PropTypes.arrayOf(IngredientType),
+};
+
+export default IngredientsList;
diff --git a/src/components/burger-constructor/total-section.jsx b/src/components/burger-constructor/total-section.jsx
new file mode 100644
index 0000000..5b62e7a
--- /dev/null
+++ b/src/components/burger-constructor/total-section.jsx
@@ -0,0 +1,28 @@
+import PropTypes from "prop-types";
+
+import {
+  Button,
+  CurrencyIcon,
+} from "@ya.praktikum/react-developer-burger-ui-components";
+import styles from "./burger-constructor.module.css";
+
+function TotalSection({ totalPrice, onOrder }) {
+  return (
+    <section className={styles.total}>
+      <section className={styles.price}>
+        <span className="text text_type_digits-medium">{totalPrice}</span>
+        <CurrencyIcon type="primary" />
+      </section>
+      <Button htmlType="button" type="primary" size="medium" onClick={onOrder}>
+        Оформить заказ
+      </Button>
+    </section>
+  );
+}
+
+TotalSection.propTypes = {
+  totalPrice: PropTypes.number,
+  onOrder: PropTypes.func.isRequired,
+};
+
+export default TotalSection;
diff --git a/src/components/burger-ingredients/burger-ingredients.jsx b/src/components/burger-ingredients/burger-ingredients.jsx
index 3103146..30ccff9 100644
--- a/src/components/burger-ingredients/burger-ingredients.jsx
+++ b/src/components/burger-ingredients/burger-ingredients.jsx
@@ -1,99 +1,96 @@
-import { useState } from "react";
-import PropTypes from "prop-types";
-import ingredientsStyles from "./burger-ingredients.module.css";
-import IngredientsMenu from "./ingredients-menu/ingredients-menu";
-import IngredientItem from "./ingredients-item/ingredients-item";
-import { IngredientType } from "../../utils/types.js";
 import Modal from "../modal/modal";
+import { useRef, useMemo } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
 import IngredientDetails from "../ingredient-details/ingredient-details";
+import ingredientsStyles from "./burger-ingredients.module.css";
+import IngredientsMenu from "./ingredients-menu/ingredients-menu";
+import IngredientSection from "./ingredients-section";
+import useScrollSection from "../../hooks/use-scroll-section";
+import { hideIngredient } from "../../services/ingredient-details-slice";
 
-function BurgerIngredients({ ingredients, selectedIngredients }) {
-  const buns = ingredients.filter((item) => item.type === "bun");
-  const sauces = ingredients.filter((item) => item.type === "sauce");
-  const mains = ingredients.filter((item) => item.type === "main");
+function BurgerIngredients() {
+  const selectedIngredients = useSelector((state) => state.burgerConstructor);
+  const ingredientDetails = useSelector((state) => state.ingredientDetails);
 
-  const [modalState, setModalState] = useState({
-    isShowingModal: false,
-    selectedIngredient: null,
-  });
+  const data = useSelector((state) => state.burgerIngredients.data);
+  const { buns, sauces, mains } = useMemo(() => {
+    const buns = data.filter((item) => item.type === "bun");
+    const sauces = data.filter((item) => item.type === "sauce");
+    const mains = data.filter((item) => item.type === "main");
+    return { buns, sauces, mains };
+  }, [data]);
 
-  const closeModal = () => {
-    setModalState({
-      isShowingModal: false,
-      selectedIngredient: null,
+  const counts = useMemo(() => {
+    const countsMap = {};
+    selectedIngredients.forEach((item) => {
+      const count = item.type === "bun" ? 2 : 1;
+      countsMap[item._id] = (countsMap[item._id] || 0) + count;
     });
-  };
+    return countsMap;
+  }, [selectedIngredients]);
 
-  const getIngredientCount = (ingredient) => {
-    return selectedIngredients.filter((item) => item._id === ingredient._id)
-      .length;
+  const dispatch = useDispatch();
+
+  const closeModal = () => {
+    dispatch(hideIngredient());
   };
 
+  const containerRef = useRef(null);
+  const bunRef = useRef(null);
+  const sauceRef = useRef(null);
+  const mainRef = useRef(null);
+
+  const { activeTab, scrollToSection } = useScrollSection(
+    containerRef,
+    bunRef,
+    sauceRef,
+    mainRef
+  );
+
   return (
     <div className={ingredientsStyles.mainBlock}>
       <section className={ingredientsStyles.header}>
         <article className="text text_type_main-large">Соберите бургер</article>
       </section>
-      <IngredientsMenu />
-      <section className={ingredientsStyles.scrollableContainer}>
-        <section>
-          <article className="text text_type_main-medium">Булки</article>
-          <ul className={ingredientsStyles.itemsGrid}>
-            {buns.map((ingredient) => (
-              <IngredientItem
-                key={ingredient._id}
-                ingredient={ingredient}
-                count={getIngredientCount(ingredient)}
-                setModalState={setModalState}
-              />
-            ))}
-          </ul>
-        </section>
-        <section>
-          <article className="text text_type_main-medium">Соусы</article>
-          <ul className={ingredientsStyles.itemsGrid}>
-            {sauces.map((ingredient) => (
-              <IngredientItem
-                key={ingredient._id}
-                ingredient={ingredient}
-                count={getIngredientCount(ingredient)}
-                setModalState={setModalState}
-              />
-            ))}
-          </ul>
-        </section>
-        <section>
-          <article className="text text_type_main-medium">Начинки</article>
-          <ul className={ingredientsStyles.itemsGrid}>
-            {mains.map((ingredient) => (
-              <IngredientItem
-                key={ingredient._id}
-                ingredient={ingredient}
-                count={getIngredientCount(ingredient)}
-                setModalState={setModalState}
-              />
-            ))}
-          </ul>
-        </section>
+      <IngredientsMenu activeTab={activeTab} setActiveTab={scrollToSection} />
+      <section
+        ref={containerRef}
+        className={ingredientsStyles.scrollableContainer}
+      >
+        <IngredientSection
+          sectionRef={bunRef}
+          ingredientsList={buns}
+          name="Булки"
+          counts={counts}
+        />
+        <IngredientSection
+          sectionRef={sauceRef}
+          ingredientsList={sauces}
+          name="Соусы"
+          counts={counts}
+        />
+        <IngredientSection
+          sectionRef={mainRef}
+          ingredientsList={mains}
+          name="Начинки"
+          counts={counts}
+        />
       </section>
 
-      {modalState.isShowingModal && modalState.selectedIngredient && (
-        <Modal
-          show={modalState.isShowingModal}
-          onCloseButtonClick={closeModal}
-          headerText="Детали ингредиента"
-          type="ingredient"
-        >
-          <IngredientDetails ingredient={modalState.selectedIngredient} />
-        </Modal>
-      )}
+      {ingredientDetails.isShowingModal &&
+        ingredientDetails.selectedIngredient && (
+          <Modal
+            show={ingredientDetails.isShowingModal}
+            onCloseButtonClick={closeModal}
+            headerText="Детали ингредиента"
+            type="ingredient"
+          >
+            <IngredientDetails />
+          </Modal>
+        )}
     </div>
   );
 }
 
-BurgerIngredients.propTypes = {
-  ingredients: PropTypes.arrayOf(IngredientType).isRequired,
-  selectedIngredients: PropTypes.arrayOf(IngredientType),
-};
-
 export default BurgerIngredients;
diff --git a/src/components/burger-ingredients/burger-ingredients.module.css b/src/components/burger-ingredients/burger-ingredients.module.css
index c47ed2d..db37a8c 100644
--- a/src/components/burger-ingredients/burger-ingredients.module.css
+++ b/src/components/burger-ingredients/burger-ingredients.module.css
@@ -24,7 +24,7 @@
 
 .scrollableContainer {
   max-height: 70vh;
-  overflow-y: auto;
+  overflow-y: scroll;
   padding-left: 20px;
 }
 
diff --git a/src/components/burger-ingredients/ingredients-item/ingredients-item.jsx b/src/components/burger-ingredients/ingredients-item/ingredients-item.jsx
index 258d906..36e5230 100644
--- a/src/components/burger-ingredients/ingredients-item/ingredients-item.jsx
+++ b/src/components/burger-ingredients/ingredients-item/ingredients-item.jsx
@@ -1,22 +1,34 @@
+import { useDispatch } from "react-redux";
+import { useDrag } from "react-dnd";
 import PropTypes from "prop-types";
-import styles from "./ingredients-item.module.css";
+
 import { IngredientType } from "../../../utils/types.js";
+import styles from "./ingredients-item.module.css";
+import { showIngredient } from "../../../services/ingredient-details-slice";
 import {
   CurrencyIcon,
   Counter,
 } from "@ya.praktikum/react-developer-burger-ui-components";
 
-const IngredientItem = ({ ingredient, count, setModalState }) => {
+const IngredientItem = ({ ingredient, count }) => {
   const { name, price, image } = ingredient;
+
+  const dispatch = useDispatch();
+
   const handleClick = () => {
-    setModalState({
-      isShowingModal: true,
-      selectedIngredient: ingredient,
-    });
+    dispatch(showIngredient(ingredient));
   };
 
+  const [{ opacity }, dragRef] = useDrag({
+    type: "addIngredient",
+    item: { ingredient },
+    collect: (monitor) => ({
+      opacity: monitor.isDragging() ? 0.5 : 1,
+    }),
+  });
+
   return (
-    <div className={styles.card}>
+    <div className={styles.card} style={{ opacity }} ref={dragRef}>
       <button className={styles.addButton} onClick={handleClick}>
         {count > 0 && (
           <div className={styles.counter}>
@@ -39,7 +51,6 @@ const IngredientItem = ({ ingredient, count, setModalState }) => {
 IngredientItem.propTypes = {
   ingredient: IngredientType.isRequired,
   count: PropTypes.number.isRequired,
-  setModalState: PropTypes.func.isRequired,
 };
 
 export default IngredientItem;
diff --git a/src/components/burger-ingredients/ingredients-menu/ingredients-menu.jsx b/src/components/burger-ingredients/ingredients-menu/ingredients-menu.jsx
index 53f625f..29cf221 100644
--- a/src/components/burger-ingredients/ingredients-menu/ingredients-menu.jsx
+++ b/src/components/burger-ingredients/ingredients-menu/ingredients-menu.jsx
@@ -1,23 +1,27 @@
-import { Tab } from "@ya.praktikum/react-developer-burger-ui-components";
-import React from "react";
+import PropTypes from "prop-types";
 
 import menuStyles from "./ingredienst-menu.module.css";
+import { Tab } from "@ya.praktikum/react-developer-burger-ui-components";
 
-const IngredientsMenu = () => {
-  const [current, setCurrent] = React.useState("bun");
+const IngredientsMenu = ({ activeTab, setActiveTab }) => {
   return (
     <div className={`${menuStyles.menu} p-5`}>
-      <Tab value="bun" active={current === "bun"} onClick={setCurrent}>
+      <Tab value="bun" active={activeTab === "bun"} onClick={setActiveTab}>
         Булки
       </Tab>
-      <Tab value="sauce" active={current === "sauce"} onClick={setCurrent}>
+      <Tab value="sauce" active={activeTab === "sauce"} onClick={setActiveTab}>
         Соусы
       </Tab>
-      <Tab value="main" active={current === "main"} onClick={setCurrent}>
+      <Tab value="main" active={activeTab === "main"} onClick={setActiveTab}>
         Начинки
       </Tab>
     </div>
   );
 };
 
+IngredientsMenu.propTypes = {
+  activeTab: PropTypes.oneOf(["bun", "sauce", "main"]),
+  setActiveTab: PropTypes.func.isRequired,
+};
+
 export default IngredientsMenu;
diff --git a/src/components/burger-ingredients/ingredients-section.jsx b/src/components/burger-ingredients/ingredients-section.jsx
new file mode 100644
index 0000000..720a5fa
--- /dev/null
+++ b/src/components/burger-ingredients/ingredients-section.jsx
@@ -0,0 +1,44 @@
+import PropTypes from "prop-types";
+
+import ingredientsStyles from "./burger-ingredients.module.css";
+import IngredientItem from "./ingredients-item/ingredients-item";
+import { IngredientType } from "../../utils/types.js";
+
+const IngredientSection = ({ sectionRef, ingredientsList, name, counts }) => {
+  const getIngredientCount = (ingredient) => counts?.[ingredient._id] || 0;
+
+  if (!ingredientsList || ingredientsList.length === 0) {
+    return (
+      <section ref={sectionRef}>
+        <article className="text text_type_main-medium">{name}</article>
+        <p className="text text_type_main-default">Нет ингредиентов</p>
+      </section>
+    );
+  }
+
+  return (
+    <section ref={sectionRef}>
+      <article className="text text_type_main-medium">{name}</article>
+      <ul className={ingredientsStyles.itemsGrid}>
+        {ingredientsList.map((ingredient) => (
+          <IngredientItem
+            key={ingredient._id}
+            ingredient={ingredient}
+            count={getIngredientCount(ingredient)}
+          />
+        ))}
+      </ul>
+    </section>
+  );
+};
+
+IngredientSection.propTypes = {
+  sectionRef: PropTypes.shape({
+    current: PropTypes.any,
+  }),
+  ingredientsList: PropTypes.arrayOf(IngredientType).isRequired,
+  name: PropTypes.string.isRequired,
+  counts: PropTypes.object,
+};
+
+export default IngredientSection;
diff --git a/src/components/ingredient-details/ingredient-details.jsx b/src/components/ingredient-details/ingredient-details.jsx
index 288122b..63c8106 100644
--- a/src/components/ingredient-details/ingredient-details.jsx
+++ b/src/components/ingredient-details/ingredient-details.jsx
@@ -1,7 +1,11 @@
+import { useSelector } from "react-redux";
+
 import styles from "./ingredient-details.module.css";
-import { IngredientType } from "../../utils/types.js";
 
-const IngredientDetails = ({ ingredient }) => {
+const IngredientDetails = () => {
+  const ingredient = useSelector(
+    (state) => state.ingredientDetails.selectedIngredient
+  );
   return (
     <div className={styles.container}>
       <img
@@ -36,8 +40,4 @@ const IngredientDetails = ({ ingredient }) => {
   );
 };
 
-IngredientDetails.propTypes = {
-  ingredient: IngredientType.isRequired,
-};
-
 export default IngredientDetails;
diff --git a/src/components/order-details/order-details.jsx b/src/components/order-details/order-details.jsx
index a651917..9e5f2ed 100644
--- a/src/components/order-details/order-details.jsx
+++ b/src/components/order-details/order-details.jsx
@@ -1,11 +1,14 @@
+import { useSelector } from "react-redux";
+
 import styles from "./order-details.module.css";
 import { CheckMarkIcon } from "@ya.praktikum/react-developer-burger-ui-components";
 
 const OrderDetails = () => {
+  const orderDetails = useSelector((state) => state.orderDetails.data);
   return (
     <div className={styles.container}>
       <h2 className={`${styles.orderNumber} text text_type_digits-large`}>
-        034536
+        {orderDetails?.order?.number}
       </h2>
       <p className={`${styles.orderId} text text_type_main-medium`}>
         Идентификатор заказа
diff --git a/src/hooks/use-scroll-section.jsx b/src/hooks/use-scroll-section.jsx
new file mode 100644
index 0000000..e663214
--- /dev/null
+++ b/src/hooks/use-scroll-section.jsx
@@ -0,0 +1,64 @@
+import { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+
+const useScrollSection = (containerRef, bunRef, sauceRef, mainRef) => {
+  const [activeTab, setActiveTab] = useState("bun");
+
+  const scrollToSection = (type) => {
+    setActiveTab(type);
+    if (type === "bun") bunRef.current?.scrollIntoView({ behavior: "smooth" });
+    if (type === "sauce")
+      sauceRef.current?.scrollIntoView({ behavior: "smooth" });
+    if (type === "main")
+      mainRef.current?.scrollIntoView({ behavior: "smooth" });
+  };
+
+  useEffect(() => {
+    const container = containerRef.current;
+    if (!container) return;
+
+    const handleScroll = () => {
+      const containerTop = container.getBoundingClientRect().top;
+
+      const bunTop = bunRef.current.getBoundingClientRect().top - containerTop;
+      const sauceTop =
+        sauceRef.current.getBoundingClientRect().top - containerTop;
+      const mainTop =
+        mainRef.current.getBoundingClientRect().top - containerTop;
+
+      const distances = [
+        { type: "bun", top: bunTop },
+        { type: "sauce", top: sauceTop },
+        { type: "main", top: mainTop },
+      ];
+
+      const nearest = distances
+        .filter((distance) => distance.top <= 1)
+        .sort((a, b) => b.top - a.top)[0].type;
+      setActiveTab(nearest);
+    };
+
+    container.addEventListener("scroll", handleScroll);
+    handleScroll();
+    return () => container.removeEventListener("scroll", handleScroll);
+  }, [containerRef, bunRef, sauceRef, mainRef]);
+
+  return { activeTab, scrollToSection };
+};
+
+useScrollSection.propTypes = {
+  containerRef: PropTypes.shape({
+    current: PropTypes.any,
+  }).isRequired,
+  bunRef: PropTypes.shape({
+    current: PropTypes.any,
+  }).isRequired,
+  sauceRef: PropTypes.shape({
+    current: PropTypes.any,
+  }).isRequired,
+  mainRef: PropTypes.shape({
+    current: PropTypes.any,
+  }).isRequired,
+};
+
+export default useScrollSection;
diff --git a/src/hooks/use-user-burger.jsx b/src/hooks/use-user-burger.jsx
deleted file mode 100644
index ed1420d..0000000
--- a/src/hooks/use-user-burger.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { useState } from "react";
-import { mockBurger } from "../utils/mock-burger";
-
-export const useUserBurger = () => {
-  const [selectedIngredients, setSelectedIngredients] = useState(mockBurger);
-
-  // Добавление ингредиентов в бургер
-  const handleAdd = (ingredient) => {
-    const uniqueId = `${ingredient._id}-${Date.now()}`;
-    const ingredientWithId = { ...ingredient, uniqueId };
-
-    if (ingredient.type === "bun") {
-      const currentBun = selectedIngredients.find(
-        (item) => item.type === "bun"
-      );
-
-      if (currentBun && currentBun._id === ingredient._id) {
-        return;
-      }
-
-      if (currentBun) {
-        const newIngredients = selectedIngredients.filter(
-          (item) => item.type !== "bun"
-        );
-        setSelectedIngredients([...newIngredients, ingredientWithId]);
-      } else {
-        setSelectedIngredients([...selectedIngredients, ingredientWithId]);
-      }
-    } else {
-      setSelectedIngredients([...selectedIngredients, ingredientWithId]);
-    }
-  };
-
-  // Удаление ингредиентов
-  const handleRemove = (ingredientToRemove) => {
-    const newIngredients = selectedIngredients.filter(
-      (item) => item.uniqueId !== ingredientToRemove.uniqueId
-    );
-    setSelectedIngredients(newIngredients);
-  };
-
-  return {
-    selectedIngredients,
-    handleAdd,
-    handleRemove,
-  };
-};
diff --git a/src/index.tsx b/src/index.tsx
index 032464f..8f96671 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -3,13 +3,17 @@ import ReactDOM from 'react-dom/client';
 import './index.css';
 import App from './App';
 import reportWebVitals from './reportWebVitals';
+import { Provider } from 'react-redux';
+import { store } from './services/store'
 
 const root = ReactDOM.createRoot(
   document.getElementById('root') as HTMLElement
 );
 root.render(
   <React.StrictMode>
-    <App />
+    <Provider store={store}>
+      <App />
+    </Provider>
   </React.StrictMode>
 );
 
diff --git a/src/services/burger-constructor-slice.tsx b/src/services/burger-constructor-slice.tsx
new file mode 100644
index 0000000..a320cfc
--- /dev/null
+++ b/src/services/burger-constructor-slice.tsx
@@ -0,0 +1,70 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+import { Ingredient } from "../utils/types"
+
+interface MoveIngredientPayload {
+  ingredient: Ingredient;
+  toIndex: number;
+}
+
+const initialState: Ingredient[] = []
+
+export const burgerConstructorSlice = createSlice({
+  name: "burgerConstructor",
+  initialState,
+  reducers: {
+    handleAdd: (state, action: PayloadAction<Ingredient>) => {
+      const ingredient = action.payload;
+      const uniqueId = `${ingredient._id}-${Date.now()}`;
+      const ingredientWithId = { ...ingredient, uniqueId };
+      if (ingredient.type === "bun") {
+        const currentBun = state.find((item) => item.type === "bun");
+
+        if (currentBun && currentBun._id === ingredient._id) {
+          return;
+        }
+
+        if (currentBun) {
+          const filtered = state.filter((item) => item.type !== "bun");
+          return [ingredientWithId, ...filtered];
+        }
+
+        return [ingredientWithId, ...state]; 
+      } else {
+        state.push(ingredientWithId);
+      }
+    },
+    handleMove: (state, action: PayloadAction<MoveIngredientPayload>) => {
+      const {ingredient, toIndex } = action.payload;
+
+      const ingredientIndex = state.findIndex(
+        (item) => item.uniqueId === ingredient.uniqueId
+      );
+      if (ingredientIndex === -1) return;
+      
+      const bun = state.find(item => item.type === 'bun') || null!;
+      const fillings = state.filter(item => item.type !== 'bun');
+
+      if (fillings.length === 0) return;
+      
+      const fromIndex = fillings.findIndex(item => item.uniqueId === ingredient.uniqueId);
+      if (fromIndex === -1 || fromIndex === toIndex) return;
+
+      const [moved] = fillings.splice(fromIndex, 1);
+      fillings.splice(toIndex, 0, moved);
+
+      return bun ? [bun, ...fillings] : [...fillings];
+
+    }, 
+    handleRemove: (state, action: PayloadAction<{ uniqueId: string }>) => {
+      return state.filter(
+        (item) => item.uniqueId !== action.payload.uniqueId
+      );
+    },
+  },
+});
+
+export const { handleAdd, handleMove, handleRemove } =
+  burgerConstructorSlice.actions;
+
+export default burgerConstructorSlice.reducer;
diff --git a/src/services/burger-ingredients-slice.tsx b/src/services/burger-ingredients-slice.tsx
new file mode 100644
index 0000000..99b949e
--- /dev/null
+++ b/src/services/burger-ingredients-slice.tsx
@@ -0,0 +1,70 @@
+import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
+
+import { Ingredient } from "../utils/types"
+
+interface IngredientsApiResponse {
+  success: boolean;
+  data: Ingredient[];
+}
+
+interface BurgerIngredientsState {
+  isLoading: boolean;
+  data: Ingredient[];
+  hasError: boolean;
+}
+
+const url = "https://norma.nomoreparties.space/api/ingredients";
+
+const initialState: BurgerIngredientsState = {
+  isLoading: false,
+  data: [],
+  hasError: false,
+};
+
+export const fetchIngredients = createAsyncThunk<
+  Ingredient[], 
+  void,  
+  { rejectValue: string } 
+>(
+  "burgerIngredients/fetchIngredients",
+  async (_, { rejectWithValue }) => {
+    try {
+      const response = await fetch(url);
+      if (!response.ok) {
+        throw new Error(`Ошибка ${response.status}: ${response.statusText}`);
+      }
+      const data: IngredientsApiResponse = await response.json();
+      if (!data.success) {
+        throw new Error("Данные с сервера не содержат success: true");
+      }
+      return data.data;
+    } catch (error: any) {
+      return rejectWithValue(error.message);
+    }
+  }
+);
+
+export const burgerIngredientsSlice = createSlice({
+  name: "burgerIngredients",
+  initialState,
+  reducers: {},
+  extraReducers: (builder) => {
+    builder.addCase(fetchIngredients.pending, (state) => {
+      state.isLoading = true;
+      state.hasError = false;
+    });
+    builder.addCase(fetchIngredients.fulfilled, (state, action: PayloadAction<Ingredient[]>) => {
+      state.isLoading = false;
+      state.hasError = false;
+      state.data = action.payload;
+    });
+    builder.addCase(fetchIngredients.rejected, (state, action) => {
+      state.isLoading = false;
+      state.hasError = true;
+      state.data = [];
+      console.error("Ошибка при загрузке данных:", action.payload);
+    });
+  },
+});
+
+export default burgerIngredientsSlice.reducer;
diff --git a/src/services/ingredient-details-slice.tsx b/src/services/ingredient-details-slice.tsx
new file mode 100644
index 0000000..4f482ce
--- /dev/null
+++ b/src/services/ingredient-details-slice.tsx
@@ -0,0 +1,29 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { Ingredient } from "../utils/types";
+
+type IngredientState = {
+  isShowingModal: boolean,
+  selectedIngredient: Ingredient | null;
+};
+
+const initialState: IngredientState = {
+  isShowingModal: false,
+  selectedIngredient: null,
+};
+
+export const ingredientDetailsSlice = createSlice({
+  name: "ingredientDetails",
+  initialState,
+  reducers: {
+    showIngredient: (state, action: PayloadAction<Ingredient>) => {
+      return {
+        isShowingModal: true,
+        selectedIngredient: action.payload,
+      };
+    },
+    hideIngredient: () => { return initialState },
+  },
+});
+
+export const { showIngredient, hideIngredient } = ingredientDetailsSlice.actions;
+export default ingredientDetailsSlice.reducer;
\ No newline at end of file
diff --git a/src/services/order-details-slice.tsx b/src/services/order-details-slice.tsx
new file mode 100644
index 0000000..2d53565
--- /dev/null
+++ b/src/services/order-details-slice.tsx
@@ -0,0 +1,92 @@
+import { createSlice, createAsyncThunk, PayloadAction  } from "@reduxjs/toolkit";
+
+type OrderDetails = {
+  number: number,
+};
+
+type OrderData = {
+  name: string,
+  order: OrderDetails,
+  success: boolean,
+};
+
+type OrderState = {
+  isShowingModal: boolean,
+  data: OrderData | null;
+  isLoading: boolean;
+  hasError: boolean;
+};
+
+// interface OrderApiResponse {
+//   success: boolean;
+//   data: OrderState;
+// }
+
+const url = "https://norma.nomoreparties.space/api/orders";
+
+const initialState: OrderState = {
+  isShowingModal: false,
+  data: null,
+  isLoading: false,
+  hasError: false,
+};
+
+export const postOrder = createAsyncThunk<
+  OrderData,
+  string[],  
+  { rejectValue: string } 
+>(
+  "orderDetails/postOrder",
+  async (ingredients, { rejectWithValue }) => {
+    console.log(ingredients)
+    try {
+      const response = await fetch(url, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({ ingredients }),
+      });
+  
+      if (!response.ok) {
+        throw new Error(`Ошибка ${response.status}: ${response.statusText}`);
+      }
+      const data: OrderData = await response.json();
+      if (!data.success) {
+        throw new Error("Данные с сервера не содержат success: true");
+      }
+      return data;
+    } catch (error: any) {
+      return rejectWithValue(error.message);
+    }
+  }
+);
+
+export const orderDetailsSlice = createSlice({
+  name: "orderDetails",
+  initialState,
+  reducers: {
+    hideOrderModal: (state) => { state.isShowingModal = false; },
+  },
+    extraReducers: (builder) => {
+      builder.addCase(postOrder.pending, (state) => {
+        state.isLoading = true;
+        state.hasError = false;
+      });
+      builder.addCase(postOrder.fulfilled, (state, action: PayloadAction<OrderData>) => {
+        state.isLoading = false;
+        state.hasError = false;
+        state.data = action.payload;
+        state.isShowingModal = true;
+      });
+      builder.addCase(postOrder.rejected, (state, action) => {
+        state.isLoading = false;
+        state.hasError = true;
+        state.data = null;
+        console.error("Ошибка при загрузке данных:", action.payload);
+      });
+    },
+});
+
+export const { hideOrderModal } = orderDetailsSlice.actions;
+export default orderDetailsSlice.reducer;
\ No newline at end of file
diff --git a/src/services/store.tsx b/src/services/store.tsx
new file mode 100644
index 0000000..fc20b05
--- /dev/null
+++ b/src/services/store.tsx
@@ -0,0 +1,24 @@
+import { configureStore } from '@reduxjs/toolkit'
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
+
+import burgerConstructorReducer from "./burger-constructor-slice";
+import burgerIngredientsReducer from "./burger-ingredients-slice";
+import ingredientDetailsReducer from "./ingredient-details-slice";
+import orderDetailsReducer from "./order-details-slice";
+
+export const store = configureStore({
+  reducer: {
+    burgerConstructor: burgerConstructorReducer,
+    burgerIngredients: burgerIngredientsReducer,
+    ingredientDetails: ingredientDetailsReducer,
+    orderDetails: orderDetailsReducer,
+  },
+  devTools: process.env.NODE_ENV !== 'production',
+});
+
+export type AppStore = typeof store;
+export type RootState = ReturnType<AppStore['getState']>;
+export type AppDispatch = AppStore['dispatch'];
+
+export const useAppDispatch: () => AppDispatch = useDispatch;
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
\ No newline at end of file
diff --git a/src/utils/mock-burger.ts b/src/utils/mock-burger.ts
deleted file mode 100644
index 24dc22b..0000000
--- a/src/utils/mock-burger.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-// Моковый бургер
-export  const mockBurger = [
-  {
-    _id: "643d69a5c3f7b9001cfa093c",
-    name: "Краторная булка N-200i",
-    type: "bun",
-    proteins: 80,
-    fat: 24,
-    carbohydrates: 53,
-    calories: 420,
-    price: 1255,
-    image: "https://code.s3.yandex.net/react/code/bun-02.png",
-    image_mobile: "https://code.s3.yandex.net/react/code/bun-02-mobile.png",
-    image_large: "https://code.s3.yandex.net/react/code/bun-02-large.png",
-    __v: 0,
-    uniqueId: "bun-000",
-  },
-  {
-    _id: "643d69a5c3f7b9001cfa0942",
-    name: "Соус Spicy-X",
-    type: "sauce",
-    proteins: 30,
-    fat: 20,
-    carbohydrates: 40,
-    calories: 30,
-    price: 90,
-    image: "https://code.s3.yandex.net/react/code/sauce-02.png",
-    image_mobile: "https://code.s3.yandex.net/react/code/sauce-02-mobile.png",
-    image_large: "https://code.s3.yandex.net/react/code/sauce-02-large.png",
-    __v: 0,
-    uniqueId: "sauce-000",
-  },
-  {
-    _id: "643d69a5c3f7b9001cfa0941", 
-    name: "Биокотлета из марсианской Магнолии",
-    type: "main",
-    proteins: 420,
-    fat: 142,
-    carbohydrates: 242,
-    calories: 424,
-    price: 424,
-    image: "https://code.s3.yandex.net/react/code/meat-01.png",
-    image_mobile: "https://code.s3.yandex.net/react/code/meat-01-mobile.png",
-    image_large: "https://code.s3.yandex.net/react/code/meat-01-large.png",
-    __v: 0,
-    uniqueId: "main-000",
-  },
-  {
-    _id: "643d69a5c3f7b9001cfa0942",
-    name: "Соус Spicy-X",
-    type: "sauce",
-    proteins: 30,
-    fat: 20,
-    carbohydrates: 40,
-    calories: 30,
-    price: 90,
-    image: "https://code.s3.yandex.net/react/code/sauce-02.png",
-    image_mobile: "https://code.s3.yandex.net/react/code/sauce-02-mobile.png",
-    image_large: "https://code.s3.yandex.net/react/code/sauce-02-large.png",
-    __v: 0,
-    uniqueId: "sauce-001",
-  },
-];
\ No newline at end of file
diff --git a/src/utils/types.tsx b/src/utils/types.tsx
new file mode 100644
index 0000000..3a7501d
--- /dev/null
+++ b/src/utils/types.tsx
@@ -0,0 +1,16 @@
+export interface Ingredient {
+    _id: string;
+    name: string;
+    type: string;
+    proteins: number;
+    fat: number;
+    carbohydrates: number;
+    calories: number;
+    price: number;
+    image: string;
+    image_mobile: string;
+    image_large: string;
+    __v: number;
+    uniqueId?: string; 
+  }
+  
\ No newline at end of file


Reviews:
user: login: kyzinatra
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T16:23:49Z
user: login: kyzinatra
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T16:25:20Z
user: login: kyzinatra
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T16:26:04Z
user: login: kyzinatra
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T16:26:59Z
user: login: kyzinatra
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T16:27:51Z
user: login: kyzinatra
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T16:28:56Z
user: login: kyzinatra
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T16:29:41Z

Review comments:
diff_hunk: @@ -1,58 +1,23 @@
-import { useState, useEffect } from "react";
+import { useEffect } from "react";
 import styles from "./App.module.css";
+import { useDispatch, useSelector } from "react-redux";
+import { DndProvider } from "react-dnd";
+import { HTML5Backend } from "react-dnd-html5-backend";
 
 import AppHeader from "./components/app-header/app-header";
 import BurgerIngredients from "./components/burger-ingredients/burger-ingredients";
 import BurgerConstructor from "./components/burger-constructor/burger-constructor";
-import { useUserBurger } from "./hooks/use-user-burger";
+import { fetchIngredients } from "./services/burger-ingredients-slice";
 
 function App() {
-  // Кастомный хук для упрощения кода в App.jsx
-  const { selectedIngredients, handleAdd, handleRemove } = useUserBurger();
-
-  const [state, setState] = useState({
-    isLoading: false,
-    hasError: false,
-    data: [],
-  });
-
-  // Получение данных
-  const url = "https://norma.nomoreparties.space/api/ingredients";
-
-  const getData = () => {
-    setState({ ...state, hasError: false, isLoading: true });
-    fetch(url)
-      .then((res) => {
-        if (res.ok) {
-          return res.json();
-        }
-        return Promise.reject(
-          new Error(`Ошибка ${res.status}: ${res.statusText}`)
-        );
-      })
-      .then((data) => {
-        if (!data.success) {
-          throw new Error("Данные с сервера не содержат success: true");
-        }
-        setState({
-          ...state,
-          data: data.data,
-          hasError: !data.success,
-          isLoading: false,
-        });
-      })
-      .catch((e) => {
-        console.log("Ошибка при загрузке данных:", e.message);
-        setState({ ...state, hasError: true, isLoading: false });
-      });
-  };
+  const dispatch = useDispatch();
+  const { hasError } = useSelector((state) => state.burgerIngredients);
path: src/App.jsx
user: login: kyzinatra
body: Можно лучше: селекторы стоит делать максимально специфичными. Так как `redux` сравнивает прошлое и новое значение, возвращенное селектором просто через `===`, то стоит возвращать из селектора только то, что нам нужно, чтобы избежать лишних перерендеров
`const hasError = useSelector((state) => state.burgerIngredients.hasError);`
Подробнее о том, как работает `useSelector()` изнутри: https://youtu.be/SVG-x-4BQic
created_at: 2025-05-02T16:23:48Z
author_association: NONE
diff_hunk: @@ -68,19 +33,14 @@ function App() {
     <div className={styles.app}>
       <AppHeader />
       <main className={styles.main}>
-        <section className={styles.leftBlock}>
-          <BurgerIngredients
-            ingredients={state.data}
-            onAdd={handleAdd}
-            selectedIngredients={selectedIngredients}
-          />
-        </section>
-        <section className={styles.rightBlock}>
-          <BurgerConstructor
-            selectedIngredients={selectedIngredients}
-            onRemove={handleRemove}
-          />
-        </section>
+        <DndProvider backend={HTML5Backend}>
+          <section className={styles.leftBlock}>
path: src/App.jsx
user: login: kyzinatra
body: Отлично! Понравилось, что вы обернули в провайдер только то, что нужно для DnD
created_at: 2025-05-02T16:25:19Z
author_association: NONE
diff_hunk: @@ -1,99 +1,138 @@
-import { useState } from "react";
-import PropTypes from "prop-types";
+import Modal from "../modal/modal";
+import { useState, useEffect, useRef } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import IngredientDetails from "../ingredient-details/ingredient-details";
 import ingredientsStyles from "./burger-ingredients.module.css";
 import IngredientsMenu from "./ingredients-menu/ingredients-menu";
 import IngredientItem from "./ingredients-item/ingredients-item";
-import { IngredientType } from "../../utils/types.js";
-import Modal from "../modal/modal";
-import IngredientDetails from "../ingredient-details/ingredient-details";
+import { hideIngredient } from "../../services/ingredient-details-slice";
+
+function BurgerIngredients() {
+  const selectedIngredients = useSelector((state) => state.burgerConstructor);
+  const ingredientDetails = useSelector((state) => state.ingredientDetails);
 
-function BurgerIngredients({ ingredients, selectedIngredients }) {
-  const buns = ingredients.filter((item) => item.type === "bun");
-  const sauces = ingredients.filter((item) => item.type === "sauce");
-  const mains = ingredients.filter((item) => item.type === "main");
+  const { data } = useSelector((state) => state.burgerIngredients);
+  const buns = data.filter((item) => item.type === "bun");
+  const sauces = data.filter((item) => item.type === "sauce");
+  const mains = data.filter((item) => item.type === "main");
path: src/components/burger-ingredients/burger-ingredients.jsx
user: login: kyzinatra
body: Можно лучше: фильтры лучше обернуть в `useMemo()`
created_at: 2025-05-02T16:26:03Z
author_association: NONE
diff_hunk: @@ -1,99 +1,138 @@
-import { useState } from "react";
-import PropTypes from "prop-types";
+import Modal from "../modal/modal";
+import { useState, useEffect, useRef } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import IngredientDetails from "../ingredient-details/ingredient-details";
 import ingredientsStyles from "./burger-ingredients.module.css";
 import IngredientsMenu from "./ingredients-menu/ingredients-menu";
 import IngredientItem from "./ingredients-item/ingredients-item";
-import { IngredientType } from "../../utils/types.js";
-import Modal from "../modal/modal";
-import IngredientDetails from "../ingredient-details/ingredient-details";
+import { hideIngredient } from "../../services/ingredient-details-slice";
+
+function BurgerIngredients() {
+  const selectedIngredients = useSelector((state) => state.burgerConstructor);
+  const ingredientDetails = useSelector((state) => state.ingredientDetails);
 
-function BurgerIngredients({ ingredients, selectedIngredients }) {
-  const buns = ingredients.filter((item) => item.type === "bun");
-  const sauces = ingredients.filter((item) => item.type === "sauce");
-  const mains = ingredients.filter((item) => item.type === "main");
+  const { data } = useSelector((state) => state.burgerIngredients);
+  const buns = data.filter((item) => item.type === "bun");
+  const sauces = data.filter((item) => item.type === "sauce");
+  const mains = data.filter((item) => item.type === "main");
 
-  const [modalState, setModalState] = useState({
-    isShowingModal: false,
-    selectedIngredient: null,
-  });
+  const dispatch = useDispatch();
 
   const closeModal = () => {
-    setModalState({
-      isShowingModal: false,
-      selectedIngredient: null,
-    });
+    dispatch(hideIngredient());
   };
 
+  const [activeTab, setActiveTab] = useState("bun");
+  const containerRef = useRef(null);
+  const bunRef = useRef(null);
+  const sauceRef = useRef(null);
+  const mainRef = useRef(null);
+
   const getIngredientCount = (ingredient) => {
     return selectedIngredients.filter((item) => item._id === ingredient._id)
       .length;
   };
 
+  const scrollToSection = (type) => {
+    setActiveTab(type);
+    if (type === "bun") bunRef.current?.scrollIntoView({ behavior: "smooth" });
+    if (type === "sauce")
+      sauceRef.current?.scrollIntoView({ behavior: "smooth" });
+    if (type === "main")
+      mainRef.current?.scrollIntoView({ behavior: "smooth" });
+  };
+
+  useEffect(() => {
+    const container = containerRef.current;
+    if (!container) return;
+
+    const handleScroll = () => {
+      const containerTop = container.getBoundingClientRect().top;
+
+      const bunTop = bunRef.current.getBoundingClientRect().top - containerTop;
+      const sauceTop =
+        sauceRef.current.getBoundingClientRect().top - containerTop;
+      const mainTop =
+        mainRef.current.getBoundingClientRect().top - containerTop;
+
+      const distances = [
+        { type: "bun", top: bunTop },
path: src/components/burger-ingredients/burger-ingredients.jsx
user: login: kyzinatra
body: Можно лучше: эту логику лучше вынести в отдельный хук, чтобы не перегружать компонент
created_at: 2025-05-02T16:26:58Z
author_association: NONE
diff_hunk: @@ -1,99 +1,138 @@
-import { useState } from "react";
-import PropTypes from "prop-types";
+import Modal from "../modal/modal";
+import { useState, useEffect, useRef } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import IngredientDetails from "../ingredient-details/ingredient-details";
 import ingredientsStyles from "./burger-ingredients.module.css";
 import IngredientsMenu from "./ingredients-menu/ingredients-menu";
 import IngredientItem from "./ingredients-item/ingredients-item";
-import { IngredientType } from "../../utils/types.js";
-import Modal from "../modal/modal";
-import IngredientDetails from "../ingredient-details/ingredient-details";
+import { hideIngredient } from "../../services/ingredient-details-slice";
+
+function BurgerIngredients() {
+  const selectedIngredients = useSelector((state) => state.burgerConstructor);
+  const ingredientDetails = useSelector((state) => state.ingredientDetails);
 
-function BurgerIngredients({ ingredients, selectedIngredients }) {
-  const buns = ingredients.filter((item) => item.type === "bun");
-  const sauces = ingredients.filter((item) => item.type === "sauce");
-  const mains = ingredients.filter((item) => item.type === "main");
+  const { data } = useSelector((state) => state.burgerIngredients);
+  const buns = data.filter((item) => item.type === "bun");
+  const sauces = data.filter((item) => item.type === "sauce");
+  const mains = data.filter((item) => item.type === "main");
 
-  const [modalState, setModalState] = useState({
-    isShowingModal: false,
-    selectedIngredient: null,
-  });
+  const dispatch = useDispatch();
 
   const closeModal = () => {
-    setModalState({
-      isShowingModal: false,
-      selectedIngredient: null,
-    });
+    dispatch(hideIngredient());
   };
 
+  const [activeTab, setActiveTab] = useState("bun");
+  const containerRef = useRef(null);
+  const bunRef = useRef(null);
+  const sauceRef = useRef(null);
+  const mainRef = useRef(null);
+
   const getIngredientCount = (ingredient) => {
     return selectedIngredients.filter((item) => item._id === ingredient._id)
       .length;
   };
 
+  const scrollToSection = (type) => {
+    setActiveTab(type);
+    if (type === "bun") bunRef.current?.scrollIntoView({ behavior: "smooth" });
+    if (type === "sauce")
+      sauceRef.current?.scrollIntoView({ behavior: "smooth" });
+    if (type === "main")
+      mainRef.current?.scrollIntoView({ behavior: "smooth" });
+  };
+
+  useEffect(() => {
+    const container = containerRef.current;
+    if (!container) return;
+
+    const handleScroll = () => {
+      const containerTop = container.getBoundingClientRect().top;
+
+      const bunTop = bunRef.current.getBoundingClientRect().top - containerTop;
+      const sauceTop =
+        sauceRef.current.getBoundingClientRect().top - containerTop;
+      const mainTop =
+        mainRef.current.getBoundingClientRect().top - containerTop;
+
+      const distances = [
+        { type: "bun", top: bunTop },
+        { type: "sauce", top: sauceTop },
+        { type: "main", top: mainTop },
+      ];
+
+      const nearest = distances
+        .filter((distance) => distance.top <= 1)
+        .sort((a, b) => b.top - a.top)[0].type;
+      setActiveTab(nearest);
+    };
+
+    container.addEventListener("scroll", handleScroll);
+    return () => container.removeEventListener("scroll", handleScroll);
+  }, []);
+
   return (
     <div className={ingredientsStyles.mainBlock}>
       <section className={ingredientsStyles.header}>
         <article className="text text_type_main-large">Соберите бургер</article>
       </section>
-      <IngredientsMenu />
-      <section className={ingredientsStyles.scrollableContainer}>
-        <section>
+      <IngredientsMenu activeTab={activeTab} setActiveTab={scrollToSection} />
+      <section
+        ref={containerRef}
+        className={ingredientsStyles.scrollableContainer}
+      >
+        <section ref={bunRef}>
           <article className="text text_type_main-medium">Булки</article>
           <ul className={ingredientsStyles.itemsGrid}>
             {buns.map((ingredient) => (
               <IngredientItem
                 key={ingredient._id}
                 ingredient={ingredient}
                 count={getIngredientCount(ingredient)}
path: src/components/burger-ingredients/burger-ingredients.jsx
user: login: kyzinatra
body: Можно лучше: Булочки добавляются по две, счетчик стоит поправить
created_at: 2025-05-02T16:27:50Z
author_association: NONE
diff_hunk: @@ -1,99 +1,138 @@
-import { useState } from "react";
-import PropTypes from "prop-types";
+import Modal from "../modal/modal";
+import { useState, useEffect, useRef } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import IngredientDetails from "../ingredient-details/ingredient-details";
 import ingredientsStyles from "./burger-ingredients.module.css";
 import IngredientsMenu from "./ingredients-menu/ingredients-menu";
 import IngredientItem from "./ingredients-item/ingredients-item";
-import { IngredientType } from "../../utils/types.js";
-import Modal from "../modal/modal";
-import IngredientDetails from "../ingredient-details/ingredient-details";
+import { hideIngredient } from "../../services/ingredient-details-slice";
+
+function BurgerIngredients() {
+  const selectedIngredients = useSelector((state) => state.burgerConstructor);
+  const ingredientDetails = useSelector((state) => state.ingredientDetails);
 
-function BurgerIngredients({ ingredients, selectedIngredients }) {
-  const buns = ingredients.filter((item) => item.type === "bun");
-  const sauces = ingredients.filter((item) => item.type === "sauce");
-  const mains = ingredients.filter((item) => item.type === "main");
+  const { data } = useSelector((state) => state.burgerIngredients);
+  const buns = data.filter((item) => item.type === "bun");
+  const sauces = data.filter((item) => item.type === "sauce");
+  const mains = data.filter((item) => item.type === "main");
 
-  const [modalState, setModalState] = useState({
-    isShowingModal: false,
-    selectedIngredient: null,
-  });
+  const dispatch = useDispatch();
 
   const closeModal = () => {
-    setModalState({
-      isShowingModal: false,
-      selectedIngredient: null,
-    });
+    dispatch(hideIngredient());
   };
 
+  const [activeTab, setActiveTab] = useState("bun");
+  const containerRef = useRef(null);
+  const bunRef = useRef(null);
+  const sauceRef = useRef(null);
+  const mainRef = useRef(null);
+
   const getIngredientCount = (ingredient) => {
     return selectedIngredients.filter((item) => item._id === ingredient._id)
       .length;
   };
 
+  const scrollToSection = (type) => {
+    setActiveTab(type);
+    if (type === "bun") bunRef.current?.scrollIntoView({ behavior: "smooth" });
+    if (type === "sauce")
+      sauceRef.current?.scrollIntoView({ behavior: "smooth" });
+    if (type === "main")
+      mainRef.current?.scrollIntoView({ behavior: "smooth" });
+  };
+
+  useEffect(() => {
+    const container = containerRef.current;
+    if (!container) return;
+
+    const handleScroll = () => {
+      const containerTop = container.getBoundingClientRect().top;
+
+      const bunTop = bunRef.current.getBoundingClientRect().top - containerTop;
+      const sauceTop =
+        sauceRef.current.getBoundingClientRect().top - containerTop;
+      const mainTop =
+        mainRef.current.getBoundingClientRect().top - containerTop;
+
+      const distances = [
+        { type: "bun", top: bunTop },
+        { type: "sauce", top: sauceTop },
+        { type: "main", top: mainTop },
+      ];
+
+      const nearest = distances
+        .filter((distance) => distance.top <= 1)
+        .sort((a, b) => b.top - a.top)[0].type;
+      setActiveTab(nearest);
+    };
+
+    container.addEventListener("scroll", handleScroll);
+    return () => container.removeEventListener("scroll", handleScroll);
+  }, []);
+
   return (
     <div className={ingredientsStyles.mainBlock}>
       <section className={ingredientsStyles.header}>
         <article className="text text_type_main-large">Соберите бургер</article>
       </section>
-      <IngredientsMenu />
-      <section className={ingredientsStyles.scrollableContainer}>
-        <section>
+      <IngredientsMenu activeTab={activeTab} setActiveTab={scrollToSection} />
+      <section
+        ref={containerRef}
+        className={ingredientsStyles.scrollableContainer}
+      >
+        <section ref={bunRef}>
           <article className="text text_type_main-medium">Булки</article>
           <ul className={ingredientsStyles.itemsGrid}>
             {buns.map((ingredient) => (
               <IngredientItem
                 key={ingredient._id}
                 ingredient={ingredient}
                 count={getIngredientCount(ingredient)}
-                setModalState={setModalState}
               />
             ))}
           </ul>
         </section>
-        <section>
+        <section ref={sauceRef}>
           <article className="text text_type_main-medium">Соусы</article>
           <ul className={ingredientsStyles.itemsGrid}>
             {sauces.map((ingredient) => (
               <IngredientItem
                 key={ingredient._id}
path: src/components/burger-ingredients/burger-ingredients.jsx
user: login: kyzinatra
body: Можно лучше: все 3 категории очень похожи. Стоит вынести в отдельный компонент
created_at: 2025-05-02T16:28:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,66 @@
+import { useRef } from "react";
+import { useDispatch } from "react-redux";
+import { useDrag, useDrop } from "react-dnd";
+
+import { IngredientType } from "../../utils/types.js";
+import {
+  handleMove,
+  handleRemove,
+} from "../../services/burger-constructor-slice";
+import styles from "./burger-constructor.module.css";
+import {
+  ConstructorElement,
+  DragIcon,
+} from "@ya.praktikum/react-developer-burger-ui-components";
+import PropTypes from "prop-types";
+
+function IngredientElement({ ingredient, newIndex }) {
+  const dispatch = useDispatch();
+  const ref = useRef(null);
+
+  const [{ opacity }, drag] = useDrag({
+    type: "moveIngredient",
+    item: { ingredient, index: newIndex },
+    collect: (monitor) => ({
+      opacity: monitor.isDragging() ? 0.5 : 1,
+    }),
+  });
+
+  const [, drop] = useDrop({
+    accept: "moveIngredient",
+    collect: (monitor) => ({
+      isHover: monitor.isOver(),
+    }),
+    hover(draggedItem) {
+      dispatch(
+        handleMove({
+          ingredient: draggedItem.ingredient,
+          toIndex: newIndex,
+        })
+      );
+    },
+  });
+
+  drag(drop(ref));
+
+  return (
+    <section className={styles.ingredientsList} style={{ opacity }} ref={ref}>
+      <div key={ingredient.uniqueId} className={styles.ingredientItem}>
+        <DragIcon type="primary" />
+        <ConstructorElement
+          text={ingredient.name}
+          price={ingredient.price}
path: src/components/burger-constructor/ingredient-element.jsx
user: login: kyzinatra
body: Отлично! Хорошая реализация DnD
created_at: 2025-05-02T16:29:40Z
author_association: NONE

PR comments:
user: login: kyzinatra
created_at: 2025-05-02T16:30:03Z
body: Выглядит очень круто! Видно, что проделана большая работа. Но все же есть моменты на которые стоит обратить внимание.
Подробнее: 

**Можно лучше:**

-  селекторы стоит делать максимально специфичными. Так как `redux` сравнивает прошлое и новое значение, возвращенное селектором просто через `===`, то стоит возвращать из селектора только то, что нам нужно, чтобы избежать лишних перерендеров
`const hasError = useSelector((state) => state.burgerIngredients.hasError);`
Подробнее о том, как работает `useSelector()` изнутри: https://youtu.be/SVG-x-4BQic
-  фильтры лучше обернуть в `useMemo()`
-  эту логику лучше вынести в отдельный хук, чтобы не перегружать компонент
-  Булочки добавляются по две, счетчик стоит поправить
-  все 3 категории очень похожи. Стоит вынести в отдельный компонент


**Что понравилось:**

-  Понравилось, что вы обернули в провайдер только то, что нужно для DnD
-  Хорошая реализация DnD


author_association: NONE
user: login: AsadovEZO
created_at: 2025-05-18T06:19:38Z
body: Спасибо за ревью! Внес все правки. Код стал чище и чуть более аккуратно структурирован
author_association: OWNER