From 887c72589946782fc9184ae201e2b5cbaf83e693 Mon Sep 17 00:00:00 2001 From: Toheed Asghar Date: Sat, 22 Nov 2025 19:03:37 +0500 Subject: [PATCH 1/3] fix: make tree updates immutable --- package-lock.json | 6 ++++++ src/App.jsx | 4 ++-- src/hooks/use-traverse-tree.js | 28 +++++++++++++++------------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 33193cc..967bcb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -1205,6 +1206,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1422,6 +1424,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", @@ -1787,6 +1790,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3292,6 +3296,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3879,6 +3884,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.19.3", "postcss": "^8.4.32", diff --git a/src/App.jsx b/src/App.jsx index f5949bb..9d4b06f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,9 +7,9 @@ import useTraverseTree from "./hooks/use-traverse-tree"; function App() { const [explorerData, setExplorerData] = useState(folderData); const { insertNode, deleteNode, updateNode } = useTraverseTree(); + const handleInsertNode = (folderId, itemName, isFolder) => { - const finalItem = insertNode(explorerData, folderId, itemName, isFolder); - return finalItem; + setExplorerData((prev) => insertNode(prev, folderId, itemName, isFolder)); }; const handleDeleteNode = (folderId) => { // Call deleteNode to get the modified tree diff --git a/src/hooks/use-traverse-tree.js b/src/hooks/use-traverse-tree.js index 3cdb5de..aa561e0 100644 --- a/src/hooks/use-traverse-tree.js +++ b/src/hooks/use-traverse-tree.js @@ -1,21 +1,23 @@ const useTraverseTree = () => { function insertNode(tree, folderId, itemName, isFolder) { if (tree.id === folderId && tree.isFolder) { - tree.items.unshift({ - id: new Date().getTime(), - name: itemName, - isFolder, - items: [], - }); - return tree; + const newChild = { + id: Date.now(), + name: itemName, + isFolder, + items: [] + }; + + return {...tree, items: [newChild, ...(tree.items || [] )]}; } - tree.items.map((obj) => { - let res = insertNode(obj, folderId, itemName, isFolder); - if (res) { - return res; - } - }); + if(!tree.items) return tree; + + return { + ...tree, + items: tree.items.map((child) => + insertNode(child, folderId, itemName, isFolder) + )}; } function deleteNode(tree, folderId) { // base case: found the node to be deleted From 0cb0865b9788c2beb241821a733bdf5dcc9fb361 Mon Sep 17 00:00:00 2001 From: Toheed Asghar Date: Sat, 22 Nov 2025 21:18:33 +0500 Subject: [PATCH 2/3] feat: persist explorerData to localStorage; memoize Folder and handlers --- src/App.jsx | 37 +++++++++++++++++++++++++------------ src/components/Folder.jsx | 7 +++---- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 9d4b06f..520344a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,28 +1,41 @@ -import { useState } from "react"; +import { useState, useEffect, useCallback } from "react"; import "./App.css"; import folderData from "./data/folderData"; import Folder from "./components/Folder"; import useTraverseTree from "./hooks/use-traverse-tree"; function App() { - const [explorerData, setExplorerData] = useState(folderData); + const [explorerData, setExplorerData] = useState(() => { + try { + const stored = localStorage.getItem("explorerData"); + return stored ? JSON.parse(stored) : folderData; + } catch { + return folderData; + } + }); const { insertNode, deleteNode, updateNode } = useTraverseTree(); - - const handleInsertNode = (folderId, itemName, isFolder) => { + + useEffect(() => { + try { + localStorage.setItem("explorerData", JSON.stringify(explorerData)); + } catch { + // ignore storage errors + } + }, [explorerData]); + + const handleInsertNode = useCallback((folderId, itemName, isFolder) => { setExplorerData((prev) => insertNode(prev, folderId, itemName, isFolder)); - }; - const handleDeleteNode = (folderId) => { - // Call deleteNode to get the modified tree + }, [insertNode]); + + const handleDeleteNode = useCallback((folderId) => { const finalItem = deleteNode(explorerData, folderId); - // Update the explorerData state with the modified tree setExplorerData(finalItem); - }; + }, [explorerData, deleteNode]); - const handleUpdateFolder = (id, updatedValue, isFolder) => { + const handleUpdateFolder = useCallback((id, updatedValue, isFolder) => { const finalItem = updateNode(explorerData, id, updatedValue, isFolder); - // Update the explorerData state with the modified tree setExplorerData(finalItem); - }; + }, [explorerData, updateNode]); return (
diff --git a/src/components/Folder.jsx b/src/components/Folder.jsx index caf8dd5..b140850 100644 --- a/src/components/Folder.jsx +++ b/src/components/Folder.jsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, memo } from "react"; import { VscChevronRight, VscChevronDown, @@ -131,12 +131,11 @@ const Folder = ({ )} {explorerData.items.map((item, index) => { return ( - ); })} @@ -178,4 +177,4 @@ const Folder = ({ } }; -export default Folder; +export default memo(Folder); From b52264a8e65c2bc317b52ccb7e6c6167b088deeb Mon Sep 17 00:00:00 2001 From: Toheed Asghar Date: Sat, 22 Nov 2025 21:31:47 +0500 Subject: [PATCH 3/3] feat(rtk): integrate Redux Toolkit with persistence --- package-lock.json | 125 ++++++++++++++++++++++--- package.json | 4 +- src/App.jsx | 44 +-------- src/Repository/Slices/explorerSlice.js | 102 ++++++++++++++++++++ src/Repository/Store.js | 37 ++++++++ src/components/Folder.jsx | 66 ++++++------- src/main.jsx | 6 +- 7 files changed, 296 insertions(+), 88 deletions(-) create mode 100644 src/Repository/Slices/explorerSlice.js create mode 100644 src/Repository/Store.js diff --git a/package-lock.json b/package-lock.json index 967bcb5..98b0702 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "file-manager", "version": "0.0.0", "dependencies": { + "@reduxjs/toolkit": "^2.10.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^5.0.1" + "react-icons": "^5.0.1", + "react-redux": "^9.2.0" }, "devDependencies": { "@types/react": "^18.2.43", @@ -72,7 +74,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -928,6 +929,32 @@ "node": ">= 8" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", + "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.2.0", + "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/@rollup/rollup-android-arm-eabi": { "version": "4.9.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", @@ -1097,6 +1124,18 @@ "win32" ] }, + "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/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1148,13 +1187,13 @@ "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.2.48", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1174,7 +1213,13 @@ "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "devOptional": true + }, + "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/@ungap/structured-clone": { "version": "1.2.0", @@ -1206,7 +1251,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1424,7 +1468,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", @@ -1540,7 +1583,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "devOptional": true }, "node_modules/debug": { "version": "4.3.4", @@ -1790,7 +1833,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2432,6 +2474,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3296,7 +3348,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3330,6 +3381,29 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "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.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -3339,6 +3413,21 @@ "node": ">=0.10.0" } }, + "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.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -3376,6 +3465,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -3879,12 +3974,20 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.19.3", "postcss": "^8.4.32", diff --git a/package.json b/package.json index b4f4da5..50c0ceb 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@reduxjs/toolkit": "^2.10.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^5.0.1" + "react-icons": "^5.0.1", + "react-redux": "^9.2.0" }, "devDependencies": { "@types/react": "^18.2.43", diff --git a/src/App.jsx b/src/App.jsx index 520344a..d58610b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,52 +1,16 @@ -import { useState, useEffect, useCallback } from "react"; +import { useSelector } from "react-redux"; import "./App.css"; -import folderData from "./data/folderData"; import Folder from "./components/Folder"; -import useTraverseTree from "./hooks/use-traverse-tree"; +import { selectRootId } from "./Repository/Slices/explorerSlice"; function App() { - const [explorerData, setExplorerData] = useState(() => { - try { - const stored = localStorage.getItem("explorerData"); - return stored ? JSON.parse(stored) : folderData; - } catch { - return folderData; - } - }); - const { insertNode, deleteNode, updateNode } = useTraverseTree(); - - useEffect(() => { - try { - localStorage.setItem("explorerData", JSON.stringify(explorerData)); - } catch { - // ignore storage errors - } - }, [explorerData]); - - const handleInsertNode = useCallback((folderId, itemName, isFolder) => { - setExplorerData((prev) => insertNode(prev, folderId, itemName, isFolder)); - }, [insertNode]); - - const handleDeleteNode = useCallback((folderId) => { - const finalItem = deleteNode(explorerData, folderId); - setExplorerData(finalItem); - }, [explorerData, deleteNode]); - - const handleUpdateFolder = useCallback((id, updatedValue, isFolder) => { - const finalItem = updateNode(explorerData, id, updatedValue, isFolder); - setExplorerData(finalItem); - }, [explorerData, updateNode]); + const rootId = useSelector(selectRootId); return (
- + {rootId ? : null}
Your content will be here
diff --git a/src/Repository/Slices/explorerSlice.js b/src/Repository/Slices/explorerSlice.js new file mode 100644 index 0000000..e9ca841 --- /dev/null +++ b/src/Repository/Slices/explorerSlice.js @@ -0,0 +1,102 @@ +import { createSlice } from "@reduxjs/toolkit"; +import folderData from "../../data/folderData"; + +const generateId = () => + typeof crypto !== "undefined" && crypto.randomUUID + ? crypto.randomUUID() + : String(Date.now()); + +function normalizeTree(node) { + const byId = {}; + function walk(n) { + const id = n.id ?? generateId(); + const children = Array.isArray(n.items) ? n.items : []; + const childIds = children.map((c) => { + const childId = walk(c); + return childId; + }); + byId[id] = { + id, + name: n.name, + isFolder: !!n.isFolder, + childrenIds: childIds, + }; + return id; + } + const rootId = walk(folderData); // seed from existing data + return { byId, rootId }; +} + +const initial = normalizeTree(folderData); + +const explorerSlice = createSlice({ + name: "explorer", + initialState: { + nodes: { byId: initial.byId }, + rootId: initial.rootId, + }, + reducers: { + insertNode: { + prepare: (parentId, name, isFolder) => ({ + payload: { parentId, name: String(name).trim(), isFolder: !!isFolder }, + }), + reducer: (state, action) => { + const { parentId, name, isFolder } = action.payload; + if (!name) return; + const parent = state.nodes.byId[parentId]; + if (!parent || !parent.isFolder) return; + + const id = generateId(); + state.nodes.byId[id] = { + id, + name, + isFolder, + childrenIds: [], + }; + parent.childrenIds.unshift(id); + }, + }, + renameNode(state, action) { + const { id, name } = action.payload; + const node = state.nodes.byId[id]; + if (node && String(name).trim()) { + node.name = String(name).trim(); + } + }, + deleteNode(state, action) { + const { id } = action.payload; + if (!state.nodes.byId[id]) return; + + // remove id from any parent list (O(n) but simple) + Object.values(state.nodes.byId).forEach((n) => { + if (n.childrenIds?.length) { + n.childrenIds = n.childrenIds.filter((cid) => cid !== id); + } + }); + + function removeRecursively(nodeId) { + const node = state.nodes.byId[nodeId]; + if (!node) return; + if (node.childrenIds?.length) { + node.childrenIds.forEach(removeRecursively); + } + delete state.nodes.byId[nodeId]; + } + // if deleting root, just clear everything but keep empty root + if (id === state.rootId) { + const root = state.nodes.byId[state.rootId]; + root.childrenIds.forEach(removeRecursively); + root.childrenIds = []; + return; + } + removeRecursively(id); + }, + }, +}); + +export const { insertNode, renameNode, deleteNode } = explorerSlice.actions; + +export const selectRootId = (state) => state.explorer.rootId; +export const selectNodeById = (state, id) => state.explorer.nodes.byId[id]; + +export default explorerSlice.reducer; \ No newline at end of file diff --git a/src/Repository/Store.js b/src/Repository/Store.js new file mode 100644 index 0000000..fc3d172 --- /dev/null +++ b/src/Repository/Store.js @@ -0,0 +1,37 @@ +import { configureStore } from "@reduxjs/toolkit"; +import explorerReducer from "./Slices/explorerSlice"; + +const LS_KEY = "explorerState"; + +function loadState() { + try { + const raw = localStorage.getItem(LS_KEY); + return raw ? JSON.parse(raw) : undefined; + } catch { + return undefined; + } +} + +function saveState(state) { + try { + localStorage.setItem(LS_KEY, JSON.stringify(state)); + } catch { + // ignore + } +} + +const preloadedState = loadState(); + +export const store = configureStore({ + reducer: { + explorer: explorerReducer, + }, + preloadedState, +}); + +store.subscribe(() => { + const state = store.getState(); + saveState({ + explorer: state.explorer, + }); +}); \ No newline at end of file diff --git a/src/components/Folder.jsx b/src/components/Folder.jsx index b140850..0e2e396 100644 --- a/src/components/Folder.jsx +++ b/src/components/Folder.jsx @@ -1,4 +1,5 @@ -import { useState, memo } from "react"; +import { useState, memo, useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; import { VscChevronRight, VscChevronDown, @@ -9,16 +10,17 @@ import { VscEdit, VscTrash, } from "react-icons/vsc"; +import { + insertNode, + deleteNode, + renameNode, + selectNodeById, +} from "../Repository/Slices/explorerSlice"; -const Folder = ({ - handleInsertNode, - handleDeleteNode, - handleUpdateFolder, - explorerData, -}) => { - const [nodeName, setNodeName] = useState( - explorerData?.name ? explorerData.name : "" - ); +const Folder = ({ nodeId }) => { + const dispatch = useDispatch(); + const explorerData = useSelector((s) => selectNodeById(s, nodeId)); + const [nodeName, setNodeName] = useState(explorerData?.name ?? ""); const [expand, setExpand] = useState(false); const [showInput, setShowInput] = useState({ visible: false, @@ -29,47 +31,46 @@ const Folder = ({ isFolder: null, }); - const handleNewFolderButton = (e, isFolder) => { + if (!explorerData) return null; + + const handleNewFolderButton = useCallback((e, isFolder) => { e.stopPropagation(); setExpand(true); setShowInput({ visible: true, isFolder, }); - }; + }, []); - const handleUpdateFolderButton = (e, isFolder, nodeValue) => { + const handleUpdateFolderButton = useCallback((e, isFolder, nodeValue) => { setNodeName(nodeValue); e.stopPropagation(); setUpdateInput({ visible: true, isFolder, }); - }; + }, []); - const handleDeleteFolder = (e, isFolder) => { + const handleDeleteFolder = useCallback((e) => { e.stopPropagation(); - handleDeleteNode(explorerData.id); - }; - const onAdd = (e) => { - if (e.keyCode === 13 && e.target.value) { - // Lets add logic for add folder - handleInsertNode(explorerData.id, e.target.value, showInput.isFolder); + dispatch(deleteNode({ id: explorerData.id })); + }, [dispatch, explorerData?.id]); + const onAdd = useCallback((e) => { + if (e.key === "Enter" && e.target.value.trim()) { + dispatch(insertNode(explorerData.id, e.target.value.trim(), !!showInput.isFolder)); setShowInput({ ...showInput, visible: false }); } - }; - const onUpdate = (e) => { - if (e.keyCode === 13 && e.target.value) { - // Lets add logic for update folder - handleUpdateFolder(explorerData.id, e.target.value, true); + }, [dispatch, explorerData?.id, showInput]); + const onUpdate = useCallback((e) => { + if (e.key === "Enter" && e.target.value.trim()) { + dispatch(renameNode({ id: explorerData.id, name: e.target.value.trim() })); setUpdateInput({ ...updateInput, visible: false }); } - }; + }, [dispatch, explorerData?.id, updateInput]); const handleChange = (event) => { setNodeName(event.target.value); }; if (explorerData.isFolder) { - console.log("nodeName", nodeName); return (
)} - {explorerData.items.map((item, index) => { + {explorerData.childrenIds.map((childId) => { return ( - + ); })}
diff --git a/src/main.jsx b/src/main.jsx index 54b39dd..a86cf6f 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,9 +2,13 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' import './index.css' +import { Provider } from 'react-redux' +import { store } from './Repository/Store.js' ReactDOM.createRoot(document.getElementById('root')).render( - + + + , )