diff --git a/ui/package-lock.json b/ui/package-lock.json index d44b148..57b7539 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,6 +8,7 @@ "name": "ui", "version": "0.1.0", "dependencies": { + "@reduxjs/toolkit": "^2.0.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -16,6 +17,7 @@ "react": "^18.2.0", "react-bootstrap": "^2.9.2", "react-dom": "^18.2.0", + "react-redux": "^9.0.4", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, @@ -3401,6 +3403,38 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.1.tgz", + "integrity": "sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.0", + "redux-thunk": "^3.1.0", + "reselect": "^5.0.1" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "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.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@restart/hooks": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.15.tgz", @@ -4651,6 +4685,11 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, + "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/@types/warning": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", @@ -15140,6 +15179,32 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-redux": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.0.4.tgz", + "integrity": "sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "react-native": ">=0.69", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-native": { + "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", @@ -15290,6 +15355,19 @@ "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==" + }, + "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==", + "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", @@ -15435,6 +15513,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.0.1.tgz", + "integrity": "sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -17378,6 +17461,14 @@ "requires-port": "^1.0.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/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 0760bcf..dea39f4 100644 --- a/ui/package.json +++ b/ui/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@reduxjs/toolkit": "^2.0.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -11,6 +12,7 @@ "react": "^18.2.0", "react-bootstrap": "^2.9.2", "react-dom": "^18.2.0", + "react-redux": "^9.0.4", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, diff --git a/ui/src/App.js b/ui/src/App.js index 3a9d0d8..167d12e 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -8,26 +8,15 @@ import PourTimeField from './components/PourTimeField'; import SystemControls from './components/SystemControls'; import SystemStatusArea from './components/SystemStatusArea'; -// TODO: Replace this with a real system status -// replace "water threshold" with "waterThreshold" -// replace "time left" with "timeLeft" -// add field "updated" -const systemStatus = { - waterThreshold: 1234, - pump: { - running: false, - timeLeft: 0, - }, - updated: new Date(), -}; - function App() { + // TODO: Move this to a redux store const [pourTime, setPourTime] = useState(1000); // TODO: Add a fake countdown timer of timeLeft + const systemStatus = null; // TODO: Remove usage of this variable and use redux store instead return (

Tea System UI

- +
@@ -45,4 +34,4 @@ function App() { ); } -export default App; +export default App; \ No newline at end of file diff --git a/ui/src/components/SystemStatusArea.js b/ui/src/components/SystemStatusArea.js index 1519e39..7b58101 100644 --- a/ui/src/components/SystemStatusArea.js +++ b/ui/src/components/SystemStatusArea.js @@ -1,5 +1,6 @@ import React from 'react'; import { Card } from 'react-bootstrap'; +import { connect } from 'react-redux'; // TODO: Update time since last update every second, // currently it only updates when the status changes @@ -22,7 +23,7 @@ function _systemStatus(status) { ); } -function SystemStatusArea({ status }) { +export function SystemStatusAreaComponent({ status }) { return ( @@ -35,4 +36,8 @@ function SystemStatusArea({ status }) { ); } -export default SystemStatusArea; \ No newline at end of file +export default connect( + state => ({ + status: state.systemStatus + }), [] +)(SystemStatusAreaComponent); \ No newline at end of file diff --git a/ui/src/index.js b/ui/src/index.js index 6a8ce38..6777eb1 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -4,15 +4,19 @@ import 'bootstrap/dist/css/bootstrap.min.css'; // Importing Bootstrap CSS import { NotificationsProvider } from './contexts/NotificationsContext.js'; import { WaterPumpAPIProvider } from './contexts/WaterPumpAPIContext.js'; +// Redux store +import { AppStore } from './store'; import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('root')); root.render( - - - - - + + + + + + + ); diff --git a/ui/src/store/index.js b/ui/src/store/index.js new file mode 100644 index 0000000..7ae8a6f --- /dev/null +++ b/ui/src/store/index.js @@ -0,0 +1,52 @@ +import { configureStore } from "@reduxjs/toolkit"; +import { Provider } from "react-redux"; + +// slices +import { ALL_APP_SLICES } from "./slices"; + +// listeners +// import { eventsListener } from "./listeners"; + +function buildAppStore(preloadedState) { + const slices = ALL_APP_SLICES; + const reducers = {}; + const state = {}; + Object.keys(slices).forEach(key => { + reducers[key] = slices[key].reducer; + const defaultState = slices[key].getInitialState(); + state[key] = defaultState; + + if (key in preloadedState) { + // if default state is an object, merge it with the preloaded state + if (typeof defaultState === 'object') { + const preloadedStateForKey = preloadedState[key] || {}; + state[key] = { ...defaultState, ...preloadedStateForKey }; + } else { // otherwise, just use the preloaded state + state[key] = preloadedState[key]; + } + } + }); + + return { reducers, state }; +} + +// AppStore is a wrapper component that provides the Redux store to the rest of the application. +// preloadedState is an optional parameter that allows you to pass in an initial state for the store. +const AppStore = ({ children, preloadedState = {}, returnStore = false }) => { + const { reducers, state } = buildAppStore(preloadedState); + const store = configureStore({ + reducer: reducers, + preloadedState: state, + // middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(eventsListener.middleware), + }); + const provider = ( + + {children} + + ); + + if (returnStore) return { store, provider }; + return provider; +}; + +export { AppStore }; \ No newline at end of file diff --git a/ui/src/store/slices/SystemStatus.js b/ui/src/store/slices/SystemStatus.js new file mode 100644 index 0000000..78eb583 --- /dev/null +++ b/ui/src/store/slices/SystemStatus.js @@ -0,0 +1,28 @@ +import { createSlice } from '@reduxjs/toolkit'; + +// TODO: Replace this with a real system status +// replace "water threshold" with "waterThreshold" +// replace "time left" with "timeLeft" +// add field "updated" +const systemStatus = { + waterThreshold: 1234, + pump: { + running: false, + timeLeft: 0, + }, + updated: new Date(), +}; + +// slice for system status +export const SystemStatusSlice = createSlice({ + name: 'systemStatus', + initialState: systemStatus, + reducers: { + updateSystemStatus(state, action) { + return action.payload; + }, + }, +}); + +export const actions = SystemStatusSlice.actions; +export const { updateSystemStatus } = actions; \ No newline at end of file diff --git a/ui/src/store/slices/index.js b/ui/src/store/slices/index.js new file mode 100644 index 0000000..65ffe9a --- /dev/null +++ b/ui/src/store/slices/index.js @@ -0,0 +1,8 @@ +import { SystemStatusSlice } from "./SystemStatus"; + +const slices = [ SystemStatusSlice ]; +// export all slices as an object { [sliceName]: slice } +export const ALL_APP_SLICES = slices.reduce((acc, slice) => { + acc[slice.name] = slice; + return acc; +}, {}); \ No newline at end of file