From 40c1cba256ebe144e734863dd9f64c7f797a8019 Mon Sep 17 00:00:00 2001 From: anshuthopsee Date: Thu, 30 Nov 2023 00:11:12 +0000 Subject: [PATCH] migrate state managment to reduxjs/toolkit --- package-lock.json | 189 +++++++++++++++++- package.json | 2 + src/App.jsx | 167 +++++++++------- src/Contexts/CustomPresetsContextProvider.jsx | 64 ------ src/Contexts/StrategyContextProvider.jsx | 32 --- src/components/Chart/PayoffChart.jsx | 11 +- src/components/Chart/index.jsx | 9 +- src/components/Configure/AddLegs.jsx | 16 +- src/components/Configure/Header.jsx | 24 ++- src/components/Configure/LegsWindow.jsx | 29 +-- src/components/Configure/SaveNewDialog.jsx | 17 +- src/components/Configure/index.jsx | 7 +- src/components/Presets/BullCallSpreadSVG.jsx | 14 +- src/components/Presets/BullPutSpreadSVG.jsx | 14 +- src/components/Presets/CustomSVG.jsx | 45 ++++- src/components/Presets/ShortStraddleSVG.jsx | 16 +- src/components/Presets/ShortStrangleSVG.jsx | 14 +- src/components/Presets/index.jsx | 11 +- src/features/custom/customSlice.js | 34 ++++ src/features/selected/selectedSlice.js | 32 +++ src/hooks/usePathBasedLegs.js | 44 ---- src/hooks/usePathBasedPresetName.js | 49 ----- src/main.jsx | 18 +- src/store.js | 55 +++++ src/utils.js | 22 ++ 25 files changed, 588 insertions(+), 347 deletions(-) delete mode 100644 src/Contexts/CustomPresetsContextProvider.jsx delete mode 100644 src/Contexts/StrategyContextProvider.jsx create mode 100644 src/features/custom/customSlice.js create mode 100644 src/features/selected/selectedSlice.js delete mode 100644 src/hooks/usePathBasedLegs.js delete mode 100644 src/hooks/usePathBasedPresetName.js create mode 100644 src/store.js create mode 100644 src/utils.js diff --git a/package-lock.json b/package-lock.json index 8937fe7..7892081 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,11 @@ "@mui/icons-material": "^5.14.1", "@mui/material": "^5.14.0", "@mui/x-data-grid": "^6.11.0", + "@reduxjs/toolkit": "^1.9.7", "d3": "^7.8.5", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^8.1.3", "react-router-dom": "^6.17.0" }, "devDependencies": { @@ -1088,6 +1090,29 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", + "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.10.0.tgz", @@ -1290,6 +1315,15 @@ "node": ">=10" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -1314,7 +1348,7 @@ "version": "18.2.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz", "integrity": "sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } @@ -1340,6 +1374,11 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.3.2.tgz", @@ -2820,6 +2859,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "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", @@ -3599,6 +3647,49 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-redux": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/react-router": { "version": "6.17.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.17.0.tgz", @@ -3644,6 +3735,22 @@ "react-dom": ">=16.6.0" } }, + "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==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -4050,6 +4157,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/vite": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.3.tgz", @@ -4799,6 +4914,17 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, + "@reduxjs/toolkit": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", + "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", + "requires": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + } + }, "@remix-run/router": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.10.0.tgz", @@ -4892,6 +5018,15 @@ "dev": true, "optional": true }, + "@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -4916,7 +5051,7 @@ "version": "18.2.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz", "integrity": "sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==", - "dev": true, + "devOptional": true, "requires": { "@types/react": "*" } @@ -4942,6 +5077,11 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "@vitejs/plugin-react-swc": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.3.2.tgz", @@ -6034,6 +6174,11 @@ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -6575,6 +6720,26 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-redux": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "react-router": { "version": "6.17.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.17.0.tgz", @@ -6603,6 +6768,20 @@ "prop-types": "^15.6.2" } }, + "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==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "requires": {} + }, "regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -6891,6 +7070,12 @@ "punycode": "^2.1.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "vite": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.3.tgz", diff --git a/package.json b/package.json index 021e66d..01dcb1d 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,11 @@ "@mui/icons-material": "^5.14.1", "@mui/material": "^5.14.0", "@mui/x-data-grid": "^6.11.0", + "@reduxjs/toolkit": "^1.9.7", "d3": "^7.8.5", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^8.1.3", "react-router-dom": "^6.17.0" }, "devDependencies": { diff --git a/src/App.jsx b/src/App.jsx index 78737dd..d2b2893 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,6 @@ import { useContext, useState, useEffect } from 'react'; -import { useLocation } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; +import { useSelector, useDispatch } from 'react-redux'; import createTheme from '@mui/material/styles/createTheme'; import ThemeProvider from '@mui/material/styles/ThemeProvider'; import CssBaseline from '@mui/material/CssBaseline'; @@ -11,9 +12,7 @@ import Box from '@mui/material/Box'; import LightModeIcon from '@mui/icons-material/LightMode'; import DarkModeIcon from '@mui/icons-material/DarkMode'; import { DarkModeContext } from './Contexts/DarkModeContextProvider'; -import StrategyContextProvider from './Contexts/StrategyContextProvider'; import ToastContextProvider from './Contexts/ToastContextProvider'; -import CustomPresetsContextProvider from './Contexts/CustomPresetsContextProvider'; import Chart from './components/Chart'; import Configure from './components/Configure'; import Presets from './components/Presets'; @@ -27,12 +26,21 @@ import Link from '@mui/material/Link'; import GitHubIcon from '@mui/icons-material/GitHub'; import useMediaQuery from '@mui/material/useMediaQuery'; import Toast from './components/Toast'; +import { splitWords, getPathName } from './utils'; +import { PRESETS } from './const/presets'; +import { setSelectedStrategy } from './features/selected/selectedSlice'; +import { getCustomStrategies } from './features/custom/customSlice'; function App() { const { darkMode, setDarkMode } = useContext(DarkModeContext); const [drawerOpen, setDrawerOpen] = useState(false); const viewportTheme = useTheme(); const isLargeScreen = useMediaQuery(viewportTheme.breakpoints.up("lg")); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const customStrategies = useSelector(getCustomStrategies); + const location = useLocation(); + const pathName = splitWords(getPathName(location)); const theme = createTheme({ palette: { @@ -84,7 +92,34 @@ function App() { if (isLargeScreen) { setDrawerOpen(false); } - }, [isLargeScreen]) + }, [isLargeScreen]); + + useEffect(() => { + if (window.location.hash) return; + history.replaceState(null, document.title, document.URL.split('#')[0] + "#/Short-Straddle"); + }, []); + + useEffect(() => { + const inPresets = Object.keys(PRESETS).includes(pathName); + if (inPresets) { + const selectedStrategy = { + strategyName: pathName, + strategyLegs: PRESETS[pathName], + }; + dispatch(setSelectedStrategy(selectedStrategy)); + } else { + const customStrategy = customStrategies.find((strategy) => { + return strategy.name === pathName; + }); + if (customStrategy) { + const selectedStrategy = { + strategyName: customStrategy.name, + strategyLegs: customStrategy.legs, + }; + dispatch(setSelectedStrategy(selectedStrategy)); + }; + }; + }, [pathName]); return ( @@ -137,72 +172,68 @@ function App() { - - - + + + +
+ + + + + + + + + + + +
+ + - - - -
- - - - - - - - - - - -
- - - - anshuthopsee/option-payoff - - -
-
-
+ + anshuthopsee/option-payoff + + +
diff --git a/src/Contexts/CustomPresetsContextProvider.jsx b/src/Contexts/CustomPresetsContextProvider.jsx deleted file mode 100644 index 2d1795a..0000000 --- a/src/Contexts/CustomPresetsContextProvider.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useContext, createContext } from 'react'; -import { useNavigate } from 'react-router-dom'; -import useLocalStorage from '../hooks/useLocalStorage'; -import { StrategyContext } from './StrategyContextProvider'; - -export const CustomPresetsContext = createContext(); - -const CustomPresetsContextProvider = ({ children }) => { - const navigate = useNavigate(); - const [customPresets, setCustomPresets] = useLocalStorage("customPresets", []); - const { - selectedPreset, - legs, - } = useContext(StrategyContext); - - const saveNewCustomPreset = (name) => { - const newCustomPreset = { - name: name, - legs: [...legs] - }; - const newCustomPresets = [...customPresets, newCustomPreset]; - if (customPresets.length <= 4) { - setCustomPresets(newCustomPresets); - }; - }; - - const saveCustomPreset = () => { - const updatedPreset = { name: selectedPreset.name, legs: legs } - setCustomPresets( - customPresets.map((preset) => (preset.name === selectedPreset.name) ? updatedPreset : preset) - ); - }; - - const deleteCustomPreset = (index) => { - const tobeDeleted = customPresets[index]; - - if (tobeDeleted.name === selectedPreset.name) { - const precedingPresetIndex = index-1; - if (precedingPresetIndex > -1) { - const precedingPresetName = customPresets[precedingPresetIndex].name; - navigate(`#/${precedingPresetName}`); - } else { - navigate("#/Bull-Put-Spread"); - }; - }; - setCustomPresets(customPresets.filter((preset, i) => index !== i)) - }; - - const value = { - customPresets: customPresets, - setCustomPresets: setCustomPresets, - deleteCustomPreset: deleteCustomPreset, - saveCustomPreset: saveCustomPreset, - saveNewCustomPreset: saveNewCustomPreset, - }; - - return ( - - {children} - - ); -}; - -export default CustomPresetsContextProvider; \ No newline at end of file diff --git a/src/Contexts/StrategyContextProvider.jsx b/src/Contexts/StrategyContextProvider.jsx deleted file mode 100644 index b010bdb..0000000 --- a/src/Contexts/StrategyContextProvider.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { createContext } from 'react'; -import usePathBasedPresetName from '../hooks/usePathBasedPresetName'; -import usePathBasedLegs from '../hooks/usePathBasedLegs'; -import { PRESETS } from '../const/presets'; - -export const StrategyContext = createContext(); - -const defaultPresetName = "Short Straddle"; - -const StrategyContextProvider = ({ children }) => { - - const [selectedPreset, setSelectedPreset] = usePathBasedPresetName({ - name: defaultPresetName, - custom: false - }); - - const [legs, setLegs] = usePathBasedLegs(selectedPreset.name); - - const updateLegs = (updatedLegs) => { - if (updatedLegs.length <= 10) { - setLegs(updatedLegs); - }; - }; - - return ( - - {children} - - ); -}; - -export default StrategyContextProvider; \ No newline at end of file diff --git a/src/components/Chart/PayoffChart.jsx b/src/components/Chart/PayoffChart.jsx index b481e1a..2994c5a 100644 --- a/src/components/Chart/PayoffChart.jsx +++ b/src/components/Chart/PayoffChart.jsx @@ -1,5 +1,6 @@ -import { useRef, useEffect, useState, useContext } from 'react'; -import { StrategyContext } from '../../Contexts/StrategyContextProvider'; +import { useRef, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { getSelectedStrategyLegs } from '../../features/selected/selectedSlice'; import * as d3 from 'd3'; const tooltipStyle = { @@ -71,7 +72,7 @@ const PayoffChart = () => { const chartAreaRef = useRef(); const [height, setHeight] = useState(0); const [width, setWidth] = useState(0); - const { legs } = useContext(StrategyContext); + const selectedStrategyLegs = useSelector(getSelectedStrategyLegs); const resizeObserver = new ResizeObserver((entries) => { const chartParent = entries[0].target; @@ -88,7 +89,7 @@ const PayoffChart = () => { const WIDTH = width - MARGIN.LEFT - MARGIN.RIGHT; const HEIGHT = height - MARGIN.TOP - MARGIN.BOTTOM; - const selectedLegs = legs.filter(leg => leg.selected); + const selectedLegs = selectedStrategyLegs.filter(leg => leg.selected); selectedLegs.sort((a, b) => a.strike - b.strike); const data = generatePayoffDiagram(selectedLegs); @@ -326,7 +327,7 @@ const PayoffChart = () => { return () => { resizeObserver.unobserve(chartParentRef.current); }; - }, [width, legs]); + }, [width, selectedStrategyLegs]); return (
{ const chartAreaRef = useRef(); - const { selectedPreset } = useContext(StrategyContext); + const selectedStrategyName = useSelector(getSelectedStrategyName); return ( { } }> - {`${selectedPreset.name} Payoff at Expiry`} + {`${selectedStrategyName} Payoff at Expiry`} diff --git a/src/components/Configure/AddLegs.jsx b/src/components/Configure/AddLegs.jsx index de7ca97..aeba6da 100644 --- a/src/components/Configure/AddLegs.jsx +++ b/src/components/Configure/AddLegs.jsx @@ -1,15 +1,17 @@ -import { useState, useContext } from "react" +import { useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import Grid from '@mui/material/Grid'; import IconButton from '@mui/material/IconButton'; import MenuItem from '@mui/material/MenuItem'; import AddIcon from '@mui/icons-material/Add'; -import { StrategyContext } from '../../Contexts/StrategyContextProvider'; +import { getSelectedStrategyLegs, updateStrategyLegs } from "../../features/selected/selectedSlice"; const AddLegs = () => { const inputHeaders = ["action", "strike", "premium", "type"]; - const { legs, updateLegs } = useContext(StrategyContext); + const dispatch = useDispatch(); + const selectedStrategyLegs = useSelector(getSelectedStrategyLegs); const [action, setAction] = useState(''); const [strike, setStrike] = useState(''); @@ -62,12 +64,12 @@ const AddLegs = () => { const addLeg = () => { const updatedLegs = []; - for (let i = 0; i < legs.length; i++) { - updatedLegs[i] = { ...legs[i], id: i }; + for (let i = 0; i < selectedStrategyLegs.length; i++) { + updatedLegs[i] = { ...selectedStrategyLegs[i], id: i }; }; updatedLegs.push({ - id: legs.length, + id: selectedStrategyLegs.length, type: type, action: action, strike: Number(strike), @@ -75,7 +77,7 @@ const AddLegs = () => { selected: true, }); - updateLegs(updatedLegs); + dispatch(updateStrategyLegs(updatedLegs)); }; const renderTextField = (labelText, onChange, value, selectItems, inputProps) => { diff --git a/src/components/Configure/Header.jsx b/src/components/Configure/Header.jsx index b51f54a..b96576e 100644 --- a/src/components/Configure/Header.jsx +++ b/src/components/Configure/Header.jsx @@ -1,6 +1,7 @@ import { useState, useContext } from 'react'; -import { StrategyContext } from '../../Contexts/StrategyContextProvider'; -import { CustomPresetsContext } from '../../Contexts/CustomPresetsContextProvider'; +import { useSelector, useDispatch } from 'react-redux'; +import { getSelectedStrategyName, getSelectedStrategyLegs } from '../../features/selected/selectedSlice'; +import { getCustomStrategies, updateCustomStrategy } from '../../features/custom/customSlice'; import { ToastContext } from '../../Contexts/ToastContextProvider'; import { Box, Typography, IconButton, Menu, MenuItem } from '@mui/material'; import SaveNewDialog from './SaveNewDialog'; @@ -10,9 +11,11 @@ const Header = () => { const [anchorEl, setAnchorEl] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const open = Boolean(anchorEl); - const { selectedPreset } = useContext(StrategyContext); - const { customPresets, saveCustomPreset } = useContext(CustomPresetsContext); + const dispatch = useDispatch(); + const selectedStrategyName = useSelector(getSelectedStrategyName); + const selectedStrategyLegs = useSelector(getSelectedStrategyLegs); const { toggleToast } = useContext(ToastContext); + const customStrategies = useSelector(getCustomStrategies); const handleClick = (e) => { setAnchorEl(e.currentTarget); @@ -23,13 +26,18 @@ const Header = () => { const handleSaveClick = () => { handleClose(); - saveCustomPreset(); + + dispatch(updateCustomStrategy({ + name: selectedStrategyName, + legs: selectedStrategyLegs + })); + toggleToast({ show: true, severity: "success", - message: `Strategy "${selectedPreset.name}" updated.`, + message: `Strategy "${selectedStrategyName}" updated.`, key: new Date().getTime() - }) + }); }; const handleSaveNewClick = () => { @@ -38,7 +46,7 @@ const Header = () => { }; const isCustomPresetSelected = () => { - return customPresets.every((preset) => preset.name !== selectedPreset.name); + return customStrategies.every((preset) => preset.name !== selectedStrategyName); }; return ( diff --git a/src/components/Configure/LegsWindow.jsx b/src/components/Configure/LegsWindow.jsx index beffdbd..078ba12 100644 --- a/src/components/Configure/LegsWindow.jsx +++ b/src/components/Configure/LegsWindow.jsx @@ -1,4 +1,5 @@ -import { useState, useContext, useMemo } from 'react'; +import { useState, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/DeleteOutlined'; import SaveIcon from '@mui/icons-material/Save'; @@ -11,33 +12,35 @@ import { } from '@mui/x-data-grid'; import CustomGridInputCell from './CustomGridInputCell'; import NoLegsOverlay from './NoLegsOverlay'; -import { StrategyContext } from '../../Contexts/StrategyContextProvider'; +import { getSelectedStrategyLegs, updateStrategyLegs } from '../../features/selected/selectedSlice'; export default function LegsWindow({ legs }) { - const { updateLegs } = useContext(StrategyContext); + const dispatch = useDispatch(); + + const selectedStrategyLegs = useSelector(getSelectedStrategyLegs); const selectionModel = useMemo(() => { - return legs.filter(leg => leg.selected).map(leg => leg.id); - }, [legs]); + return selectedStrategyLegs.filter(leg => leg.selected).map(leg => leg.id); + }, [selectedStrategyLegs]); const [rowModesModel, setRowModesModel] = useState({}); const handleDeleteClick = (id) => (e) => { e.stopPropagation(); - const updatedLegs = legs.filter((row) => row.id !== id); - updateLegs(updatedLegs); + const updatedLegs = selectedStrategyLegs.filter((row) => row.id !== id); + dispatch(updateStrategyLegs(updatedLegs)); }; const handleRowSelectedModelChange = (newSelectionModel) => { - const deepCopyLegs = JSON.parse(JSON.stringify(legs)); + const deepCopyLegs = JSON.parse(JSON.stringify(selectedStrategyLegs)); const updatedLegs = deepCopyLegs.map((leg) => { leg.selected = newSelectionModel.includes(leg.id); return leg; }); - updateLegs(updatedLegs); + dispatch(updateStrategyLegs(updatedLegs)); }; const handleRowEditStop = (params, event) => { @@ -60,9 +63,9 @@ export default function LegsWindow({ legs }) { [id]: { mode: GridRowModes.View, ignoreModifications: true }, }); - const editedRow = legs.find((row) => row.id === id); + const editedRow = selectedStrategyLegs.find((row) => row.id === id); if (editedRow.isNew) { - updateLegs(legs.filter((row) => row.id !== id)); + dispatch(updateStrategyLegs(selectedStrategyLegs.filter((row) => row.id !== id))); }; }; @@ -72,7 +75,7 @@ export default function LegsWindow({ legs }) { const processRowUpdate = (newRow) => { const updatedRow = { ...newRow, isNew: false }; - updateLegs(legs.map((row) => (row.id === newRow.id ? updatedRow : row))); + dispatch(updateStrategyLegs(selectedStrategyLegs.map((row) => (row.id === newRow.id ? updatedRow : row)))); return updatedRow; }; @@ -193,7 +196,7 @@ export default function LegsWindow({ legs }) {
{ setDialogOpen(false); @@ -34,7 +38,7 @@ export default function SaveNewDialog({ dialogOpen, setDialogOpen }) { setStrategyName(value); }; - const nameExists = !customPresets.every((preset) => preset.name !== value) + const nameExists = !customStrategies.every((preset) => preset.name !== value) || Object.keys(PRESETS).includes(value); if (nameExists) { @@ -47,7 +51,12 @@ export default function SaveNewDialog({ dialogOpen, setDialogOpen }) { const handleSave = () => { if (strategyName) { const nameWithoutExtraSpaces = strategyName.trim().replace(/ +/g, ' '); - saveNewCustomPreset(nameWithoutExtraSpaces); + // saveNewCustomPreset(nameWithoutExtraSpaces); + dispatch(addCustomStrategy({ + name: nameWithoutExtraSpaces, + legs: [...selectedStrategyLegs] + })); + setDialogOpen(false); setStrategyName(""); toggleToast({ diff --git a/src/components/Configure/index.jsx b/src/components/Configure/index.jsx index 39ed3c7..e6d035a 100644 --- a/src/components/Configure/index.jsx +++ b/src/components/Configure/index.jsx @@ -1,12 +1,11 @@ import React, { useContext } from 'react' +import { useSelector } from 'react-redux'; import LegsWindow from './LegsWindow'; import Box from '@mui/material/Box'; import Header from './Header'; -import AddLegs from './AddLegs' -import { StrategyContext } from '../../Contexts/StrategyContextProvider'; +import AddLegs from './AddLegs'; const Configure = React.memo(({ darkMode }) => { - const { legs } = useContext(StrategyContext); return ( { mt: "5px", } }> - + {/* */} diff --git a/src/components/Presets/BullCallSpreadSVG.jsx b/src/components/Presets/BullCallSpreadSVG.jsx index 592ecfb..c2737dc 100644 --- a/src/components/Presets/BullCallSpreadSVG.jsx +++ b/src/components/Presets/BullCallSpreadSVG.jsx @@ -1,15 +1,19 @@ -import { useContext } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useSelector } from "react-redux"; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; -import { StrategyContext } from '../../Contexts/StrategyContextProvider'; +import { getSelectedStrategyName } from '../../features/selected/selectedSlice'; const BullCallSpreadSVG = () => { const navigate = useNavigate(); - const { selectedPreset } = useContext(StrategyContext); + const selectedStrategyName = useSelector(getSelectedStrategyName); const isSelected = () => { - return (selectedPreset.name === "Bull Call Spread" && !selectedPreset.custom); + return (selectedStrategyName === "Bull Call Spread"); + }; + + const handleClick = () => { + navigate("#/Bull-Call-Spread"); }; return ( @@ -27,7 +31,7 @@ const BullCallSpreadSVG = () => { justifyContent: "center" } } - onClick={() => navigate("#/Bull-Call-Spread")} + onClick={handleClick} > {/* Background */} diff --git a/src/components/Presets/BullPutSpreadSVG.jsx b/src/components/Presets/BullPutSpreadSVG.jsx index ee2c6cd..92b96c0 100644 --- a/src/components/Presets/BullPutSpreadSVG.jsx +++ b/src/components/Presets/BullPutSpreadSVG.jsx @@ -1,14 +1,18 @@ -import { useContext } from 'react'; import { useNavigate } from 'react-router-dom'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; -import { StrategyContext } from '../../Contexts/StrategyContextProvider'; +import { useSelector } from 'react-redux'; +import { getSelectedStrategyName } from '../../features/selected/selectedSlice'; const BullPutSpreadSVG = () => { const navigate = useNavigate(); - const { selectedPreset } = useContext(StrategyContext); + const selectedStrategyName = useSelector(getSelectedStrategyName); const isSelected = () => { - return (selectedPreset.name === "Bull Put Spread" && !selectedPreset.custom); + return (selectedStrategyName === "Bull Put Spread"); + }; + + const handleClick = () => { + navigate("#/Bull-Put-Spread"); }; return ( @@ -26,7 +30,7 @@ const BullPutSpreadSVG = () => { justifyContent: "center" } } - onClick={() => navigate("#/Bull-Put-Spread")} + onClick={handleClick} > {/* Background */} diff --git a/src/components/Presets/CustomSVG.jsx b/src/components/Presets/CustomSVG.jsx index 2fa688d..1ddf1c9 100644 --- a/src/components/Presets/CustomSVG.jsx +++ b/src/components/Presets/CustomSVG.jsx @@ -1,35 +1,58 @@ import { useContext } from 'react'; import { useNavigate } from 'react-router-dom'; -import { joinWords } from '../../hooks/utils'; +import { useDispatch, useSelector } from 'react-redux'; +import { joinWords } from '../../utils.js'; +import { setSelectedStrategy, getSelectedStrategyName } from '../../features/selected/selectedSlice'; +import { removeCustomStrategy, getCustomStrategies } from '../../features/custom/customSlice'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import IconButton from '@mui/material/IconButton'; -import { StrategyContext } from '../../Contexts/StrategyContextProvider'; -import { CustomPresetsContext } from '../../Contexts/CustomPresetsContextProvider'; import { ToastContext } from '../../Contexts/ToastContextProvider'; import CancelIcon from '@mui/icons-material/Close'; +import { PRESETS } from '../../const/presets'; -const CustomSVG = ({ name, index }) => { +const CustomSVG = ({ strategy, index }) => { + const dispatch = useDispatch(); const navigate = useNavigate(); - const { selectedPreset } = useContext(StrategyContext); - const { deleteCustomPreset } = useContext(CustomPresetsContext); + const selectedStrategyName = useSelector(getSelectedStrategyName); + const customStrategies = useSelector(getCustomStrategies); const { toggleToast } = useContext(ToastContext); const isSelected = () => { - return (selectedPreset.name === name && selectedPreset.custom); + return (selectedStrategyName === strategy.name); }; const handleDeleteClick = (e) => { e.stopPropagation(); - deleteCustomPreset(index); + dispatch(removeCustomStrategy(index)); + + if (index === 0 && isSelected()) { + navigate("#/Bull-Put-Spread"); + dispatch(setSelectedStrategy({ + strategyName: "Bull Put Spread", + strategyLegs: [...PRESETS["Bull Put Spread"]] + })); + } else if (isSelected()) { + const precedingPreset = customStrategies[index-1]; + navigate(`#/${joinWords(precedingPreset.name)}`); + dispatch(setSelectedStrategy({ + strategyName: precedingPreset.name, + strategyLegs: precedingPreset.legs, + })); + }; + toggleToast({ show: true, severity: "error", - message: `Strategy "${name}" deleted.`, + message: `Strategy "${strategy.name}" deleted.`, key: new Date().getTime() }) }; + const handleClick = () => { + navigate(`#/${joinWords(strategy.name)}`); + }; + return ( { justifyContent: "center" } } - onClick={() => navigate(`#/${joinWords(name)}`)} + onClick={handleClick} > @@ -62,7 +85,7 @@ const CustomSVG = ({ name, index }) => {
- {`${name}`} + {`${strategy.name}`}
diff --git a/src/components/Presets/ShortStraddleSVG.jsx b/src/components/Presets/ShortStraddleSVG.jsx index 029b24c..856c90b 100644 --- a/src/components/Presets/ShortStraddleSVG.jsx +++ b/src/components/Presets/ShortStraddleSVG.jsx @@ -1,14 +1,20 @@ -import { useContext } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; -import { StrategyContext } from '../../Contexts/StrategyContextProvider'; +import { getSelectedStrategyName } from '../../features/selected/selectedSlice'; const ShortStraddleSVG = () => { const navigate = useNavigate(); - const { selectedPreset } = useContext(StrategyContext); + const dispatch = useDispatch(); + const selectedStrategyName = useSelector(getSelectedStrategyName); + const isSelected = () => { - return (selectedPreset.name === "Short Straddle" && !selectedPreset.custom) + return (selectedStrategyName === "Short Straddle") + }; + + const handleClick = () => { + navigate("#/Short-Straddle"); }; return ( @@ -26,7 +32,7 @@ const ShortStraddleSVG = () => { justifyContent: "center" } } - onClick={() => navigate("#/Short-Straddle")} + onClick={handleClick} > {/* Background */} diff --git a/src/components/Presets/ShortStrangleSVG.jsx b/src/components/Presets/ShortStrangleSVG.jsx index 1ed4531..aafb05f 100644 --- a/src/components/Presets/ShortStrangleSVG.jsx +++ b/src/components/Presets/ShortStrangleSVG.jsx @@ -1,14 +1,18 @@ -import { useContext } from 'react'; import { useNavigate } from 'react-router-dom'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; -import { StrategyContext } from '../../Contexts/StrategyContextProvider'; +import { useSelector } from 'react-redux'; +import { getSelectedStrategyName } from '../../features/selected/selectedSlice'; const ShortStrangleSVG = () => { const navigate = useNavigate(); - const { selectedPreset } = useContext(StrategyContext); + const selectedStrategyName = useSelector(getSelectedStrategyName); const isSelected = () => { - return (selectedPreset.name === "Short Strangle" && !selectedPreset.custom); + return (selectedStrategyName === "Short Strangle"); + }; + + const handleClick = () => { + navigate("#/Short-Strangle"); }; return ( @@ -26,7 +30,7 @@ const ShortStrangleSVG = () => { justifyContent: "center" } } - onClick={() => navigate("#/Short-Strangle")} + onClick={handleClick} > {/* Background */} diff --git a/src/components/Presets/index.jsx b/src/components/Presets/index.jsx index 3a19319..bb3e8ee 100644 --- a/src/components/Presets/index.jsx +++ b/src/components/Presets/index.jsx @@ -1,7 +1,8 @@ -import { memo, useContext } from 'react'; +import { memo } from 'react'; +import { useSelector } from 'react-redux'; +import { getCustomStrategies } from '../../features/custom/customSlice'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; -import { CustomPresetsContext } from '../../Contexts/CustomPresetsContextProvider'; import IconButton from '@mui/material/IconButton'; import CloseIcon from '@mui/icons-material/Close'; import ShortStraddleSVG from './ShortStraddleSVG' @@ -11,7 +12,7 @@ import BullPutSpreadSVG from './BullPutSpreadSVG'; import CustomSVG from './CustomSVG'; const Presets = memo(({ drawerView, toggleDrawer }) => { - const { customPresets } = useContext(CustomPresetsContext); + const customStrategies = useSelector(getCustomStrategies); return ( @@ -47,9 +48,9 @@ const Presets = memo(({ drawerView, toggleDrawer }) => { - {customPresets.map((customPreset, i) => { + {customStrategies.map((customStrategy, i) => { return ( - + ) })} diff --git a/src/features/custom/customSlice.js b/src/features/custom/customSlice.js new file mode 100644 index 0000000..cac503e --- /dev/null +++ b/src/features/custom/customSlice.js @@ -0,0 +1,34 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + strategies: [] +}; + +const customSlice = createSlice({ + name: 'custom', + initialState, + reducers: { + setCustomStrategies: (state, action) => { + state.strategies = [...action.payload]; + }, + addCustomStrategy: (state, action) => { + state.strategies.push(action.payload); + }, + removeCustomStrategy: (state, action) => { + state.strategies = state.strategies.filter((strategy, i) => i !== action.payload); + }, + updateCustomStrategy: (state, action) => { + const updatedStrategy = action.payload; + const strategyIndex = state.strategies.findIndex((strategy) => strategy.name === updatedStrategy.name); + if (strategyIndex !== -1) { + state.strategies[strategyIndex] = updatedStrategy; + }; + }, + }, +}); + +export const getCustomStrategies = (store) => store.custom.strategies; + +export const { setCustomStrategies, addCustomStrategy, removeCustomStrategy, updateCustomStrategy } = customSlice.actions; + +export default customSlice.reducer; \ No newline at end of file diff --git a/src/features/selected/selectedSlice.js b/src/features/selected/selectedSlice.js new file mode 100644 index 0000000..ae85bc8 --- /dev/null +++ b/src/features/selected/selectedSlice.js @@ -0,0 +1,32 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { PRESETS } from '../../const/presets'; + +const initialState = { + strategyName: "Short Straddle", + strategyLegs: [...PRESETS["Short Straddle"]], +}; + +const selectedSlice = createSlice({ + name: 'selected', + initialState, + reducers: { + setSelectedStrategy: (state, action) => { + state.strategyName = action.payload.strategyName; + state.strategyLegs = action.payload.strategyLegs; + }, + updateStrategyLegs: (state, action) => { + state.strategyLegs = [...action.payload]; + }, + clearSelectedStrategy: (state) => { + return null; + }, + }, +}); + +export const getSelectedStrategyName = (store) => store.selected.strategyName; + +export const getSelectedStrategyLegs = (store) => store.selected.strategyLegs; + +export const { setSelectedStrategy, clearSelectedStrategy, updateStrategyLegs } = selectedSlice.actions; + +export default selectedSlice.reducer; diff --git a/src/hooks/usePathBasedLegs.js b/src/hooks/usePathBasedLegs.js deleted file mode 100644 index 520e118..0000000 --- a/src/hooks/usePathBasedLegs.js +++ /dev/null @@ -1,44 +0,0 @@ -import { useState, useEffect } from "react"; -import { useLocation } from "react-router-dom"; -import { PRESETS } from "../const/presets"; -import { splitWords, getPathName } from "./utils"; - -const getLegs = (defaultPresetName, pathName) => { - if (!pathName) return PRESETS[defaultPresetName]; - - pathName = splitWords(pathName); - const inPresets = Object.keys(PRESETS).includes(pathName); - if (inPresets) { - return PRESETS[pathName]; - }; - - const customPresets = JSON.parse(localStorage.getItem("customPresets")); - if (customPresets) { - for (const preset of customPresets) { - if (preset.name === pathName) { - return preset.legs; - }; - }; - return PRESETS[defaultPresetName]; - }; - return PRESETS[defaultPresetName]; -}; - -const usePathBasedLegs = (defaultPresetName) => { - const location = useLocation(); - const pathName = getPathName(location); - - const [legs, setLegs] = useState(() => { - return getLegs(defaultPresetName); - }); - - useEffect(() => { - if (pathName && pathName !== defaultPresetName) { - setLegs(getLegs(defaultPresetName, pathName)); - }; - }, [pathName]); - - return [legs, setLegs]; -}; - -export default usePathBasedLegs; \ No newline at end of file diff --git a/src/hooks/usePathBasedPresetName.js b/src/hooks/usePathBasedPresetName.js deleted file mode 100644 index eb39cbd..0000000 --- a/src/hooks/usePathBasedPresetName.js +++ /dev/null @@ -1,49 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { useLocation } from "react-router-dom"; -import { getPathName, joinWords } from "./utils"; -import { PRESETS } from "../const/presets"; -import { splitWords } from "./utils"; - -const getSelectedPreset = (defaultPresetName, pathName) => { - if (!pathName) return { name: defaultPresetName, custom: false }; - - pathName = splitWords(pathName); - const inPresets = Object.keys(PRESETS).includes(pathName); - if (inPresets) { - return { name: pathName, custom: false } - }; - - const customPresets = JSON.parse(localStorage.getItem("customPresets")); - if (customPresets) { - for (const preset of customPresets) { - if (preset.name === pathName) { - return { name: pathName, custom: true } - }; - }; - return { name: defaultPresetName, custom: false }; - }; - return { name: defaultPresetName, custom: false }; -}; - -const usePathBasedPresetName = (initialState) => { - const location = useLocation(); - const pathName = getPathName(location); - - const [selectedPreset, setSelectedPreset] = useState(() => getSelectedPreset(initialState.name, pathName)); - - useEffect(() => { - if (window.location.hash) return; - const joinedWords = joinWords(initialState.name); - history.replaceState(selectedPreset.name, document.title, document.URL.split('#')[0] + `#/${joinedWords}`); - }, []); - - useEffect(() => { - if (pathName && pathName !== selectedPreset.name) { - setSelectedPreset(getSelectedPreset(selectedPreset.name, pathName)); - }; - }, [pathName]); - - return [selectedPreset, setSelectedPreset]; -}; - -export default usePathBasedPresetName; \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 55add09..114e0f9 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,19 +1,23 @@ import React from 'react' import ReactDOM from 'react-dom/client' import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { Provider } from "react-redux"; import DarkModeContextProvider from './Contexts/DarkModeContextProvider.jsx' +import store from './store.js'; import App from './App.jsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( - - - - } /> - - - + + + + + } /> + + + + , ); \ No newline at end of file diff --git a/src/store.js b/src/store.js new file mode 100644 index 0000000..56e8205 --- /dev/null +++ b/src/store.js @@ -0,0 +1,55 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { PRESETS } from './const/presets.js'; +import { splitWords, getPathName } from './hooks/utils.js'; +import selectedReducer, { setSelectedStrategy, getSelectedStrategyName } from './features/selected/selectedSlice.js'; +import customReducer, { setCustomStrategies } from './features/custom/customSlice.js'; + +const localStorageMiddleware = (store) => (next) => (action) => { + const result = next(action); + + if (action.type === "selected/fetchMFData/pending" || + action.type === "selected/removeFund") { + + const selected = { + schemeName: store.getState().selected.schemeName, + schemeCode: store.getState().selected.schemeCode, + }; + + localStorage.setItem("selectedFund", JSON.stringify(selected)); + }; + + if (action.type === "theme/switchTheme") { + const theme = { + mode: store.getState().theme.mode + }; + + localStorage.setItem("appTheme", JSON.stringify(theme)); + }; + + if (action.type === "custom/addCustomStrategy" || + "custom/removeCustomStrategy" || + "custom/updateCustomStrategy" + ) { + const strategies = store.getState().custom.strategies + + localStorage.setItem("customStrategies", JSON.stringify(strategies)); + }; + + return result; +}; + +const store = configureStore({ + reducer: { + selected: selectedReducer, + custom: customReducer, + }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(localStorageMiddleware), +}); + +const savedCustomStrategies = JSON.parse(localStorage.getItem("customStrategies")); + +if (savedCustomStrategies.length > 0) { + store.dispatch(setCustomStrategies(savedCustomStrategies)); +}; + +export default store; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..126f085 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,22 @@ +export const joinWords = (inputString) => { + const wordsArray = inputString.split(' '); + const lowercasedWords = wordsArray.map(word => word); + return lowercasedWords.join('-'); +}; + +export const splitWords = (hyphenSeparatedString) => { + hyphenSeparatedString = hyphenSeparatedString; + const lowercaseWords = hyphenSeparatedString.split('-'); + return lowercaseWords.join(' '); +}; + +export const getPathName = (location) => { + let pathName = location.hash.slice(2); + const indexOfSlash = pathName.indexOf('/'); + + if (indexOfSlash !== -1) { + pathName = pathName.slice(0, indexOfSlash); + }; + pathName = splitWords(pathName); + return pathName; +}; \ No newline at end of file