├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── assets │ ├── auth-illustration.svg │ ├── imgs │ │ └── bg │ │ │ └── error-404.png │ ├── logo.svg │ └── logoIcon.svg ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── favicon1.ico ├── index.html ├── manifest.json └── robots.txt └── src ├── App.js ├── actions ├── auth.js ├── group.js └── metrics.js ├── assets └── svgs │ ├── heart-beat-svgrepo-com.svg │ ├── logo.svg │ ├── logoIcon.svg │ └── pulse-line-svgrepo-com.svg ├── components ├── BootstrapDialog.js ├── ConfirmModal │ └── index.js ├── Icons.js ├── chart.js ├── loader │ ├── CirclesLoader.js │ └── CirclesLoader.scss ├── logo.js ├── routing │ ├── MainRoutes.js │ └── PrivateRoute.js ├── scrollbar.js └── severity-pill.js ├── hooks ├── use-nprogress.js ├── use-popover.js └── use-selection.js ├── index.js ├── layouts ├── auth │ └── layout.js └── dashboard │ ├── account-popover.js │ ├── config.js │ ├── layout.js │ ├── side-nav-item.js │ ├── side-nav.js │ └── top-nav.js ├── pages ├── Dashboard │ ├── BloodPressureChart.js │ ├── Dashboard.js │ ├── EveryTimeChart.js │ ├── GroupChart.js │ ├── OneLineChart.js │ └── TableForTextMetrics.js ├── Metrics │ └── Metrics.js ├── MetricsGroup │ └── MetricsGroup.js ├── NotFound.js ├── TotalMetricsValues │ └── TotalMetricsValues.js ├── Track │ ├── EditTrackDialog.js │ ├── EditTrackDialogForBlood.js │ └── Track.js └── auth │ ├── Login.js │ └── Register.js ├── reportWebVitals.js ├── sections ├── dashboard │ ├── to-do-card.js │ ├── to-do-dialog-for-bloodPressure.js │ └── to-do-dialog.js ├── group │ ├── groupCard.js │ └── metricCard.js ├── metrics │ ├── metrics-dialog-title.js │ ├── metrics-dialog.js │ ├── metrics-search.js │ └── metrics-table.js └── overview │ └── overview-latest-metrics-value.js ├── setupTests.js ├── store ├── authSlice.js ├── groupSlice.js ├── index.js └── metricsSlice.js ├── styles └── index.scss ├── theme ├── colors.js ├── create-components.js ├── create-palette.js ├── create-shadows.js ├── create-typography.js └── index.js └── utils ├── api.js ├── index.js ├── setAuthToken.js └── toast.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | 72 | ### -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smart-metrics-logbook", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.10.6", 7 | "@emotion/styled": "^11.10.6", 8 | "@fontsource/roboto": "^4.5.8", 9 | "@heroicons/react": "^2.0.16", 10 | "@mui/icons-material": "^5.11.11", 11 | "@mui/lab": "^5.0.0-alpha.125", 12 | "@mui/material": "^5.11.12", 13 | "@mui/styles": "^5.11.13", 14 | "@mui/system": "^5.11.12", 15 | "@mui/x-date-pickers": "^6.0.0-beta.5", 16 | "@reduxjs/toolkit": "^1.9.3", 17 | "@testing-library/jest-dom": "^5.16.5", 18 | "@testing-library/react": "^13.4.0", 19 | "@testing-library/user-event": "^13.5.0", 20 | "apexcharts": "^3.37.1", 21 | "axios": "^1.3.4", 22 | "clsx": "^1.2.1", 23 | "date-fns": "^2.29.3", 24 | "dayjs": "^1.11.7", 25 | "formik": "^2.2.9", 26 | "moment": "^2.29.4", 27 | "moment-timezone": "^0.5.43", 28 | "notistack": "^3.0.1", 29 | "nprogress": "^0.2.0", 30 | "prop-types": "^15.8.1", 31 | "react": "^18.2.0", 32 | "react-apexcharts": "^1.4.0", 33 | "react-beautiful-dnd": "^13.1.1", 34 | "react-chartjs-2": "^5.2.0", 35 | "react-dom": "^18.2.0", 36 | "react-feather": "^2.0.10", 37 | "react-helmet": "^6.1.0", 38 | "react-redux": "^8.0.5", 39 | "react-router-dom": "^6.8.2", 40 | "react-scripts": "5.0.1", 41 | "react-toastify": "^9.1.1", 42 | "recharts": "^2.5.0", 43 | "redux-devtools-extension": "^2.13.9", 44 | "redux-thunk": "^2.4.2", 45 | "sass": "^1.58.3", 46 | "simplebar-react": "^3.2.1", 47 | "web-vitals": "^2.1.4", 48 | "yup": "^1.0.2" 49 | }, 50 | "scripts": { 51 | "start": "set port=3040 && react-scripts start", 52 | "build": "react-scripts build", 53 | "test": "react-scripts test", 54 | "eject": "react-scripts eject" 55 | }, 56 | "eslintConfig": { 57 | "extends": [ 58 | "react-app", 59 | "react-app/jest" 60 | ] 61 | }, 62 | "browserslist": { 63 | "production": [ 64 | ">0.2%", 65 | "not dead", 66 | "not op_mini all" 67 | ], 68 | "development": [ 69 | "last 1 chrome version", 70 | "last 1 firefox version", 71 | "last 1 safari version" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /public/assets/imgs/bg/error-404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-miro26/Metrics-health-for-react-frontend/3b768129b02faf5ae94a965fa0e811bcedbd3646/public/assets/imgs/bg/error-404.png -------------------------------------------------------------------------------- /public/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/logoIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | Created with Fabric.js 3.5.0 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-miro26/Metrics-health-for-react-frontend/3b768129b02faf5ae94a965fa0e811bcedbd3646/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-miro26/Metrics-health-for-react-frontend/3b768129b02faf5ae94a965fa0e811bcedbd3646/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-miro26/Metrics-health-for-react-frontend/3b768129b02faf5ae94a965fa0e811bcedbd3646/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon1.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-miro26/Metrics-health-for-react-frontend/3b768129b02faf5ae94a965fa0e811bcedbd3646/public/favicon1.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 24 | 28 | 29 | 30 | Smart Metrcis Logbook 31 | 32 | 33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Smart Metrics Logbook", 3 | "name": "Smart Metrics Logbook", 4 | "icons": [ 5 | { 6 | "src": "./favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router } from "react-router-dom"; 3 | import { ToastContainer } from "react-toastify"; 4 | import { createTheme } from "./theme"; 5 | import { CssBaseline } from "@mui/material"; 6 | 7 | import MainRoutes from "./components/routing/MainRoutes"; 8 | import { ThemeProvider } from "@mui/material/styles"; 9 | import "react-toastify/dist/ReactToastify.css"; 10 | import "simplebar-react/dist/simplebar.min.css"; 11 | 12 | import "./styles/index.scss"; 13 | 14 | const App = () => { 15 | const theme = createTheme(); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | userAuthError, 3 | userLogined, 4 | userLoginSuccess, 5 | userLogOut, 6 | userRegister, 7 | } from "../store/authSlice"; 8 | import { api, setAuthToken, toast } from "../utils"; 9 | // Load User 10 | export const apiLoadUser = (navigate) => async (dispatch) => { 11 | try { 12 | const res = await api.get("auth/loadUser"); 13 | 14 | await dispatch(userLogined(res.data.doc)); 15 | } catch (err) { 16 | navigate("/auth/login"); 17 | const errors = err?.response?.data?.errors; 18 | if (errors) { 19 | errors.forEach((error) => console.log(error.msg)); 20 | } 21 | await dispatch(userAuthError()); 22 | } 23 | }; 24 | 25 | // Register User 26 | export const apiRegister = (formData) => async (dispatch) => { 27 | try { 28 | const res = await api.post("auth/register", formData); 29 | 30 | dispatch(userRegister(res.data.doc)); 31 | setAuthToken(res.data.doc); 32 | toast.success("You are successfully registered!"); 33 | dispatch(apiLoadUser()); 34 | } catch (err) { 35 | const errors = err?.response?.data?.errors; 36 | if (errors) { 37 | errors.forEach((error) => toast.error(error.msg)); 38 | } 39 | 40 | } 41 | }; 42 | 43 | // Login User 44 | export const apiLogin = (formData) => async (dispatch) => { 45 | try { 46 | const res = await api.post("auth/login", formData); 47 | 48 | dispatch(userLoginSuccess(res.data.doc)); 49 | 50 | dispatch(apiLoadUser()); 51 | } catch (err) { 52 | const errors = err?.response?.data?.errors; 53 | 54 | if (errors) { 55 | errors.forEach((error) => toast.error(error.msg)); 56 | } 57 | } 58 | }; 59 | 60 | // Logout 61 | export const apiLogout = () => userLogOut(); 62 | -------------------------------------------------------------------------------- /src/actions/group.js: -------------------------------------------------------------------------------- 1 | import { 2 | addGroup, 3 | deleteGroup, 4 | getGroups, 5 | groupError, 6 | updateGroup, 7 | } from "../store/groupSlice"; 8 | import { api, toast } from "../utils"; 9 | 10 | export const apiGetGroupsByUserId = () => async (dispatch) => { 11 | try { 12 | const res = await api.get("group"); 13 | 14 | return dispatch(getGroups(res.data.docs)); 15 | } catch (err) { 16 | const errors = err?.response?.data?.errors; 17 | if (errors) { 18 | errors.forEach((error) => toast.error(error.msg)); 19 | return dispatch(groupError()); 20 | } 21 | } 22 | }; 23 | 24 | export const apiAddGroup = (formData) => async (dispatch) => { 25 | try { 26 | const res = await api.post("group", formData); 27 | 28 | toast.success("New Group is added!"); 29 | return dispatch(addGroup(res.data.doc)); 30 | } catch (err) { 31 | console.log(err); 32 | const errors = err?.response.data?.errors; 33 | if (errors) { 34 | errors.forEach((error) => toast.error(error.msg)); 35 | return dispatch(groupError()); 36 | } 37 | } 38 | }; 39 | export const apiUpdateGroup = (formData) => async (dispatch) => { 40 | console.log(formData); 41 | try { 42 | const res = await api.put("group", formData); 43 | 44 | toast.success(" Group is updated!"); 45 | return dispatch(updateGroup(res.data.doc)); 46 | } catch (err) { 47 | const errors = err?.response.data?.errors; 48 | if (errors) { 49 | errors.forEach((error) => error && toast.error(error.msg)); 50 | return dispatch(groupError()); 51 | } 52 | } 53 | }; 54 | 55 | export const apiDeleteGroupById = (_id) => async (dispatch) => { 56 | try { 57 | await api.delete(`group?_id=${_id}`); 58 | toast.success("The Group has been successfully deleted."); 59 | return dispatch(deleteGroup({ _id: _id })); 60 | } catch (err) { 61 | const errors = err?.response.data?.errors; 62 | if (errors) { 63 | errors.forEach((error) => toast.error(error.msg)); 64 | } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/actions/metrics.js: -------------------------------------------------------------------------------- 1 | import { 2 | addMetric, 3 | metricsError, 4 | getUserMetrics, 5 | deleteMetric, 6 | updateMetric, 7 | addMetricWage, 8 | deleteMetricWage, 9 | getMetricsAllWages, 10 | getMetricsTodayWages, 11 | updateMetricsWage, 12 | getMetricsLastestWages, 13 | } from "../store/metricsSlice"; 14 | import { api, toast } from "../utils"; 15 | 16 | export const apiAddMetric = (formData) => async (dispatch) => { 17 | console.log("@@@", formData); 18 | 19 | try { 20 | const res = await api.post("metrics", formData); 21 | 22 | toast.success("New metrics is added!"); 23 | return dispatch(addMetric(res.data.doc)); 24 | } catch (err) { 25 | const errors = err?.response?.data?.errors; 26 | if (errors) { 27 | errors.forEach((error) => toast.error(error.msg)); 28 | return dispatch(metricsError()); 29 | } 30 | } 31 | }; 32 | export const apiUpdateMetric = (formData) => async (dispatch) => { 33 | console.log(formData); 34 | try { 35 | const res = await api.put("metrics", formData); 36 | 37 | toast.success(" Metrics is updated!"); 38 | return dispatch(updateMetric(res.data.doc)); 39 | } catch (err) { 40 | const errors = err?.response?.data?.errors; 41 | if (errors) { 42 | errors.forEach((error) => toast.error(error.msg)); 43 | return dispatch(metricsError()); 44 | } 45 | } 46 | }; 47 | export const apiGetMetricsByUserId = () => async (dispatch) => { 48 | try { 49 | const res = await api.get("metrics"); 50 | 51 | return dispatch(getUserMetrics(res.data.docs)); 52 | } catch (err) { 53 | const errors = err?.response?.data?.errors; 54 | if (errors) { 55 | errors.forEach((error) => toast.error(error.msg)); 56 | return dispatch(metricsError()); 57 | } 58 | } 59 | }; 60 | 61 | export const apiDeleteMetricById = (_id) => async (dispatch) => { 62 | try { 63 | await api.delete(`metrics?_id=${_id}`); 64 | toast.success("The metric has been successfully deleted."); 65 | return dispatch(deleteMetric({ _id: _id })); 66 | } catch (err) { 67 | const errors = err?.response?.data?.errors; 68 | if (errors) { 69 | errors.forEach((error) => toast.error(error.msg)); 70 | // return dispatch(metricsError()); 71 | } 72 | } 73 | }; 74 | 75 | export const apiAddMetricWage = (wage) => async (dispatch) => { 76 | try { 77 | const res = await api.post("metrics/wage", wage); 78 | 79 | toast.success(" Metrics Value is added!"); 80 | // dispatch(apiGetMetricsLastestWagesByUserId()); 81 | return dispatch(addMetricWage(res.data.doc)); 82 | } catch (err) { 83 | const errors = err?.response?.data?.errors; 84 | if (errors) { 85 | errors.forEach((error) => toast.error(error.msg)); 86 | return dispatch(metricsError()); 87 | } 88 | } 89 | }; 90 | 91 | export const apiDeleteMetricWageById = (_id) => async (dispatch) => { 92 | try { 93 | await api.delete(`metrics/wage?_id=${_id}`); 94 | await dispatch(apiGetMetricsLastestWagesByUserId()); 95 | toast.success("The metric value has been successfully deleted."); 96 | return dispatch(deleteMetricWage({ _id: _id })); 97 | } catch (err) { 98 | const errors = err?.response?.data?.errors; 99 | if (errors) { 100 | errors.forEach((error) => toast.error(error.msg)); 101 | } 102 | } 103 | }; 104 | 105 | export const apiUpdateMetricWage = (formData) => async (dispatch) => { 106 | try { 107 | const res = await api.put("metrics/wage", formData); 108 | await dispatch(apiGetMetricsLastestWagesByUserId()); 109 | console.log(res.data.doc); 110 | toast.success(" Metrics is updated!"); 111 | return dispatch(updateMetricsWage(res.data.doc)); 112 | } catch (err) { 113 | const errors = err?.response?.data?.errors; 114 | if (errors) { 115 | errors.forEach((error) => toast.error(error.msg)); 116 | return dispatch(metricsError()); 117 | } 118 | } 119 | }; 120 | export const apiGetMetricsAllWagesByUserId = () => async (dispatch) => { 121 | try { 122 | const res = await api.get("metrics/wage"); 123 | 124 | return dispatch(getMetricsAllWages(res.data.docs)); 125 | } catch (err) { 126 | const errors = err?.response?.data?.errors; 127 | if (errors) { 128 | errors.forEach((error) => toast.error(error.msg)); 129 | return dispatch(metricsError()); 130 | } 131 | } 132 | }; 133 | export const apiGetMetricsTodayWagesByUserId = () => async (dispatch) => { 134 | try { 135 | const res = await api.get("metrics/wage/today"); 136 | return dispatch(getMetricsTodayWages(res.data.docs)); 137 | } catch (err) { 138 | const errors = err?.response?.data?.errors; 139 | if (errors) { 140 | errors.forEach((error) => toast.error(error.msg)); 141 | return dispatch(metricsError()); 142 | } 143 | } 144 | }; 145 | export const apiGetMetricsLastestWagesByUserId = () => async (dispatch) => { 146 | try { 147 | const res = await api.get("metrics/wage/lastest"); 148 | 149 | return dispatch(getMetricsLastestWages(res.data.docs)); 150 | } catch (err) { 151 | const errors = err?.response?.data?.errors; 152 | if (errors) { 153 | errors.forEach((error) => toast.error(error.msg)); 154 | return dispatch(metricsError()); 155 | } 156 | } 157 | }; 158 | -------------------------------------------------------------------------------- /src/assets/svgs/heart-beat-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 17 | 22 | 25 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/assets/svgs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 26 | 27 | 28 | 29 | 31 | 33 | 35 | 36 | 37 | 38 | 40 | 42 | 44 | 46 | 47 | 49 | 51 | 52 | 54 | 56 | 58 | 60 | 61 | 63 | 65 | 67 | 69 | 71 | 73 | 75 | 77 | 79 | 81 | 83 | 85 | 87 | 89 | 91 | 93 | 95 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/assets/svgs/logoIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 26 | 27 | 28 | 29 | 31 | 33 | 35 | 36 | 37 | 38 | 45 | 56 | 73 | 75 | 77 | 79 | 81 | 83 | 85 | 87 | 89 | 91 | 93 | 95 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/assets/svgs/pulse-line-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/BootstrapDialog.js: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | import Dialog from "@mui/material/Dialog"; 3 | 4 | export const BootstrapDialog = styled(Dialog)(({ theme }) => ({ 5 | "& .MuiDialogContent-root": { 6 | padding: theme.spacing(3), 7 | }, 8 | "& .MuiDialogActions-root": { 9 | padding: theme.spacing(1), 10 | }, 11 | })); 12 | -------------------------------------------------------------------------------- /src/components/ConfirmModal/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogContentText, 7 | DialogActions, 8 | Button, 9 | } from "@mui/material"; 10 | 11 | const ConfirmDialog = ({ openDialog, onCancel, onOK, title, content }) => { 12 | return ( 13 | 20 | {title} 21 | 22 | {content} 23 | 24 | 25 | 28 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default ConfirmDialog; 37 | -------------------------------------------------------------------------------- /src/components/Icons.js: -------------------------------------------------------------------------------- 1 | import { ReactComponent as Heart } from "../assets/svgs/heart-beat-svgrepo-com.svg"; 2 | import { ReactComponent as BP } from "../assets/svgs/pulse-line-svgrepo-com.svg"; 3 | 4 | export { Heart as IconHeart, BP as IconBP }; 5 | -------------------------------------------------------------------------------- /src/components/chart.js: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from "react"; 2 | import { styled } from "@mui/material/styles"; 3 | 4 | const ApexChart = lazy(() => import("react-apexcharts"), { 5 | ssr: false, 6 | loading: () => null, 7 | }); 8 | 9 | export const Chart = styled(ApexChart); 10 | -------------------------------------------------------------------------------- /src/components/loader/CirclesLoader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Typography } from "@mui/material"; 3 | 4 | import "./CirclesLoader.scss"; 5 | 6 | function CirclesLoader() { 7 | return ( 8 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | Loading ... 27 | 28 |
29 | ); 30 | } 31 | 32 | export default CirclesLoader; 33 | -------------------------------------------------------------------------------- /src/components/loader/CirclesLoader.scss: -------------------------------------------------------------------------------- 1 | .circles-loader { 2 | display: inline-block; 3 | position: relative; 4 | width: 80px; 5 | height: 80px; 6 | z-index: 1000; 7 | } 8 | .circles-loader div { 9 | animation: circles-loader 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 10 | transform-origin: 40px 40px; 11 | } 12 | .circles-loader div:after { 13 | content: " "; 14 | display: block; 15 | position: absolute; 16 | width: 7px; 17 | height: 7px; 18 | border-radius: 50%; 19 | background: #797979; 20 | margin: -4px 0 0 -4px; 21 | } 22 | .circles-loader div:nth-child(1) { 23 | animation-delay: -0.036s; 24 | } 25 | .circles-loader div:nth-child(1):after { 26 | top: 63px; 27 | left: 63px; 28 | } 29 | .circles-loader div:nth-child(2) { 30 | animation-delay: -0.072s; 31 | } 32 | .circles-loader div:nth-child(2):after { 33 | top: 68px; 34 | left: 56px; 35 | } 36 | .circles-loader div:nth-child(3) { 37 | animation-delay: -0.108s; 38 | } 39 | .circles-loader div:nth-child(3):after { 40 | top: 71px; 41 | left: 48px; 42 | } 43 | .circles-loader div:nth-child(4) { 44 | animation-delay: -0.144s; 45 | } 46 | .circles-loader div:nth-child(4):after { 47 | top: 72px; 48 | left: 40px; 49 | } 50 | .circles-loader div:nth-child(5) { 51 | animation-delay: -0.18s; 52 | } 53 | .circles-loader div:nth-child(5):after { 54 | top: 71px; 55 | left: 32px; 56 | } 57 | .circles-loader div:nth-child(6) { 58 | animation-delay: -0.216s; 59 | } 60 | .circles-loader div:nth-child(6):after { 61 | top: 68px; 62 | left: 24px; 63 | } 64 | .circles-loader div:nth-child(7) { 65 | animation-delay: -0.252s; 66 | } 67 | .circles-loader div:nth-child(7):after { 68 | top: 63px; 69 | left: 17px; 70 | } 71 | .circles-loader div:nth-child(8) { 72 | animation-delay: -0.288s; 73 | } 74 | .circles-loader div:nth-child(8):after { 75 | top: 56px; 76 | left: 12px; 77 | } 78 | @keyframes circles-loader { 79 | 0% { 80 | transform: rotate(0deg); 81 | } 82 | 100% { 83 | transform: rotate(360deg); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/components/logo.js: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@mui/material/styles'; 2 | 3 | export const Logo = () => { 4 | const theme = useTheme(); 5 | const fillColor = theme.palette.primary.main; 6 | 7 | return ( 8 | 15 | 20 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/routing/MainRoutes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Routes, useNavigate } from "react-router-dom"; 3 | import PrivateRoute from "./PrivateRoute"; 4 | import Login from "../../pages/auth/Login.js"; 5 | import Register from "../../pages/auth/Register.js"; 6 | import Dashboard from "../../pages/Dashboard/Dashboard"; 7 | import Metrics from "../../pages/Metrics/Metrics"; 8 | import NotFound from "../../pages/NotFound.js"; 9 | import MetricsGroup from "../../pages/MetricsGroup/MetricsGroup"; 10 | import TotalMetricsValues from "../../pages/TotalMetricsValues/TotalMetricsValues"; 11 | import Track from "../../pages/Track/Track"; 12 | import { setAuthToken } from "../../utils"; 13 | import store from "../../store"; 14 | import { apiLoadUser } from "../../actions/auth"; 15 | import { userLogOut } from "../../store/authSlice"; 16 | const MainRoutes = () => { 17 | const navigate = useNavigate(); 18 | React.useEffect(() => { 19 | if (localStorage.getItem("smart-metrics-logbook")) { 20 | setAuthToken(localStorage.getItem("smart-metrics-logbook")); 21 | store.dispatch(apiLoadUser(navigate)); 22 | } else { 23 | store.dispatch(userLogOut()); 24 | navigate("api/login"); 25 | localStorage.removeItem("smart-metrics-logbook"); 26 | } 27 | window.addEventListener("storage", () => { }); 28 | // eslint-disable-next-line react-hooks/exhaustive-deps 29 | }, []); 30 | return ( 31 | 32 | } /> 33 | }> 34 | }> 35 | } /> 36 | } /> 37 | } 40 | /> 41 | } 44 | /> 45 | } /> 46 | 47 | ); 48 | }; 49 | 50 | export default MainRoutes; 51 | -------------------------------------------------------------------------------- /src/components/routing/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Navigate } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | import { connect, useSelector } from "react-redux"; 5 | import CirclesLoader from "../loader/CirclesLoader"; 6 | 7 | const PrivateRoute = ({ component: Component }) => { 8 | const isAuthenticated = useSelector((state) => state.auth.isAuthenticated); 9 | const loading = useSelector((state) => state.auth.loading); 10 | if (loading) return ; 11 | if (isAuthenticated) return ; 12 | 13 | return ; 14 | }; 15 | 16 | PrivateRoute.propTypes = { 17 | auth: PropTypes.object.isRequired, 18 | }; 19 | 20 | const mapStateToProps = (state) => ({ 21 | auth: state.auth, 22 | }); 23 | 24 | export default connect(mapStateToProps)(PrivateRoute); 25 | -------------------------------------------------------------------------------- /src/components/scrollbar.js: -------------------------------------------------------------------------------- 1 | import SimpleBar from 'simplebar-react'; 2 | import { styled } from '@mui/material/styles'; 3 | 4 | export const Scrollbar = styled(SimpleBar)``; 5 | -------------------------------------------------------------------------------- /src/components/severity-pill.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { styled } from '@mui/material/styles'; 3 | 4 | const SeverityPillRoot = styled('span')(({ theme, ownerState }) => { 5 | const backgroundColor = theme.palette[ownerState.color].alpha12; 6 | const color = theme.palette.mode === 'dark' 7 | ? theme.palette[ownerState.color].main 8 | : theme.palette[ownerState.color].dark; 9 | 10 | return { 11 | alignItems: 'center', 12 | backgroundColor, 13 | borderRadius: 12, 14 | color, 15 | cursor: 'default', 16 | display: 'inline-flex', 17 | flexGrow: 0, 18 | flexShrink: 0, 19 | fontFamily: theme.typography.fontFamily, 20 | fontSize: theme.typography.pxToRem(12), 21 | lineHeight: 2, 22 | fontWeight: 600, 23 | justifyContent: 'center', 24 | letterSpacing: 0.5, 25 | minWidth: 20, 26 | paddingLeft: theme.spacing(1), 27 | paddingRight: theme.spacing(1), 28 | textTransform: 'uppercase', 29 | whiteSpace: 'nowrap' 30 | }; 31 | }); 32 | 33 | export const SeverityPill = (props) => { 34 | const { color = 'primary', children, ...other } = props; 35 | 36 | const ownerState = { color }; 37 | 38 | return ( 39 | 43 | {children} 44 | 45 | ); 46 | }; 47 | 48 | SeverityPill.propTypes = { 49 | children: PropTypes.node, 50 | color: PropTypes.oneOf([ 51 | 'primary', 52 | 'secondary', 53 | 'error', 54 | 'info', 55 | 'warning', 56 | 'success' 57 | ]) 58 | }; 59 | -------------------------------------------------------------------------------- /src/hooks/use-nprogress.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import nprogress from "nprogress"; 3 | import { useNavigate } from "react-router-dom"; 4 | 5 | export function useNProgress() { 6 | // const navigate = useNavigate(); 7 | // useEffect(() => { 8 | // const startProgressBar = () => { 9 | // nprogress.start(); 10 | // }; 11 | // const stopProgressBar = () => { 12 | // nprogress.done(); 13 | // }; 14 | // navigate.listen(startProgressBar); 15 | // return () => { 16 | // navigate.listen(stopProgressBar); 17 | // }; 18 | // }, [navigate]); 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/use-popover.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef, useState } from "react"; 2 | 3 | export function usePopover() { 4 | const anchorRef = useRef(null); 5 | const [open, setOpen] = useState(false); 6 | 7 | const handleOpen = useCallback(() => { 8 | setOpen(true); 9 | }, []); 10 | 11 | const handleClose = useCallback(() => { 12 | setOpen(false); 13 | }, []); 14 | 15 | const handleToggle = useCallback(() => { 16 | setOpen((prevState) => !prevState); 17 | }, []); 18 | 19 | return { 20 | anchorRef, 21 | handleClose, 22 | handleOpen, 23 | handleToggle, 24 | open, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/use-selection.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | export const useSelection = (items = []) => { 4 | const [selected, setSelected] = useState([]); 5 | 6 | useEffect(() => { 7 | setSelected([]); 8 | }, [items]); 9 | 10 | const handleSelectAll = useCallback(() => { 11 | setSelected([...items]); 12 | }, [items]); 13 | 14 | const handleSelectOne = useCallback((item) => { 15 | setSelected((prevState) => [...prevState, item]); 16 | }, []); 17 | 18 | const handleDeselectAll = useCallback(() => { 19 | setSelected([]); 20 | }, []); 21 | 22 | const handleDeselectOne = useCallback((item) => { 23 | setSelected((prevState) => { 24 | return prevState?.filter((_item) => _item !== item); 25 | }); 26 | }, []); 27 | 28 | return { 29 | handleDeselectAll, 30 | handleDeselectOne, 31 | handleSelectAll, 32 | handleSelectOne, 33 | selected, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.js"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | import { Provider } from "react-redux"; 6 | 7 | import store from "./store/index"; 8 | 9 | const root = ReactDOM.createRoot(document.getElementById("root")); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /src/layouts/auth/layout.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { Box, Typography, Unstable_Grid2 as Grid } from "@mui/material"; 3 | 4 | 5 | export const Layout = (props) => { 6 | const { children } = props; 7 | 8 | return ( 9 | 16 | 17 | 27 | 37 | 45 | Go to pro 46 | 47 | 48 | {children} 49 | 50 | 65 | 66 | 76 | Welcome to{" "} 77 | 78 | Smart Metrics Logbook 79 | 80 | 81 | 82 | You can track your health status and exercise metrics at any time 83 | here. 84 | 85 | 86 | 87 | 88 | 89 | 90 | ); 91 | }; 92 | 93 | Layout.prototypes = { 94 | children: PropTypes.node, 95 | }; 96 | -------------------------------------------------------------------------------- /src/layouts/dashboard/account-popover.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { Navigate } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | import { 5 | Box, 6 | Divider, 7 | MenuItem, 8 | MenuList, 9 | Popover, 10 | Typography, 11 | } from "@mui/material"; 12 | import { useSelector } from "react-redux"; 13 | 14 | export const AccountPopover = (props) => { 15 | const { anchorEl, onClose, open } = props; 16 | const auth = useSelector((state) => state.auth.user); 17 | 18 | const handleSignOut = useCallback(() => { 19 | onClose?.(); 20 | props.onAction(); 21 | return ; 22 | 23 | // eslint-disable-next-line 24 | }, []); 25 | 26 | return ( 27 | 37 | 43 | Account 44 | 45 | {auth?.name} 46 | 47 | 48 | 49 | *": { 55 | borderRadius: 1, 56 | }, 57 | }} 58 | > 59 | { 61 | handleSignOut(); 62 | localStorage.removeItem("smart-metrics-logbook"); 63 | }} 64 | > 65 | Sign out 66 | 67 | 68 | 69 | ); 70 | }; 71 | 72 | AccountPopover.propTypes = { 73 | anchorEl: PropTypes.any, 74 | onClose: PropTypes.func, 75 | open: PropTypes.bool.isRequired, 76 | }; 77 | -------------------------------------------------------------------------------- /src/layouts/dashboard/config.js: -------------------------------------------------------------------------------- 1 | import ChartBarIcon from "@heroicons/react/24/solid/ChartBarIcon"; 2 | 3 | import ViewListIcon from "@mui/icons-material/ViewList"; 4 | import { SvgIcon } from "@mui/material"; 5 | import DnsIcon from "@mui/icons-material/Dns"; 6 | export const items = [ 7 | { 8 | title: "Dashboard", 9 | path: "/", 10 | icon: ( 11 | 12 | 13 | 14 | ), 15 | }, 16 | { 17 | title: "Metrics", 18 | path: "/metrics", 19 | icon: ( 20 | 21 | 22 | 23 | ), 24 | }, 25 | { 26 | title: "Metrics Group", 27 | path: "/group", 28 | icon: ( 29 | 30 | 31 | 32 | ), 33 | }, 34 | 35 | ]; 36 | -------------------------------------------------------------------------------- /src/layouts/dashboard/layout.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { styled } from "@mui/material/styles"; 3 | import { SideNav } from "./side-nav"; 4 | import { TopNav } from "./top-nav"; 5 | import { useLocation } from "react-router-dom"; 6 | 7 | const SIDE_NAV_WIDTH = 280; 8 | 9 | const LayoutRoot = styled("div")(({ theme }) => ({ 10 | display: "flex", 11 | flex: "1 1 auto", 12 | maxWidth: "100%", 13 | background: "#fbfbfb", 14 | [theme.breakpoints.up("lg")]: { 15 | paddingLeft: SIDE_NAV_WIDTH, 16 | }, 17 | })); 18 | 19 | const LayoutContainer = styled("div")({ 20 | display: "flex", 21 | flex: "1 1 auto", 22 | flexDirection: "column", 23 | width: "100%", 24 | }); 25 | 26 | export const Layout = (props) => { 27 | const { children } = props; 28 | const location = useLocation(); 29 | const pathname = location.pathname; 30 | const [openNav, setOpenNav] = useState(true); 31 | 32 | useEffect(() => { 33 | if (openNav) { 34 | setOpenNav(true); 35 | } 36 | }, [pathname, openNav]); 37 | 38 | return ( 39 | <> 40 | setOpenNav(true)} onAction={props.onAction} /> 41 | setOpenNav(false)} open={openNav} /> 42 | 43 | {children} 44 | 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/layouts/dashboard/side-nav-item.js: -------------------------------------------------------------------------------- 1 | import { Link as RouterLink } from "react-router-dom"; 2 | import PropTypes from "prop-types"; 3 | import { Box, ButtonBase } from "@mui/material"; 4 | 5 | export const SideNavItem = (props) => { 6 | const { active = false, disabled, external, icon, path, title } = props; 7 | 8 | const linkProps = path 9 | ? external 10 | ? { 11 | component: "a", 12 | to: path, 13 | target: "_blank", 14 | } 15 | : { 16 | component: RouterLink, 17 | to: path, 18 | } 19 | : {}; 20 | 21 | return ( 22 |
  • 23 | 43 | {icon && ( 44 | 57 | {icon} 58 | 59 | )} 60 | theme.typography.fontFamily, 66 | fontSize: 14, 67 | fontWeight: 600, 68 | lineHeight: "24px", 69 | whiteSpace: "nowrap", 70 | ...(active && { 71 | color: "common.white", 72 | }), 73 | ...(disabled && { 74 | color: "neutral.500", 75 | }), 76 | }} 77 | > 78 | {title} 79 | 80 | 81 |
  • 82 | ); 83 | }; 84 | 85 | SideNavItem.propTypes = { 86 | active: PropTypes.bool, 87 | disabled: PropTypes.bool, 88 | external: PropTypes.bool, 89 | icon: PropTypes.node, 90 | path: PropTypes.string, 91 | title: PropTypes.string.isRequired, 92 | }; 93 | -------------------------------------------------------------------------------- /src/layouts/dashboard/side-nav.js: -------------------------------------------------------------------------------- 1 | import { Link as RouterLink, useLocation } from "react-router-dom"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Box, 5 | Divider, 6 | Drawer, 7 | Stack, 8 | Typography, 9 | useMediaQuery, IconButton 10 | } from "@mui/material"; 11 | import { Scrollbar } from "../../components/scrollbar"; 12 | import { items } from "./config"; 13 | import { SideNavItem } from "./side-nav-item"; 14 | import CloseIcon from '@mui/icons-material/Close'; 15 | 16 | export const SideNav = (props) => { 17 | const { open, onClose } = props; 18 | const location = useLocation(); 19 | const pathname = location.pathname; 20 | const lgUp = useMediaQuery((theme) => theme.breakpoints.up("lg")); 21 | 22 | const content = ( 23 | 34 | 41 | 42 | {!lgUp && 43 | 44 | 45 | 46 | 47 | 48 | } 49 | 50 | 59 | {/* */} 60 | Go to pro 61 | 62 | 74 | 75 | Smart Metrics Logbook 76 | 77 | 78 | 79 | 80 | 81 | 89 | 98 | {items.map((item) => { 99 | const active = item.path ? pathname === item.path : false; 100 | 101 | return ( 102 | 111 | ); 112 | })} 113 | 114 | 115 | 116 | 122 | 123 | 134 | Go to pro 135 | 136 | 137 | 138 | 139 | 140 | ); 141 | 142 | if (lgUp) { 143 | return ( 144 | 156 | {content} 157 | 158 | ); 159 | } 160 | 161 | return ( 162 | theme.zIndex.appBar + 100 }} 174 | variant="temporary" 175 | > 176 | {content} 177 | 178 | ); 179 | }; 180 | 181 | SideNav.propTypes = { 182 | onClose: PropTypes.func, 183 | open: PropTypes.bool, 184 | }; 185 | -------------------------------------------------------------------------------- /src/layouts/dashboard/top-nav.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import BellIcon from "@heroicons/react/24/solid/BellIcon"; 3 | import UsersIcon from "@heroicons/react/24/solid/UsersIcon"; 4 | import Bars3Icon from "@heroicons/react/24/solid/Bars3Icon"; 5 | import MagnifyingGlassIcon from "@heroicons/react/24/solid/MagnifyingGlassIcon"; 6 | import { 7 | Avatar, 8 | Badge, 9 | Box, 10 | IconButton, 11 | Stack, 12 | SvgIcon, 13 | Tooltip, 14 | useMediaQuery, 15 | } from "@mui/material"; 16 | import { alpha } from "@mui/material/styles"; 17 | import { usePopover } from "../../hooks/use-popover"; 18 | import { AccountPopover } from "./account-popover"; 19 | 20 | const SIDE_NAV_WIDTH = 280; 21 | const TOP_NAV_HEIGHT = 64; 22 | 23 | export const TopNav = (props) => { 24 | const { onNavOpen } = props; 25 | const lgUp = useMediaQuery((theme) => theme.breakpoints.up("lg")); 26 | const accountPopover = usePopover(); 27 | 28 | return ( 29 | <> 30 | 35 | alpha(theme.palette.background.default, 0.8), 36 | position: "sticky", 37 | left: { 38 | lg: `${SIDE_NAV_WIDTH}px`, 39 | }, 40 | top: 0, 41 | width: { 42 | lg: `calc(100% - ${SIDE_NAV_WIDTH}px)`, 43 | }, 44 | zIndex: (theme) => theme.zIndex.appBar, 45 | }} 46 | > 47 | 57 | 58 | { ( 59 | 60 | 61 | 62 | 63 | 64 | )} 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 100 | 101 | 102 | 103 | 109 | 110 | ); 111 | }; 112 | 113 | TopNav.propTypes = { 114 | onNavOpen: PropTypes.func, 115 | }; 116 | -------------------------------------------------------------------------------- /src/pages/Dashboard/BloodPressureChart.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { useSelector } from "react-redux"; 4 | import ReactApexChart from "react-apexcharts"; 5 | import moment from "moment"; 6 | const BloodChart = ({ 7 | data, 8 | filterStartDay, 9 | filterEndDay, 10 | selectedShowChatMetric, 11 | }) => { 12 | const metric = useSelector((state) => state.metrics.metrics).filter( 13 | (metric) => metric._id === selectedShowChatMetric 14 | )[0]; 15 | const ignore = metric.ignore; 16 | const sortedData = data.sort( 17 | (a, b) => new Date(b.updatedAt) - new Date(a.updatedAt) 18 | ); 19 | 20 | const dateArray = []; 21 | const hP = []; 22 | const lP = []; 23 | const hR = []; 24 | 25 | for ( 26 | let currentDate = filterStartDay.clone(); 27 | currentDate <= filterEndDay; 28 | currentDate.add(1, "days") 29 | ) { 30 | dateArray.push(currentDate.format("DD/MMMM")); 31 | 32 | const mm = sortedData.filter( 33 | (d) => 34 | moment(d.createdAt).local().format("YYYY-MM-DD") === 35 | currentDate.local().format("YYYY-MM-DD") 36 | ); 37 | const total = mm[0]?.wage?.split(","); 38 | 39 | 40 | if (currentDate.format("DD/MMMM") === filterStartDay.format("DD/MMMM")) { 41 | mm[0] 42 | ? hP.push({ 43 | x: currentDate.format("DD/MMMM"), 44 | y: parseInt(total[0]), 45 | }) 46 | : hP.push({ 47 | x: currentDate.format("DD/MMMM"), 48 | y: 0, 49 | }); 50 | mm[0] 51 | ? lP.push({ 52 | x: currentDate.format("DD/MMMM"), 53 | y: parseInt(total[1]), 54 | }) 55 | : lP.push({ 56 | x: currentDate.format("DD/MMMM"), 57 | y: 0, 58 | }); 59 | mm[0] 60 | ? hR.push({ 61 | x: currentDate.format("DD/MMMM"), 62 | y: parseInt(total[2]), 63 | }) 64 | : hR.push({ 65 | x: currentDate.format("DD/MMMM"), 66 | y: 0, 67 | }); 68 | } else if ( 69 | currentDate.format("DD/MMMM") === filterEndDay.format("DD/MMMM") 70 | ) { 71 | mm[0] 72 | ? hP.push({ 73 | x: currentDate.format("DD/MMMM"), 74 | y: parseInt(total[0]), 75 | }) 76 | : hP.push({ 77 | x: currentDate.format("DD/MMMM"), 78 | y: 0, 79 | }); 80 | mm[0] 81 | ? lP.push({ 82 | x: currentDate.format("DD/MMMM"), 83 | y: parseInt(total[1]), 84 | }) 85 | : lP.push({ 86 | x: currentDate.format("DD/MMMM"), 87 | y: 0, 88 | }); 89 | mm[0] 90 | ? hR.push({ 91 | x: currentDate.format("DD/MMMM"), 92 | y: parseInt(total[2]), 93 | }) 94 | : hR.push({ 95 | x: currentDate.format("DD/MMMM"), 96 | y: 0, 97 | }); 98 | } else { 99 | mm[0] 100 | ? hP.push({ 101 | x: currentDate.format("DD/MMMM"), 102 | y: parseInt(total[0]), 103 | }) 104 | : !ignore && 105 | hP.push({ 106 | x: currentDate.format("DD/MMMM"), 107 | y: 0, 108 | }); 109 | mm[0] 110 | ? lP.push({ 111 | x: currentDate.format("DD/MMMM"), 112 | y: parseInt(total[1]), 113 | }) 114 | : !ignore && 115 | lP.push({ 116 | x: currentDate.format("DD/MMMM"), 117 | y: 0, 118 | }); 119 | mm[0] 120 | ? hR.push({ 121 | x: currentDate.format("DD/MMMM"), 122 | y: parseInt(total[2]), 123 | }) 124 | : !ignore && 125 | hR.push({ 126 | x: currentDate.format("DD/MMMM"), 127 | y: 0, 128 | }); 129 | } 130 | } 131 | console.log(hP, lP, hR); 132 | const state = { 133 | options: { 134 | colors: ["#008FFB", "#00E396", "#F04438", "#FF4560"], 135 | chart: { 136 | id: "basic-line", 137 | dropShadow: { 138 | enabled: true, 139 | top: 4, 140 | left: 4, 141 | blur: 3, 142 | opacity: 0.2, 143 | }, 144 | background: "#ffffff", 145 | }, 146 | xaxis: { 147 | categories: dateArray, 148 | }, 149 | stroke: { 150 | curve: "smooth", 151 | }, 152 | title: { 153 | text: metric.name, 154 | }, 155 | markers: { 156 | size: 6, 157 | strokeWidth: 0, 158 | colors: ["#FFA41B"], 159 | hover: { 160 | size: 8, 161 | }, 162 | }, 163 | 164 | tooltip: { 165 | theme: "dark", 166 | x: { 167 | show: true, 168 | format: "MMM", 169 | }, 170 | y: [ 171 | { 172 | title: { 173 | formatter: function () { 174 | return ""; 175 | }, 176 | }, 177 | }, 178 | { 179 | title: { 180 | formatter: function () { 181 | return ""; 182 | }, 183 | }, 184 | }, 185 | ], 186 | }, 187 | }, 188 | series: [ 189 | { 190 | name: "Systolic pressure", 191 | data: hP, 192 | }, 193 | { 194 | name: "Diastolic pressure ", 195 | data: lP, 196 | }, 197 | { 198 | name: "Heart Rate", 199 | data: hR, 200 | }, 201 | ], 202 | }; 203 | 204 | return ( 205 | 211 | ); 212 | }; 213 | 214 | export default BloodChart; 215 | -------------------------------------------------------------------------------- /src/pages/Dashboard/EveryTimeChart.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import ReactApexChart from "react-apexcharts"; 4 | import moment from "moment-timezone"; 5 | const EveryTiimeChart = ({ data, selectedShowChatMetric, everyTimeDate }) => { 6 | const metric = useSelector((state) => state.metrics.metrics).filter( 7 | (metric) => metric._id === selectedShowChatMetric 8 | )[0]; 9 | const sortedData = data.sort( 10 | (a, b) => new Date(b.createdAt) - new Date(a.createdAt) 11 | ); 12 | const Today = moment(everyTimeDate).format("YYYY-MM-DD").toString(); 13 | function getDataBetweenDates(data) { 14 | const result = []; 15 | console.log(Today); 16 | 17 | data.forEach((item) => { 18 | const createdAt = moment(item.createdAt); 19 | console.log(createdAt.isSame(moment(Today), "day")); 20 | if (createdAt.isSame(moment(Today), "day") === true) { 21 | result.push(item); 22 | } 23 | }); 24 | return result; 25 | } 26 | const realDate = getDataBetweenDates(sortedData); 27 | console.log(realDate, selectedShowChatMetric); 28 | 29 | const valueArray = []; 30 | for (let dateIndex = 0; dateIndex < realDate.length; dateIndex++) { 31 | valueArray.push({ 32 | x: moment(data[dateIndex]?.createdAt), 33 | y: data[dateIndex]?.wage, 34 | }); 35 | } 36 | console.log(valueArray); 37 | const times = [ 38 | "00:00", 39 | "01:30", 40 | "03:00", 41 | "04:30", 42 | "06:00", 43 | "07:30", 44 | "09:00", 45 | "10:30", 46 | "12:00", 47 | "13:30", 48 | "15:00", 49 | "16:30", 50 | "18:00", 51 | "19:30", 52 | "21:00", 53 | "22:30", 54 | ]; 55 | const state = { 56 | colors: ["#00e396"], 57 | options: { 58 | chart: { 59 | id: "basic-line", 60 | height: 350, 61 | }, 62 | xaxis: { 63 | tickAmount: 8, 64 | type: "datetime", 65 | categories: valueArray ? "" : times, 66 | labels: { 67 | formatter: function (val, timestamp) { 68 | return moment(new Date(timestamp)).format("HH:mm:ss"); 69 | }, 70 | }, 71 | }, 72 | yaxis: { 73 | show: true, 74 | labels: { 75 | show: true, 76 | 77 | formatter: function (value) { 78 | return value; 79 | }, 80 | }, 81 | }, 82 | stroke: { 83 | curve: "smooth", 84 | width: 2, 85 | }, 86 | title: { 87 | text: metric?.name, 88 | }, 89 | dataLabels: { 90 | enabled: true, 91 | formatter: function (val) { 92 | return val === 0 ? "" : val; 93 | }, 94 | 95 | style: { 96 | fontSize: "12px", 97 | }, 98 | }, 99 | tooltip: { 100 | theme: "dark", 101 | x: { 102 | show: true, 103 | format: "MMM", 104 | }, 105 | y: { 106 | show: true, 107 | title: { 108 | formatter: function () { 109 | return `/${metric.postfix}`; 110 | }, 111 | }, 112 | }, 113 | }, 114 | tickAmount: 7, 115 | }, 116 | series: [ 117 | { 118 | name: "date", 119 | data: valueArray, 120 | }, 121 | ], 122 | }; 123 | 124 | return ( 125 | 131 | ); 132 | }; 133 | 134 | export default EveryTiimeChart; 135 | -------------------------------------------------------------------------------- /src/pages/Dashboard/GroupChart.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Chart from "react-apexcharts"; 3 | import { useSelector } from "react-redux"; 4 | import moment from "moment"; 5 | 6 | export default function GroupChart({ 7 | filterStartDay, 8 | filterEndDay, 9 | selectedGroup, 10 | metrics, 11 | allWages, 12 | setAllData, 13 | // allData, 14 | }) { 15 | const groups = useSelector((state) => state.group.groups); 16 | 17 | const dateArray = []; 18 | for ( 19 | let currentDate = filterStartDay.clone(); 20 | currentDate <= filterEndDay; 21 | currentDate.add(1, "days") 22 | ) { 23 | dateArray.push(currentDate.local().format("DD/MMMM")); 24 | } 25 | const realGroupMetrics = []; 26 | const mainGroup = groups.filter((group) => group?._id === selectedGroup)[0]; 27 | 28 | // eslint-disable-next-line array-callback-return 29 | mainGroup?.contents?.map((content) => { 30 | const filterMetric = metrics.filter( 31 | (metric) => metric?._id === content 32 | )[0]; 33 | filterMetric?.fieldType !== "bloodPressure" && 34 | filterMetric?.timing !== "everytime" && 35 | realGroupMetrics.push(filterMetric); 36 | }); 37 | 38 | // eslint-disable-next-line array-callback-return 39 | const TotalData = realGroupMetrics?.map((metric) => { 40 | const valueArray = []; 41 | 42 | const ignore = metric?.ignore; 43 | const eachWages = allWages.filter( 44 | (wage) => wage?.metricsId === metric?._id 45 | ); 46 | const sortedData = eachWages?.sort( 47 | (a, b) => new Date(b.createdAt) - new Date(a.createdAt) 48 | ); 49 | 50 | for ( 51 | let currentDate = filterStartDay.clone(); 52 | currentDate <= filterEndDay; 53 | currentDate.add(1, "days") 54 | ) { 55 | dateArray.push(currentDate.format("DD/MMMM")); 56 | 57 | const mm = sortedData.filter( 58 | (d) => 59 | moment(d.createdAt).local().format("YYYY-MM-DD") === 60 | currentDate.local().format("YYYY-MM-DD") 61 | ); 62 | if ( 63 | currentDate.local().format("DD/MMMM") === 64 | filterStartDay.local().format("DD/MMMM") 65 | ) { 66 | mm[0] 67 | ? valueArray.push({ 68 | x: currentDate.local().format("DD/MMMM"), 69 | y: 70 | parseFloat(mm[0]?.wage) - parseInt(mm[0]?.wage) === 0 71 | ? parseInt(mm[0]?.wage) 72 | : parseFloat(mm[0]?.wage), 73 | }) 74 | : valueArray.push({ 75 | x: currentDate.local().format("DD/MMMM"), 76 | y: 0, 77 | }); 78 | } else if ( 79 | currentDate.local().format("DD") === filterEndDay.format("DD") 80 | ) { 81 | mm[0] 82 | ? valueArray.push({ 83 | x: currentDate.local().format("DD/MMMM"), 84 | y: 85 | parseFloat(mm[0]?.wage) - parseInt(mm[0]?.wage) === 0 86 | ? parseInt(mm[0]?.wage) 87 | : parseFloat(mm[0]?.wage), 88 | }) 89 | : valueArray.push({ 90 | x: currentDate.local().format("DD/MMMM"), 91 | y: 0, 92 | }); 93 | } else { 94 | mm[0] 95 | ? valueArray.push({ 96 | x: currentDate.local().format("DD/MMMM"), 97 | y: 98 | parseFloat(mm[0]?.wage) - parseInt(mm[0]?.wage) === 0 99 | ? parseInt(mm[0]?.wage) 100 | : parseFloat(mm[0]?.wage), 101 | }) 102 | : !ignore && 103 | valueArray.push({ 104 | x: currentDate.local().format("DD/MMMM"), 105 | y: 0, 106 | }); 107 | } 108 | } 109 | return { name: metric?.name, data: valueArray }; 110 | 111 | // eslint-disable-next-line array-callback-return 112 | }); 113 | // eslint-disable-next-line react-hooks/exhaustive-deps 114 | const options = { 115 | chart: { 116 | id: "basic-bar", 117 | }, 118 | xaxis: { 119 | type: "datetime", 120 | labels: { 121 | formatter: function (val, timestamp) { 122 | return moment(new Date(timestamp)).format("DD-MMM "); 123 | }, 124 | }, 125 | }, 126 | yaxis: { 127 | labels: { 128 | show: true, 129 | formatter: function (value) { 130 | return value; 131 | }, 132 | }, 133 | }, 134 | dataLabels: { 135 | enabled: false, 136 | }, 137 | stroke: { 138 | curve: "smooth", 139 | }, 140 | 141 | }; 142 | const series = [...TotalData]; 143 | return ( 144 |
    145 | 146 |
    147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /src/pages/Dashboard/OneLineChart.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import ReactApexChart from "react-apexcharts"; 4 | import moment from "moment-timezone"; 5 | const OneLineChart = ({ 6 | data, 7 | selectedShowChatMetric, 8 | filterStartDay, 9 | filterEndDay, 10 | }) => { 11 | const metric = useSelector((state) => state.metrics.metrics).filter( 12 | (metric) => metric._id === selectedShowChatMetric 13 | )[0]; 14 | const sortedData = data.sort( 15 | (a, b) => new Date(b.updatedAt) - new Date(a.updatedAt) 16 | ); 17 | const ignore = metric.ignore; 18 | const dateArray = []; 19 | const valueArray = []; 20 | for ( 21 | let currentDate = filterStartDay.clone(); 22 | currentDate <= filterEndDay; 23 | currentDate.add(1, "days") 24 | ) { 25 | dateArray.push(currentDate.format("DD/MMMM")); 26 | 27 | const mm = sortedData.filter( 28 | (d) => 29 | moment(d.createdAt).format("YYYY-MM-DD") === 30 | currentDate.format("YYYY-MM-DD") 31 | ); 32 | if (currentDate.format("DD/MMMM") === filterStartDay.format("DD/MMMM")) { 33 | mm[0] 34 | ? valueArray.push({ 35 | x: currentDate.format("DD/MMMM"), 36 | y: 37 | parseFloat(mm[0]?.wage) - parseInt(mm[0]?.wage) === 0 38 | ? parseInt(mm[0]?.wage) 39 | : parseFloat(mm[0]?.wage), 40 | }) 41 | : valueArray.push({ 42 | x: currentDate.format("DD/MMMM"), 43 | y: 0, 44 | }); 45 | } else if ( 46 | currentDate.format("DD/MMMM") === filterEndDay.format("DD/MMMM") 47 | ) { 48 | mm[0] 49 | ? valueArray.push({ 50 | x: currentDate.format("DD/MMMM"), 51 | y: 52 | parseFloat(mm[0]?.wage) - parseInt(mm[0]?.wage) === 0 53 | ? parseInt(mm[0]?.wage) 54 | : parseFloat(mm[0]?.wage), 55 | }) 56 | : valueArray.push({ 57 | x: currentDate.format("DD/MMMM"), 58 | y: 0, 59 | }); 60 | } else { 61 | mm[0] 62 | ? valueArray.push({ 63 | x: currentDate.format("DD/MMMM"), 64 | y: 65 | parseFloat(mm[0]?.wage) - parseInt(mm[0]?.wage) === 0 66 | ? parseInt(mm[0]?.wage) 67 | : parseFloat(mm[0]?.wage), 68 | }) 69 | : !ignore && 70 | valueArray.push({ 71 | x: currentDate.format("DD/MMMM"), 72 | y: 0, 73 | }); 74 | } 75 | } 76 | 77 | const state = { 78 | colors: ["#00e396"], 79 | options: { 80 | chart: { 81 | id: "basic-line", 82 | height: 350, 83 | }, 84 | xaxis: { 85 | categories: dateArray, 86 | }, 87 | yaxis: { 88 | show: true, 89 | labels: { 90 | show: true, 91 | formatter: function (value) { 92 | return value; 93 | }, 94 | }, 95 | }, 96 | stroke: { 97 | curve: "smooth", 98 | width: 2, 99 | }, 100 | title: { 101 | text: metric?.name, 102 | }, 103 | dataLabels: { 104 | enabled: true, 105 | formatter: function (val) { 106 | return val === 0 ? "" : val; 107 | }, 108 | 109 | style: { 110 | fontSize: "12px", 111 | }, 112 | }, 113 | tooltip: { 114 | theme: "dark", 115 | x: { 116 | show: true, 117 | format: "MMM", 118 | }, 119 | y: { 120 | show: true, 121 | title: { 122 | formatter: function () { 123 | return `/${metric.postfix}`; 124 | }, 125 | }, 126 | }, 127 | }, 128 | tickAmount: 7, 129 | }, 130 | series: [ 131 | { 132 | name: "date", 133 | data: valueArray, 134 | }, 135 | ], 136 | }; 137 | 138 | return ( 139 | 145 | ); 146 | }; 147 | 148 | export default OneLineChart; 149 | -------------------------------------------------------------------------------- /src/pages/Dashboard/TableForTextMetrics.js: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | TableBody, 4 | TableCell, 5 | TableHead, 6 | TableRow, 7 | TableContainer, 8 | Box, 9 | } from "@mui/material"; 10 | import moment from "moment"; 11 | import React from "react"; 12 | 13 | const TableForTextMetrics = ({ 14 | data, 15 | selectedShowChatMetric, 16 | filterStartDay, 17 | filterEndDay, 18 | }) => { 19 | const valueArray = []; 20 | const sortedData = data.sort( 21 | (a, b) => new Date(b.createdAt) - new Date(a.createdAt) 22 | ); 23 | for ( 24 | let currentDate = filterStartDay.clone(); 25 | currentDate <= filterEndDay; 26 | currentDate.add(1, "days") 27 | ) { 28 | const mm = sortedData.filter( 29 | (d) => 30 | moment(d.createdAt).format("YYYY-MM-DD") === 31 | currentDate.format("YYYY-MM-DD") 32 | ); 33 | valueArray.push(...mm); 34 | } 35 | 36 | function sortDataByRecentDate(data) { 37 | const sortedData = data.sort( 38 | (a, b) => new Date(b.createdAt) - new Date(a.createdAt) 39 | ); 40 | 41 | const groupedData = sortedData.reduce((acc, curr) => { 42 | const date = new Date(curr.createdAt).toLocaleDateString(); 43 | if (!acc[date]) { 44 | acc[date] = []; 45 | } 46 | acc[date].push(curr); 47 | return acc; 48 | }, {}); 49 | 50 | const sortedDates = Object.keys(groupedData).sort( 51 | (a, b) => new Date(b) - new Date(a) 52 | ); 53 | 54 | const sortedGroupedData = []; 55 | for (const date of sortedDates) { 56 | sortedGroupedData.push(groupedData[date]); 57 | } 58 | return sortedGroupedData; 59 | } 60 | const rows = sortDataByRecentDate(valueArray); 61 | 62 | return ( 63 | 64 | 65 | 66 | 67 | 68 | 69 | No 70 | 71 | 72 | Date 73 | 74 | 75 | Content 76 | 77 | 78 | 79 | 80 | {rows.map((row, ind) => 81 | row?.map((item, index) => 82 | index === 0 ? ( 83 | 84 | 91 | {ind + 1} 92 | 93 | 100 | {moment(row[0]?.createdAt).format("MM-DD-YYYY")} 101 | 102 | 109 | {item.wage} 110 | 111 | 112 | ) : ( 113 | 114 | 121 | {item.wage} 122 | 123 | 124 | ) 125 | ) 126 | )} 127 | 128 |
    129 |
    130 |
    131 | ); 132 | }; 133 | 134 | export default TableForTextMetrics; 135 | -------------------------------------------------------------------------------- /src/pages/Metrics/Metrics.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect, useDispatch } from "react-redux"; 4 | import PlusIcon from "@heroicons/react/24/solid/PlusIcon"; 5 | import { 6 | Box, 7 | Button, 8 | Container, 9 | Stack, 10 | SvgIcon, 11 | Typography, 12 | } from "@mui/material"; 13 | import { Layout as DashboardLayout } from "../../layouts/dashboard/layout"; 14 | import { MetricsTable } from "../../sections/metrics/metrics-table"; 15 | import { MetricsSearch } from "../../sections/metrics/metrics-search"; 16 | import { usePopover } from "../../hooks/use-popover"; 17 | import { 18 | fieldTypes, 19 | chartTypes, 20 | statuses, 21 | timings, 22 | } from "../../sections/metrics/metrics-dialog-title"; 23 | import { MetricsDialog } from "../../sections/metrics/metrics-dialog"; 24 | import { 25 | apiAddMetric, 26 | apiUpdateMetric, 27 | apiGetMetricsByUserId, 28 | } from "../../actions/metrics"; 29 | import { apiLogout } from "../../actions/auth"; 30 | 31 | const Metrics = ({ 32 | metrics: { metrics }, 33 | apiAddMetric, 34 | apiUpdateMetric, 35 | apiLogout, 36 | }) => { 37 | const dispatch = useDispatch(); 38 | const metricsPopover = usePopover(); 39 | const [deletedId, setDeletedId] = React.useState(""); 40 | const [search, setSearch] = React.useState(""); 41 | 42 | const initialValues = { 43 | _id: "", 44 | name: "", 45 | description: "", 46 | fieldType: fieldTypes[0].value, 47 | prefix: "", 48 | postfix: "", 49 | chartType: chartTypes[0].value, 50 | status: statuses[0].value, 51 | ignore: true, 52 | timing: timings[0].value, 53 | }; 54 | const [editedMetric, setEditedMetric] = React.useState({ ...initialValues }); 55 | React.useEffect(() => { 56 | dispatch(apiGetMetricsByUserId()); 57 | }, [dispatch]); 58 | return ( 59 | 60 | 67 | 68 | 69 | 70 | 71 | Metrics 72 | 73 |
    74 | 90 |
    91 |
    92 | 93 | 98 | metric.name.toLowerCase().includes(search.toLowerCase()) 99 | ) 100 | } 101 | // onDeselectAll={metricsSelection.handleDeselectAll} 102 | // onDeselectOne={metricsSelection.handleDeselectOne} 103 | // onSelectAll={metricsSelection.handleSelectAll} 104 | // onSelectOne={metricsSelection.handleSelectOne} 105 | // selected={metricsSelection.selected} 106 | setDeletedId={setDeletedId} 107 | deletedId={deletedId} 108 | setEditedMetric={setEditedMetric} 109 | openMetricModal={metricsPopover.handleOpen} 110 | /> 111 |
    112 |
    113 |
    114 | 126 |
    127 | ); 128 | }; 129 | 130 | Metrics.propTypes = { 131 | apiAddMetric: PropTypes.func.isRequired, 132 | apiGetMetricsByUserId: PropTypes.func.isRequired, 133 | apiUpdateMetric: PropTypes.func.isRequired, 134 | apiLogout: PropTypes.func.isRequired, 135 | metrics: PropTypes.object.isRequired, 136 | }; 137 | 138 | const mapStateToProps = (state) => ({ 139 | metrics: state.metrics, 140 | }); 141 | 142 | export default connect(mapStateToProps, { 143 | apiAddMetric, 144 | apiGetMetricsByUserId, 145 | apiUpdateMetric, 146 | apiLogout, 147 | })(Metrics); 148 | -------------------------------------------------------------------------------- /src/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | import { Box, Button, Container, SvgIcon, Typography } from "@mui/material"; 2 | import { ArrowLeft as IconArrowLeft } from "@mui/icons-material"; 3 | const Page404 = () => ( 4 | 13 | 14 | 21 | 27 | Under development 36 | 37 | 38 | 404: The page you are looking for isn’t here 39 | 40 | 41 | You either tried some shady route or you came here by mistake. 42 | Whichever it is, try using the navigation 43 | 44 | 57 | 58 | 59 | 60 | ); 61 | 62 | export default Page404; 63 | -------------------------------------------------------------------------------- /src/pages/TotalMetricsValues/TotalMetricsValues.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import moment from "moment"; 4 | import { 5 | Box, 6 | Container, 7 | IconButton, 8 | SvgIcon, 9 | Tooltip, 10 | Table, 11 | TableHead, 12 | TableRow, 13 | TableCell, 14 | TableBody, 15 | TablePagination, 16 | TextField, 17 | TableContainer, 18 | Paper, 19 | Grid, 20 | } from "@mui/material"; 21 | import { Layout as DashboardLayout } from "../../layouts/dashboard/layout"; 22 | import { ArrowLeftIcon } from "@heroicons/react/24/solid"; 23 | import { MetricsSearch } from "../../sections/metrics/metrics-search"; 24 | import { apiLogout } from "../../actions/auth"; 25 | 26 | import { apiGetMetricsAllWagesByUserId } from "../../actions/metrics"; 27 | import { useNavigate } from "react-router-dom"; 28 | 29 | const TotalMetricsValues = () => { 30 | const navigate = useNavigate(); 31 | const dispatch = useDispatch(); 32 | const metrics = useSelector((state) => state.metrics.metrics); 33 | const allWages = useSelector((state) => state.metrics.wages); 34 | const [search, setSearch] = React.useState(""); 35 | 36 | const [page, setPage] = React.useState(0); 37 | const [rowsPerPage, setRowsPerPage] = React.useState(5); 38 | const matchingWages = allWages?.reduce((acc, wage) => { 39 | const metric = metrics.find( 40 | (metric) => metric._id.toString() === wage.metricsId.toString() 41 | ); 42 | if (metric) { 43 | const date = moment(wage.createdAt) 44 | .format("YYYY-MM-DD HH:mm:ss") 45 | .toLocaleString() 46 | .slice(0, 10); 47 | if (!acc[date]) { 48 | acc[date] = []; 49 | } 50 | acc[date].push({ metric, wage }); 51 | } 52 | return acc; 53 | }, {}); 54 | 55 | const rows = 56 | matchingWages && 57 | Object.entries(matchingWages) 58 | .sort((a, b) => b[0].localeCompare(a[0])) 59 | .map(([date, wages]) => ({ date, wages })); 60 | const emptyRows = 61 | rowsPerPage - Math.min(rowsPerPage, rows?.length - page * rowsPerPage); 62 | const [startDate, setStartDate] = React.useState(""); 63 | const [current, setCurrentRows] = React.useState(rows); 64 | 65 | const [endDate, setEndDate] = React.useState(moment().format("YYYY-MM-DD")); 66 | React.useEffect(() => { 67 | dispatch(apiGetMetricsAllWagesByUserId); 68 | // eslint-disable-next-line 69 | }, []); 70 | 71 | const handleChangePage = (event, newPage) => { 72 | setPage(newPage); 73 | }; 74 | 75 | const handleChangeRowsPerPage = (event) => { 76 | setRowsPerPage(parseInt(event.target.value, 10)); 77 | setPage(0); 78 | }; 79 | 80 | React.useEffect(() => { 81 | const start = startDate.valueOf(); 82 | const end = endDate.valueOf(); 83 | const findWages = (rows) => { 84 | let newRows = []; 85 | let date = []; 86 | 87 | rows.forEach((item) => { 88 | item.wages.forEach((wageItem) => { 89 | if (wageItem.wage.wage.includes(search)) { 90 | !date.includes(item.date) && newRows.push(item); 91 | !date.includes(item.date) && date.push(item.date); 92 | } 93 | }); 94 | }); 95 | 96 | return newRows; 97 | }; 98 | setCurrentRows( 99 | findWages(rows).filter( 100 | (row) => start <= row.date.valueOf() && end >= row.date.valueOf() 101 | ) 102 | ); 103 | }, [search, startDate, endDate]); //eslint-disable-line 104 | 105 | return ( 106 | 107 | 114 | 115 | 122 | 123 | 124 | navigate("/")}> 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | setStartDate(e.target.value)} 147 | /> 148 | 149 | 150 | setEndDate(e.target.value)} 159 | /> 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | No 171 | Date 172 | {metrics && 173 | metrics.map((metric) => ( 174 | 175 | {metric?.name} 176 | 177 | ))} 178 | 179 | 180 | 181 | {current 182 | ?.slice( 183 | page * rowsPerPage, 184 | page * rowsPerPage + rowsPerPage 185 | ) 186 | ?.map((row, index) => ( 187 | 188 | {index + 1} 189 | {row?.date} 190 | {metrics?.map((metric) => { 191 | const wage = row.wages.find( 192 | (w) => w?.metric?._id === metric?._id 193 | ); 194 | return ( 195 | 196 | {wage ? wage.wage.wage : "-"} 197 | 198 | ); 199 | })} 200 | 201 | ))} 202 | {emptyRows > 0 && ( 203 | 204 | 205 | 206 | )} 207 | 208 | 217 | 218 | 219 |
    220 |
    221 |
    222 |
    223 |
    224 |
    225 |
    226 | ); 227 | }; 228 | export default TotalMetricsValues; 229 | -------------------------------------------------------------------------------- /src/pages/Track/EditTrackDialogForBlood.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | import * as yup from "yup"; 4 | import PropTypes from "prop-types"; 5 | import { useFormik } from "formik"; 6 | import { useDispatch, useSelector } from "react-redux"; 7 | import { 8 | Box, 9 | Button, 10 | Card, 11 | CardActions, 12 | CardContent, 13 | CardHeader, 14 | Divider, 15 | TextField, 16 | InputAdornment, 17 | SvgIcon, 18 | } from "@mui/material"; 19 | 20 | import { HeartIcon } from "@heroicons/react/24/solid"; 21 | 22 | import MonitorHeartIcon from "@mui/icons-material/MonitorHeart"; 23 | 24 | import { BootstrapDialog } from "../../components/BootstrapDialog"; 25 | 26 | import { apiUpdateMetricWage } from "../../actions/metrics"; 27 | 28 | const EditTrackDialogForBlood = (props) => { 29 | const dispatch = useDispatch(); 30 | const { onClose, openEditMetricWageDialogForBlood, editedMetricWage } = props; 31 | const user = useSelector((state) => state.auth.user); 32 | 33 | 34 | const validationNumber = yup.object({ 35 | BPH: yup 36 | .number("Metric Vaule must be number") 37 | .required("metricValue is required"), 38 | BPL: yup 39 | .number("Metric Vaule must be number") 40 | .required("metricValue is required"), 41 | HR: yup 42 | .number("Metric Vaule must be number") 43 | .required("metricValue is required"), 44 | }); 45 | 46 | const formik = useFormik({ 47 | initialValues: { 48 | fieldType: editedMetricWage?.fieldType, 49 | _id: editedMetricWage._id ? editedMetricWage._id : "", 50 | BPH: "", 51 | BPL: "", 52 | HR: "", 53 | }, 54 | validateOnSubmit: true, 55 | validationSchema: validationNumber, 56 | 57 | onSubmit: (values) => { 58 | dispatch( 59 | apiUpdateMetricWage({ 60 | _id: values._id, 61 | userId: user?._id, 62 | metricId: values._id, 63 | wage: values.BPH + "," + values.BPL + "," + values.HR, 64 | }) 65 | ); 66 | 67 | props.onClose(); 68 | }, 69 | }); 70 | 71 | useEffect(() => { 72 | const kk = editedMetricWage?.wage?.split(","); 73 | formik.setValues({ 74 | _id: editedMetricWage?._id, 75 | BPH: kk && kk[0], 76 | BPL: kk && kk[1], 77 | HR: kk && kk[2], 78 | fieldType: editedMetricWage?.fieldType, 79 | }); 80 | // eslint-disable-next-line 81 | }, [editedMetricWage]); 82 | 83 | return ( 84 | 92 |
    93 | 96 | 100 | 101 | 102 | 103 | 108 | 109 | 121 | 126 | 127 | 128 | 129 | ), 130 | }} 131 | /> 132 | 133 | 134 | 145 | 150 | 151 | 152 | 153 | ), 154 | }} 155 | /> 156 | 157 | 158 | 169 | 174 | 175 | 176 | 177 | ), 178 | }} 179 | /> 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 190 | 191 | 194 | 195 | 196 |
    197 |
    198 | ); 199 | }; 200 | EditTrackDialogForBlood.propTypes = { 201 | onClose: PropTypes.func, 202 | }; 203 | 204 | export default EditTrackDialogForBlood; 205 | -------------------------------------------------------------------------------- /src/pages/auth/Login.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link as RouterLink, Navigate } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | import { connect } from "react-redux"; 5 | import { apiLogin } from "../../actions/auth"; 6 | import { useFormik } from "formik"; 7 | import * as Yup from "yup"; 8 | import { 9 | Box, 10 | Button, 11 | Stack, 12 | Link, 13 | TextField, 14 | Typography, 15 | } from "@mui/material"; 16 | import { Layout as AuthLayout } from "../../layouts/auth/layout"; 17 | 18 | const Login = ({ isAuthenticated, apiLogin }) => { 19 | const formik = useFormik({ 20 | initialValues: { 21 | email: "", 22 | password: "", 23 | submit: null, 24 | }, 25 | validationSchema: Yup.object({ 26 | email: Yup.string() 27 | .email("Must be a valid email") 28 | .max(255) 29 | .required("Email is required"), 30 | password: Yup.string().max(255).required("Password is required"), 31 | }), 32 | onSubmit: (values) => { 33 | apiLogin({ 34 | email: values.email, 35 | password: values.password, 36 | }); 37 | }, 38 | }); 39 | 40 | if (isAuthenticated) { 41 | return ; 42 | } 43 | 44 | return ( 45 | 46 | 55 | 63 |
    64 | 65 | Login 66 | 67 | Don't have an account?   68 | 74 | Register 75 | 76 | 77 | 78 |
    79 | 80 | 91 | 104 | // 105 | // 106 | // 107 | // 108 | // ), 109 | // }} 110 | /> 111 | 112 | {formik.errors.submit && ( 113 | 114 | {formik.errors.submit} 115 | 116 | )} 117 | 126 |
    127 |
    128 |
    129 |
    130 |
    131 | ); 132 | }; 133 | 134 | Login.propTypes = { 135 | apiLogin: PropTypes.func.isRequired, 136 | isAuthenticated: PropTypes.bool, 137 | }; 138 | 139 | const mapStateToProps = (state) => ({ 140 | isAuthenticated: state.auth.isAuthenticated, 141 | }); 142 | 143 | export default connect(mapStateToProps, { apiLogin })(Login); 144 | -------------------------------------------------------------------------------- /src/pages/auth/Register.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link as RouterLink, Navigate } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import { useFormik } from "formik"; 5 | import * as Yup from "yup"; 6 | import { 7 | Box, 8 | Button, 9 | Link, 10 | Stack, 11 | TextField, 12 | Typography, 13 | } from "@mui/material"; 14 | import PropTypes from "prop-types"; 15 | import { Layout as AuthLayout } from "../../layouts/auth/layout"; 16 | 17 | 18 | import { apiRegister } from "../../actions/auth"; 19 | 20 | const Register = ({ apiRegister, isAuthenticated }) => { 21 | const formik = useFormik({ 22 | initialValues: { 23 | email: "", 24 | name: "", 25 | password: "", 26 | submit: null, 27 | }, 28 | validationSchema: Yup.object({ 29 | email: Yup.string() 30 | .email("Must be a valid email") 31 | .max(255) 32 | .required("Email is required"), 33 | name: Yup.string().max(255).required("Name is required"), 34 | password: Yup.string().max(255).required("Password is required"), 35 | }), 36 | onSubmit: (values) => { 37 | apiRegister({ 38 | name: values.name, 39 | email: values.email, 40 | password: values.password, 41 | }); 42 | }, 43 | }); 44 | 45 | if (isAuthenticated) { 46 | return ; 47 | } 48 | 49 | return ( 50 | 51 | 59 | 67 |
    68 | 69 | Register 70 | 71 | Already have an account?   72 | 78 | Log in 79 | 80 | 81 | 82 |
    83 | 84 | 96 | // 97 | // 98 | // 99 | // 100 | // ), 101 | // }} 102 | /> 103 | 116 | // 117 | // 118 | // 119 | // 120 | // ), 121 | // }} 122 | /> 123 | 136 | // 137 | // 138 | // 139 | // 140 | // ), 141 | // }} 142 | /> 143 | 144 | {formik.errors.submit && ( 145 | 146 | {formik.errors.submit} 147 | 148 | )} 149 | 158 |
    159 |
    160 |
    161 |
    162 |
    163 | ); 164 | }; 165 | 166 | Register.propTypes = { 167 | // register: PropTypes.func.isRequired, 168 | isAuthenticated: PropTypes.bool, 169 | }; 170 | 171 | const mapStateToProps = (state) => ({ 172 | isAuthenticated: state.auth.isAuthenticated, 173 | }); 174 | 175 | export default connect(mapStateToProps, { apiRegister })(Register); 176 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/sections/dashboard/to-do-card.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | import ClockIcon from "@heroicons/react/24/solid/ClockIcon"; 4 | import { 5 | Avatar, 6 | Card, 7 | CardContent, 8 | Stack, 9 | SvgIcon, 10 | Tooltip, 11 | Typography, 12 | } from "@mui/material"; 13 | 14 | export const ToDoCard = (props) => { 15 | const { sx, metric, setOpenModal, setOpenBlood, setSelectedMetric } = props; 16 | 17 | return ( 18 | 25 | { 29 | metric.fieldType === "bloodPressure" 30 | ? setOpenBlood(true) 31 | : setOpenModal(true); 32 | 33 | setSelectedMetric(metric); 34 | }} 35 | > 36 | 37 | 43 | 50 | 51 | 52 | 53 | 54 | 55 | 60 | {metric.name} 61 | 62 | 63 | 64 | 65 | 66 | 67 | ); 68 | }; 69 | 70 | ToDoCard.prototypes = { 71 | sx: PropTypes.object, 72 | value: PropTypes.string.isRequired, 73 | }; 74 | -------------------------------------------------------------------------------- /src/sections/dashboard/to-do-dialog-for-bloodPressure.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | import * as yup from "yup"; 4 | import PropTypes from "prop-types"; 5 | import { useFormik } from "formik"; 6 | import { useDispatch } from "react-redux"; 7 | import { 8 | Box, 9 | Button, 10 | Card, 11 | CardActions, 12 | CardContent, 13 | CardHeader, 14 | Divider, 15 | TextField, 16 | InputAdornment, 17 | SvgIcon, 18 | } from "@mui/material"; 19 | 20 | import { HeartIcon } from "@heroicons/react/24/solid"; 21 | 22 | import MonitorHeartIcon from "@mui/icons-material/MonitorHeart"; 23 | 24 | import { BootstrapDialog } from "../../components/BootstrapDialog"; 25 | 26 | import { apiAddMetricWage } from "../../actions/metrics"; 27 | 28 | const ToDoForBloodPressure = (props) => { 29 | const dispatch = useDispatch(); 30 | const { onClose, open, selectedMetric } = props; 31 | 32 | 33 | const validationNumber = yup.object({ 34 | BPH: yup 35 | .number("Metric Vaule must be number") 36 | .required("metricValue is required"), 37 | BPL: yup 38 | .number("Metric Vaule must be number") 39 | .required("metricValue is required"), 40 | HR: yup 41 | .number("Metric Vaule must be number") 42 | .required("metricValue is required"), 43 | }); 44 | 45 | const formik = useFormik({ 46 | initialValues: { 47 | fieldType: selectedMetric?.fieldType, 48 | _id: selectedMetric._id ? selectedMetric._id : "", 49 | BPH: "", 50 | BPL: "", 51 | HR: "", 52 | }, 53 | validateOnSubmit: true, 54 | validationSchema: validationNumber, 55 | 56 | onSubmit: (values) => { 57 | dispatch( 58 | apiAddMetricWage({ 59 | fieldType: values.fieldType, 60 | metricId: values._id, 61 | metricValue: values.BPH + "," + values.BPL + "," + values.HR, 62 | }) 63 | ); 64 | 65 | props.onClose(); 66 | }, 67 | }); 68 | useEffect(() => { 69 | formik.setValues({ 70 | _id: selectedMetric._id, 71 | BPH: "", 72 | BPL: "", 73 | HR: "", 74 | fieldType: selectedMetric.fieldType, 75 | }); 76 | // eslint-disable-next-line 77 | }, [selectedMetric]); 78 | return ( 79 | 87 |
    88 | 89 | 93 | 94 | 95 | 96 | 101 | 102 | 114 | 119 | 120 | 121 | 122 | ), 123 | }} 124 | /> 125 | 126 | 127 | 138 | 143 | 144 | 145 | 146 | ), 147 | }} 148 | /> 149 | 150 | 151 | 162 | 167 | 168 | 169 | 170 | ), 171 | }} 172 | /> 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 183 | 184 | 187 | 188 | 189 |
    190 |
    191 | ); 192 | }; 193 | ToDoForBloodPressure.propTypes = { 194 | onClose: PropTypes.func, 195 | open: PropTypes.bool.isRequired, 196 | }; 197 | 198 | export default ToDoForBloodPressure; 199 | -------------------------------------------------------------------------------- /src/sections/dashboard/to-do-dialog.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { styled } from "@mui/material/styles"; 3 | import * as yup from "yup"; 4 | import PropTypes from "prop-types"; 5 | import { useFormik } from "formik"; 6 | import { useDispatch } from "react-redux"; 7 | import { 8 | Box, 9 | Button, 10 | Card, 11 | CardActions, 12 | CardContent, 13 | CardHeader, 14 | Divider, 15 | TextField, 16 | InputAdornment, 17 | SvgIcon, 18 | Typography, 19 | } from "@mui/material"; 20 | import { Rating } from "@mui/material"; 21 | import { HeartIcon } from "@heroicons/react/24/solid"; 22 | import FavoriteIcon from "@mui/icons-material/Favorite"; 23 | import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder"; 24 | import MonitorHeartIcon from "@mui/icons-material/MonitorHeart"; 25 | 26 | import { BootstrapDialog } from "../../components/BootstrapDialog"; 27 | 28 | import { apiAddMetricWage } from "../../actions/metrics"; 29 | 30 | const StyledRating = styled(Rating)({ 31 | "& .MuiRating-iconFilled": { 32 | color: "#f31212", 33 | }, 34 | "& .MuiRating-iconHover": { 35 | color: "#ff5d02", 36 | }, 37 | }); 38 | 39 | const ToDoDialog = (props) => { 40 | const dispatch = useDispatch(); 41 | const { onClose, open, selectedMetric } = props; 42 | 43 | const validationString = yup.object({ 44 | metricValue: yup 45 | .string("Enter your metricValue") 46 | .required("metricValue is required"), 47 | }); 48 | const validationNumber = yup.object({ 49 | metricValue: yup 50 | .number("Metric Vaule must be number") 51 | .required("metricValue is required"), 52 | }); 53 | 54 | const formik = useFormik({ 55 | initialValues: { 56 | fieldType: selectedMetric?.fieldType, 57 | _id: selectedMetric._id ? selectedMetric._id : "", 58 | metricValue: "", 59 | }, 60 | validateOnSubmit: true, 61 | validationSchema: 62 | selectedMetric.fieldType === "text" ? validationString : validationNumber, 63 | 64 | onSubmit: (values) => { 65 | dispatch( 66 | apiAddMetricWage({ 67 | fieldType: values.fieldType, 68 | metricId: values._id, 69 | metricValue: values.metricValue, 70 | }) 71 | ); 72 | 73 | props.onClose(); 74 | }, 75 | }); 76 | useEffect(() => { 77 | formik.setValues({ 78 | _id: selectedMetric._id, 79 | metricValue: "", 80 | fieldType: selectedMetric.fieldType, 81 | }); 82 | // eslint-disable-next-line 83 | }, [selectedMetric]); 84 | return ( 85 | 93 |
    94 | 97 | {selectedMetric.name}} 100 | /> 101 | 102 | 103 | 104 | 105 | {selectedMetric.fieldType === "number" || 106 | selectedMetric.fieldType === "text" ? ( 107 | 132 | {selectedMetric.fieldType !== "text" && 133 | selectedMetric.prefix} 134 | 135 | ), 136 | endAdornment: ( 137 | 138 | {selectedMetric.fieldType !== "text" && 139 | selectedMetric.postfix} 140 | 141 | ), 142 | }} 143 | /> 144 | ) : null} 145 | 146 | {selectedMetric.fieldType === "bloodPressure" ? ( 147 | 151 | 152 | 164 | 169 | 170 | 171 | 172 | ), 173 | }} 174 | /> 175 | 176 | 177 | 188 | 193 | 194 | 195 | 196 | ), 197 | }} 198 | /> 199 | 200 | 201 | 212 | 217 | 218 | 219 | 220 | ), 221 | }} 222 | /> 223 | 224 | 225 | ) : null} 226 | {selectedMetric.fieldType === "5rating" || 227 | selectedMetric.fieldType === "10rating" ? ( 228 | 229 | 241 | } 242 | emptyIcon={ 243 | 246 | } 247 | /> 248 | 249 | ) : null} 250 | 251 | 252 | 253 | 254 | 255 | 258 | 259 | 262 | 263 | 264 |
    265 |
    266 | ); 267 | }; 268 | ToDoDialog.propTypes = { 269 | onClose: PropTypes.func, 270 | open: PropTypes.bool.isRequired, 271 | }; 272 | 273 | export default ToDoDialog; 274 | -------------------------------------------------------------------------------- /src/sections/group/groupCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Card, 4 | Typography, 5 | IconButton, 6 | SvgIcon, 7 | MenuItem, 8 | Menu, 9 | } from "@mui/material"; 10 | import { 11 | EllipsisVerticalIcon as MoreIcon, 12 | } from "@heroicons/react/24/solid"; 13 | import { Box } from "@mui/system"; 14 | 15 | const GroupCard = ({ group, handleClickEdit, setDeletedId, setOpenDialog }) => { 16 | const moreRef = React.useRef(null); 17 | const [isMenuOpen, setMenuOpen] = React.useState(false); 18 | const handleMenuOpen = () => { 19 | setMenuOpen(true); 20 | }; 21 | 22 | const handleMenuClose = () => { 23 | setMenuOpen(false); 24 | }; 25 | return ( 26 | 27 | 28 | 29 | {group.name} 30 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 53 | { 55 | handleClickEdit(group); 56 | handleMenuClose(); 57 | }} 58 | > 59 | Edit 60 | 61 | { 63 | setDeletedId(group._id); 64 | handleMenuClose(); 65 | setOpenDialog(true); 66 | }} 67 | > 68 | Delete 69 | 70 | 71 | 72 | ); 73 | }; 74 | 75 | export default GroupCard; 76 | -------------------------------------------------------------------------------- /src/sections/group/metricCard.js: -------------------------------------------------------------------------------- 1 | import { Card } from "@mui/material"; 2 | import React from "react"; 3 | 4 | const MetricCard = () => { 5 | return ; 6 | }; 7 | 8 | export default MetricCard; 9 | -------------------------------------------------------------------------------- /src/sections/metrics/metrics-dialog-title.js: -------------------------------------------------------------------------------- 1 | export const fieldTypes = [ 2 | { 3 | value: "number", 4 | label: "Number", 5 | }, 6 | { 7 | value: "text", 8 | label: "Text", 9 | }, 10 | { value: "bloodPressure", label: "Blood Pressure" }, 11 | { 12 | value: "5rating", 13 | label: "Rate From 1-5", 14 | }, 15 | { 16 | value: "10rating", 17 | label: "Rate From 1-10", 18 | }, 19 | ]; 20 | 21 | export const chartTypes = [ 22 | { 23 | value: "line", 24 | label: "Line", 25 | }, 26 | ]; 27 | 28 | export const statuses = [ 29 | { 30 | value: "active", 31 | label: "Active", 32 | }, 33 | { 34 | value: "inactive", 35 | label: "Inactive", 36 | }, 37 | { 38 | value: "fixed", 39 | label: "Fixed", 40 | }, 41 | ]; 42 | 43 | export const timings = [ 44 | { 45 | value: "daily", 46 | label: "Daily", 47 | }, 48 | { 49 | value: "everytime", 50 | label: "Everytime", 51 | }, 52 | ]; 53 | -------------------------------------------------------------------------------- /src/sections/metrics/metrics-dialog.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useFormik } from "formik"; 4 | import { 5 | Box, 6 | Button, 7 | Card, 8 | CardActions, 9 | CardContent, 10 | CardHeader, 11 | Divider, 12 | TextField, 13 | Unstable_Grid2 as Grid, 14 | FormControlLabel, 15 | Checkbox, 16 | } from "@mui/material"; 17 | import { BootstrapDialog } from "../../components/BootstrapDialog"; 18 | import ConfirmDialog from "../../components/ConfirmModal"; 19 | import { toast } from "react-toastify"; 20 | 21 | export const MetricsDialog = (props) => { 22 | const { onClose, open, timings, fieldTypes, chartTypes, statuses } = props; 23 | const [openDialog, setOpenDialog] = React.useState(false); 24 | 25 | const formik = useFormik({ 26 | initialValues: props.initialValues, 27 | 28 | onSubmit: (values) => { 29 | if (values.name === "") { 30 | toast.error("You must input Metric name!"); 31 | } else { 32 | if (values._id) { 33 | setOpenDialog(true); 34 | } else { 35 | props.onAddMetric(values); 36 | props.onClose(); 37 | } 38 | } 39 | }, 40 | }); 41 | 42 | useEffect(() => { 43 | props.initialValues && formik.setValues(props.initialValues); 44 | // eslint-disable-next-line 45 | }, [props.initialValues]); // eslint-disable-next-line 46 | 47 | return ( 48 | 54 |
    55 | 56 | 60 | 61 | 62 | 63 | 64 | 72 | 73 | 74 | 82 | 83 | 84 | 94 | {fieldTypes && 95 | fieldTypes.map((option) => ( 96 | 99 | ))} 100 | 101 | 102 | 103 | 111 | 112 | 113 | 121 | 122 | 123 | 133 | {chartTypes && 134 | chartTypes.map((option) => ( 135 | 138 | ))} 139 | 140 | 141 | 142 | 152 | {statuses && 153 | statuses.map((option) => ( 154 | 157 | ))} 158 | 159 | 160 | 161 | 171 | {timings && 172 | timings.map((option) => ( 173 | 176 | ))} 177 | 178 | 179 | {formik.values.fieldType !== "text" ? ( 180 | 181 | 187 | } 188 | onChange={formik.handleChange} 189 | label="Ignoring zeros to show charts?" 190 | style={{ userSelect: "none" }} 191 | /> 192 | 193 | ) : null} 194 | 195 | 196 | 197 | 198 | 199 | 202 | 203 | 206 | 207 | 208 | setOpenDialog(false)} 213 | onOK={(e) => { 214 | props.onUpdateMetric(formik.values); 215 | setOpenDialog(false); 216 | props.onClose(); 217 | }} 218 | /> 219 | 220 |
    221 | ); 222 | }; 223 | 224 | MetricsDialog.propTypes = { 225 | onClose: PropTypes.func, 226 | open: PropTypes.bool.isRequired, 227 | }; 228 | -------------------------------------------------------------------------------- /src/sections/metrics/metrics-search.js: -------------------------------------------------------------------------------- 1 | import MagnifyingGlassIcon from "@heroicons/react/24/solid/MagnifyingGlassIcon"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { InputAdornment, OutlinedInput, SvgIcon } from "@mui/material"; 5 | 6 | export const MetricsSearch = ({ search, setSearch }) => ( 7 |
    8 | { 13 | setSearch(e.target.value); 14 | }} 15 | startAdornment={ 16 | 17 | 18 | 19 | 20 | 21 | } 22 | /> 23 |
    24 | ); 25 | MetricsSearch.prototype = { 26 | search: PropTypes.object.isRequired, 27 | setSearch: PropTypes.func.isRequired, 28 | }; 29 | -------------------------------------------------------------------------------- /src/sections/metrics/metrics-table.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Box, 5 | Card, 6 | IconButton, 7 | Stack, 8 | Table, 9 | TableBody, 10 | TableCell, 11 | TableHead, 12 | TableRow, 13 | SvgIcon, 14 | Tooltip, 15 | Typography, 16 | } from "@mui/material"; 17 | import { Scrollbar } from "../../components/scrollbar"; 18 | import { SeverityPill } from "../../components/severity-pill"; 19 | import { PencilIcon, TrashIcon } from "@heroicons/react/24/solid"; 20 | import ConfirmDialog from "../../components/ConfirmModal"; 21 | import { apiDeleteMetricById } from "../../actions/metrics"; 22 | import { useDispatch } from "react-redux"; 23 | const statusMap = { 24 | active: "success", 25 | inactive: "warning", 26 | fixed: "error", 27 | }; 28 | 29 | export const MetricsTable = (props) => { 30 | const { 31 | metrics, 32 | 33 | selected = [], 34 | deletedId, 35 | setDeletedId, 36 | setEditedMetric, 37 | openMetricModal, 38 | } = props; 39 | const dispatch = useDispatch(); 40 | const [openDialog, setOpenDialog] = React.useState(false); 41 | 42 | 43 | const onOK = () => { 44 | deletedId && dispatch(apiDeleteMetricById(deletedId)); 45 | setDeletedId(""); 46 | setOpenDialog(false); 47 | }; 48 | const onCancel = () => { 49 | setOpenDialog(false); 50 | }; 51 | 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | No 60 | Name 61 | Description 62 | Chart Type 63 | Timing 64 | Status 65 | 66 | 67 | 68 | 69 | {metrics?.map((metric, index) => { 70 | const isSelected = selected.includes(metric._id); 71 | 72 | return ( 73 | 74 | {index + 1} 75 | 76 | 77 | 78 | {metric.name} 79 | 80 | 81 | 82 | {metric.description} 83 | {metric.chartType} 84 | {metric.timing} 85 | 86 | 87 | {metric.status} 88 | 89 | 90 | 91 | 92 | { 94 | setEditedMetric(metric); 95 | openMetricModal(); 96 | }} 97 | > 98 | 99 | 100 | 101 | 102 | 103 | 104 | { 106 | setDeletedId(metric._id); 107 | setOpenDialog(true); 108 | }} 109 | > 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | ); 118 | })} 119 | 120 |
    121 |
    122 |
    123 | 130 |
    131 | ); 132 | }; 133 | 134 | MetricsTable.propTypes = { 135 | metrics: PropTypes.array, 136 | onDeselectAll: PropTypes.func, 137 | onDeselectOne: PropTypes.func, 138 | onSelectAll: PropTypes.func, 139 | onSelectOne: PropTypes.func, 140 | selected: PropTypes.array, 141 | }; 142 | -------------------------------------------------------------------------------- /src/sections/overview/overview-latest-metrics-value.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDispatch } from "react-redux"; 4 | import { useNavigate } from "react-router-dom"; 5 | import { formatDistanceToNow } from "date-fns"; 6 | import ArrowRightIcon from "@heroicons/react/24/solid/ArrowRightIcon"; 7 | import EllipsisVerticalIcon from "@heroicons/react/24/solid/EllipsisVerticalIcon"; 8 | import { 9 | Menu, 10 | Card, 11 | List, 12 | Avatar, 13 | Button, 14 | SvgIcon, 15 | Divider, 16 | ListItem, 17 | MenuItem, 18 | CardHeader, 19 | IconButton, 20 | CardActions, 21 | ListItemText, 22 | ListItemAvatar, 23 | } from "@mui/material"; 24 | import ClockIcon from "@heroicons/react/24/solid/ClockIcon"; 25 | import { apiDeleteMetricWageById } from "../../actions/metrics"; 26 | import ConfirmDialog from "../../components/ConfirmModal"; 27 | 28 | export const OverviewLatestMetricsValue = (props) => { 29 | const dispatch = useDispatch(); 30 | const navigate = useNavigate(); 31 | const { lastestWages = [], metrics = [], sx } = props; 32 | const [deletedMetricWageId, setDeleteMetricWageId] = React.useState(""); 33 | const [anchorEl, setAnchorEl] = React.useState(null); 34 | const [openDialog, setOpenDialog] = React.useState(false); 35 | const open = Boolean(anchorEl); 36 | const handleClick = (event, id) => { 37 | setDeleteMetricWageId(id); 38 | setAnchorEl(event.currentTarget); 39 | }; 40 | const handleClose = () => { 41 | setAnchorEl(null); 42 | }; 43 | const onOK = () => { 44 | console.log(deletedMetricWageId); 45 | dispatch(apiDeleteMetricWageById(deletedMetricWageId)); 46 | setOpenDialog(false); 47 | handleClose(); 48 | }; 49 | return ( 50 | <> 51 | 52 | 53 | 54 | {lastestWages && 55 | lastestWages.map((wage, index) => { 56 | const hasDivider = index < lastestWages.length - 1; 57 | const ago = formatDistanceToNow( 58 | new Date(wage.updatedAt).getTime() 59 | ); 60 | 61 | return ( 62 | 63 | 64 | 71 | 72 | 73 | 74 | 75 | 76 | metric._id === wage.metricsId 80 | )[0] 81 | ? `${ 82 | metrics?.filter( 83 | (metric) => metric._id === wage.metricsId 84 | )[0]?.name 85 | }` 86 | : "" 87 | } 88 | primaryTypographyProps={{ variant: "subtitle1" }} 89 | secondary={`Updated ${ago} ago`} 90 | secondaryTypographyProps={{ variant: "body2" }} 91 | /> 92 | handleClick(e, wage._id)} 95 | > 96 | 97 | 98 | 99 | 100 | 109 | { 111 | // setDeleteMetricWageId(wage._id); 112 | setOpenDialog(true); 113 | }} 114 | > 115 | Delete 116 | 117 | {/* My account 118 | Logout */} 119 | 120 | 121 | ); 122 | })} 123 | 124 | 125 | 126 | 141 | 142 | 143 | { 147 | setOpenDialog(false); 148 | }} 149 | onOK={onOK} 150 | openDialog={openDialog} 151 | /> 152 | 153 | ); 154 | }; 155 | 156 | OverviewLatestMetricsValue.propTypes = { 157 | products: PropTypes.array, 158 | sx: PropTypes.object, 159 | }; 160 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/store/authSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | const initialState = { 3 | token: localStorage.getItem("token"), 4 | isAuthenticated: null, 5 | loading: true, 6 | user: {}, 7 | }; 8 | 9 | export const authReducer = createSlice({ 10 | name: "auth", 11 | initialState: initialState, 12 | reducers: { 13 | userLogined: (state, action) => { 14 | state.isAuthenticated = true; 15 | state.loading = false; 16 | state.user = action.payload; 17 | }, 18 | userRegister: (state) => { 19 | state.isAuthenticated = true; 20 | state.loading = false; 21 | }, 22 | userLoginSuccess: (state, action) => { 23 | state.isAuthenticated = true; 24 | state.loading = false; 25 | state.token = action.payload; 26 | }, 27 | userLogOut: (state) => { 28 | state.token = null; 29 | state.isAuthenticated = false; 30 | state.loading = false; 31 | state.user = null; 32 | }, 33 | userAuthError: (state, action) => { 34 | state.token = null; 35 | }, 36 | }, 37 | }); 38 | 39 | // Action creators are generated for each case reducer function 40 | export const { 41 | userLogined, 42 | userLoginSuccess, 43 | userLogOut, 44 | userRegister, 45 | userAuthError, 46 | } = authReducer.actions; 47 | 48 | export default authReducer.reducer; 49 | -------------------------------------------------------------------------------- /src/store/groupSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | groups: [{ name: "", userId: "", contents: [] }], 5 | loading: true, 6 | }; 7 | 8 | export const groupSlice = createSlice({ 9 | name: "auth", 10 | initialState: initialState, 11 | reducers: { 12 | addGroup: (state, action) => { 13 | state.loading = false; 14 | state.groups = [...state.groups, action.payload]; 15 | }, 16 | getGroups: (state, action) => { 17 | if (action.payload) { 18 | state.groups = action.payload; 19 | } else { 20 | state.groups = []; 21 | } 22 | }, 23 | deleteGroup: (state, action) => { 24 | state.groups = state.groups.filter( 25 | (group) => group._id !== action.payload._id 26 | ); 27 | state.loading = false; 28 | }, 29 | updateGroup: (state, action) => { 30 | state.loading = false; 31 | state.groups = 32 | state.groups && 33 | state.groups.map((group) => 34 | group._id === action.payload._id ? action.payload : group 35 | ); 36 | }, 37 | groupError: (state) => { 38 | state.loading = false; 39 | }, 40 | }, 41 | }); 42 | 43 | // Action creators are generated for each case reducer function 44 | export const { addGroup, getGroups, deleteGroup, updateGroup, groupError } = 45 | groupSlice.actions; 46 | 47 | export default groupSlice.reducer; 48 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | 3 | import authSlice from "./authSlice"; 4 | import metricsSlice from "./metricsSlice"; 5 | import groupSlice from "./groupSlice"; 6 | import { setAuthToken } from "../utils"; 7 | // import kabanSlice from "./kabanSlice"; 8 | const store = configureStore({ 9 | reducer: { 10 | auth: authSlice, 11 | metrics: metricsSlice, 12 | group: groupSlice, 13 | }, 14 | }); 15 | 16 | export default store; 17 | 18 | let currentState = store.getState(); 19 | 20 | store.subscribe(() => { 21 | // keep track of the previous and current state to compare changes 22 | let previousState = currentState; 23 | currentState = store.getState(); 24 | // if the token changes set the value in localStorage and axios headers 25 | if (previousState.auth.token !== currentState.auth.token) { 26 | const token = currentState.auth.token; 27 | setAuthToken(token); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /src/store/metricsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | metrics: [], 5 | loading: true, 6 | wages: [], 7 | todayWages: [], 8 | lastestWages: [], 9 | }; 10 | 11 | export const metricsSlice = createSlice({ 12 | name: "auth", 13 | initialState: initialState, 14 | reducers: { 15 | addMetric: (state, action) => { 16 | state.loading = false; 17 | 18 | state.metrics = action.payload 19 | ? [...state.metrics, action.payload] 20 | : state.metrics; 21 | }, 22 | 23 | updateMetric: (state, action) => { 24 | state.loading = false; 25 | state.metrics = state.metrics?.map((metric) => 26 | metric._id === action.payload._id ? action.payload : metric 27 | ); 28 | }, 29 | 30 | getUserMetrics: (state, action) => { 31 | state.metrics = action.payload ? action.payload : []; 32 | state.loading = false; 33 | }, 34 | 35 | deleteMetric: (state, action) => { 36 | state.metrics = state.metrics?.filter( 37 | (metric) => metric._id !== action.payload._id 38 | ); 39 | 40 | state.loading = false; 41 | }, 42 | 43 | addMetricWage: (state, action) => { 44 | state.wages = [...state.wages, action.payload]; 45 | state.todayWages = [...state.todayWages, action.payload]; 46 | state.lastestWages.unshift(action.payload); 47 | state.lastestWages.length > 3 && state.lastestWages.pop(); 48 | }, 49 | 50 | getMetricsAllWages: (state, action) => { 51 | state.wages = action.payload ? action.payload : []; 52 | }, 53 | getMetricsTodayWages: (state, action) => { 54 | state.todayWages = action.payload ? action.payload : []; 55 | }, 56 | getMetricsLastestWages: (state, action) => { 57 | state.lastestWages = action.payload ? action.payload : []; 58 | }, 59 | deleteMetricWage: (state, action) => { 60 | state.wages = state.wages?.filter( 61 | (value) => value._id !== action.payload._id 62 | ); 63 | state.todayWages = state.todayWages?.filter( 64 | (value) => value._id !== action.payload._id 65 | ); 66 | state.lastestWages = state.lastestWages?.filter( 67 | (value) => value._id !== action.payload._id 68 | ); 69 | state.loading = false; 70 | }, 71 | updateMetricsWage: (state, action) => { 72 | state.loading = false; 73 | state.wages = state.wages?.map((metric) => 74 | metric._id === action.payload._id ? action.payload : metric 75 | ); 76 | state.todayWages = state.todayWages?.map((wage) => 77 | wage._id === action.payload._id ? action.payload : wage 78 | ); 79 | }, 80 | metricsError: (state) => { 81 | state.loading = false; 82 | }, 83 | }, 84 | }); 85 | 86 | // Action creators are generated for each case reducer function 87 | export const { 88 | addMetric, 89 | getUserMetrics, 90 | metricsError, 91 | deleteMetric, 92 | updateMetric, 93 | addMetricWage, 94 | deleteMetricWage, 95 | updateMetricsWage, 96 | getMetricsAllWages, 97 | getMetricsTodayWages, 98 | getMetricsLastestWages, 99 | } = metricsSlice.actions; 100 | 101 | export default metricsSlice.reducer; 102 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | #root { 2 | --toastify-font-family: "Poppins", sans-serif; 3 | display: flex; 4 | flex: 1 1 auto; 5 | flex-direction: column; 6 | height: 100%; 7 | width: 100%; 8 | font-family: "Roboto", "Helvetica", "Arial", "sans-serif" !important; 9 | } 10 | 11 | ::-webkit-scrollbar { 12 | width: 6px; 13 | height: 6px; 14 | } 15 | 16 | ::-webkit-scrollbar-thumb { 17 | background-color: #0003; 18 | border-radius: 6px; 19 | transition: all 0.2s ease-in-out; 20 | } 21 | ::-webkit-scrollbar-track { 22 | border-radius: 6px; 23 | } 24 | p { 25 | font-family: "Roboto", "Helvetica", "Arial", "sans-serif" !important; 26 | } 27 | -------------------------------------------------------------------------------- /src/theme/colors.js: -------------------------------------------------------------------------------- 1 | import { alpha } from "@mui/material/styles"; 2 | 3 | const withAlphas = (color) => { 4 | return { 5 | ...color, 6 | alpha4: alpha(color.main, 0.04), 7 | alpha8: alpha(color.main, 0.08), 8 | alpha12: alpha(color.main, 0.12), 9 | alpha30: alpha(color.main, 0.3), 10 | alpha50: alpha(color.main, 0.5), 11 | }; 12 | }; 13 | 14 | export const neutral = { 15 | 50: "#F8F9FA", 16 | 100: "#F3F4F6", 17 | 200: "#E5E7EB", 18 | 300: "#D2D6DB", 19 | 400: "#9DA4AE", 20 | 500: "#6C737F", 21 | 600: "#4D5761", 22 | 700: "#2F3746", 23 | 800: "#1C2536", 24 | 900: "#111927", 25 | }; 26 | 27 | export const indigo = withAlphas({ 28 | lightest: "#F5F7FF", 29 | light: "#EBEEFE", 30 | main: "#6366F1", 31 | dark: "#4338CA", 32 | darkest: "#312E81", 33 | contrastText: "#FFFFFF", 34 | }); 35 | 36 | export const success = withAlphas({ 37 | lightest: "#F0FDF9", 38 | light: "#3FC79A", 39 | main: "#10B981", 40 | dark: "#0B815A", 41 | darkest: "#134E48", 42 | contrastText: "#FFFFFF", 43 | }); 44 | 45 | export const info = withAlphas({ 46 | lightest: "#ECFDFF", 47 | light: "#CFF9FE", 48 | main: "#06AED4", 49 | dark: "#0E7090", 50 | darkest: "#164C63", 51 | contrastText: "#FFFFFF", 52 | }); 53 | 54 | export const warning = withAlphas({ 55 | lightest: "#FFFAEB", 56 | light: "#FEF0C7", 57 | main: "#F79009", 58 | dark: "#B54708", 59 | darkest: "#7A2E0E", 60 | contrastText: "#FFFFFF", 61 | }); 62 | 63 | export const error = withAlphas({ 64 | lightest: "#FEF3F2", 65 | light: "#FEE4E2", 66 | main: "#F04438", 67 | dark: "#B42318", 68 | darkest: "#7A271A", 69 | contrastText: "#FFFFFF", 70 | }); 71 | -------------------------------------------------------------------------------- /src/theme/create-components.js: -------------------------------------------------------------------------------- 1 | import { 2 | createTheme, 3 | filledInputClasses, 4 | inputLabelClasses, 5 | outlinedInputClasses, 6 | paperClasses, 7 | tableCellClasses, 8 | } from "@mui/material"; 9 | 10 | // Used only to create transitions 11 | const muiTheme = createTheme(); 12 | 13 | export function createComponents(config) { 14 | const { palette } = config; 15 | 16 | return { 17 | MuiAvatar: { 18 | styleOverrides: { 19 | root: { 20 | fontSize: 14, 21 | fontWeight: 600, 22 | letterSpacing: 0, 23 | }, 24 | }, 25 | }, 26 | MuiButton: { 27 | styleOverrides: { 28 | root: { 29 | borderRadius: "12px", 30 | textTransform: "none", 31 | }, 32 | sizeSmall: { 33 | padding: "6px 16px", 34 | }, 35 | sizeMedium: { 36 | padding: "8px 20px", 37 | }, 38 | sizeLarge: { 39 | padding: "11px 24px", 40 | }, 41 | textSizeSmall: { 42 | padding: "7px 12px", 43 | }, 44 | textSizeMedium: { 45 | padding: "9px 16px", 46 | }, 47 | textSizeLarge: { 48 | padding: "12px 16px", 49 | }, 50 | }, 51 | }, 52 | MuiCard: { 53 | styleOverrides: { 54 | root: { 55 | [`&.${paperClasses.elevation1}`]: { 56 | boxShadow: 57 | "0px 5px 22px rgba(0, 0, 0, 0.04), 0px 0px 0px 0.5px rgba(0, 0, 0, 0.03)", 58 | }, 59 | }, 60 | }, 61 | }, 62 | MuiCardContent: { 63 | styleOverrides: { 64 | root: { 65 | padding: "32px 24px", 66 | "&:last-child": { 67 | paddingBottom: "32px", 68 | }, 69 | }, 70 | }, 71 | }, 72 | MuiCardHeader: { 73 | defaultProps: { 74 | titleTypographyProps: { 75 | variant: "h6", 76 | }, 77 | subheaderTypographyProps: { 78 | variant: "body2", 79 | }, 80 | }, 81 | styleOverrides: { 82 | root: { 83 | padding: "32px 24px 16px", 84 | }, 85 | }, 86 | }, 87 | MuiCssBaseline: { 88 | styleOverrides: { 89 | "*": { 90 | boxSizing: "border-box", 91 | }, 92 | html: { 93 | MozOsxFontSmoothing: "grayscale", 94 | WebkitFontSmoothing: "antialiased", 95 | display: "flex", 96 | flexDirection: "column", 97 | minHeight: "100%", 98 | width: "100%", 99 | }, 100 | body: { 101 | display: "flex", 102 | flex: "1 1 auto", 103 | flexDirection: "column", 104 | minHeight: "100%", 105 | width: "100%", 106 | }, 107 | "#__next": { 108 | display: "flex", 109 | flex: "1 1 auto", 110 | flexDirection: "column", 111 | height: "100%", 112 | width: "100%", 113 | }, 114 | "#nprogress": { 115 | pointerEvents: "none", 116 | }, 117 | "#nprogress .bar": { 118 | backgroundColor: palette.primary.main, 119 | height: 3, 120 | left: 0, 121 | position: "fixed", 122 | top: 0, 123 | width: "100%", 124 | zIndex: 2000, 125 | }, 126 | }, 127 | }, 128 | MuiInputBase: { 129 | styleOverrides: { 130 | input: { 131 | "&::placeholder": { 132 | opacity: 1, 133 | }, 134 | }, 135 | }, 136 | }, 137 | MuiInput: { 138 | styleOverrides: { 139 | input: { 140 | fontSize: 14, 141 | fontWeight: 500, 142 | lineHeight: "24px", 143 | "&::placeholder": { 144 | color: palette.text.secondary, 145 | }, 146 | }, 147 | }, 148 | }, 149 | MuiFilledInput: { 150 | styleOverrides: { 151 | root: { 152 | backgroundColor: "transparent", 153 | borderRadius: 8, 154 | borderStyle: "solid", 155 | borderWidth: 1, 156 | overflow: "hidden", 157 | borderColor: palette.neutral[200], 158 | transition: muiTheme.transitions.create([ 159 | "border-color", 160 | "box-shadow", 161 | ]), 162 | "&:hover": { 163 | backgroundColor: palette.action.hover, 164 | }, 165 | "&:before": { 166 | display: "none", 167 | }, 168 | "&:after": { 169 | display: "none", 170 | }, 171 | [`&.${filledInputClasses.disabled}`]: { 172 | backgroundColor: "transparent", 173 | }, 174 | [`&.${filledInputClasses.focused}`]: { 175 | backgroundColor: "transparent", 176 | borderColor: palette.primary.main, 177 | boxShadow: `${palette.primary.main} 0 0 0 2px`, 178 | }, 179 | [`&.${filledInputClasses.error}`]: { 180 | borderColor: palette.error.main, 181 | boxShadow: `${palette.error.main} 0 0 0 2px`, 182 | }, 183 | }, 184 | input: { 185 | fontSize: 14, 186 | fontWeight: 500, 187 | lineHeight: "24px", 188 | }, 189 | }, 190 | }, 191 | MuiOutlinedInput: { 192 | styleOverrides: { 193 | root: { 194 | "&:hover": { 195 | backgroundColor: palette.action.hover, 196 | [`& .${outlinedInputClasses.notchedOutline}`]: { 197 | borderColor: palette.neutral[200], 198 | }, 199 | }, 200 | [`&.${outlinedInputClasses.focused}`]: { 201 | backgroundColor: "transparent", 202 | [`& .${outlinedInputClasses.notchedOutline}`]: { 203 | borderColor: palette.primary.main, 204 | boxShadow: `${palette.primary.main} 0 0 0 2px`, 205 | }, 206 | }, 207 | [`&.${filledInputClasses.error}`]: { 208 | [`& .${outlinedInputClasses.notchedOutline}`]: { 209 | borderColor: palette.error.main, 210 | boxShadow: `${palette.error.main} 0 0 0 2px`, 211 | }, 212 | }, 213 | }, 214 | input: { 215 | fontSize: 14, 216 | fontWeight: 500, 217 | lineHeight: "24px", 218 | }, 219 | notchedOutline: { 220 | borderColor: palette.neutral[200], 221 | transition: muiTheme.transitions.create([ 222 | "border-color", 223 | "box-shadow", 224 | ]), 225 | }, 226 | }, 227 | }, 228 | MuiFormLabel: { 229 | styleOverrides: { 230 | root: { 231 | fontSize: 14, 232 | fontWeight: 500, 233 | [`&.${inputLabelClasses.filled}`]: { 234 | transform: "translate(12px, 18px) scale(1)", 235 | }, 236 | [`&.${inputLabelClasses.shrink}`]: { 237 | [`&.${inputLabelClasses.standard}`]: { 238 | transform: "translate(0, -1.5px) scale(0.85)", 239 | }, 240 | [`&.${inputLabelClasses.filled}`]: { 241 | transform: "translate(12px, 6px) scale(0.85)", 242 | }, 243 | [`&.${inputLabelClasses.outlined}`]: { 244 | transform: "translate(14px, -9px) scale(0.85)", 245 | }, 246 | }, 247 | }, 248 | }, 249 | }, 250 | MuiTab: { 251 | styleOverrides: { 252 | root: { 253 | fontSize: 14, 254 | fontWeight: 500, 255 | lineHeight: 1.71, 256 | minWidth: "auto", 257 | paddingLeft: 0, 258 | paddingRight: 0, 259 | textTransform: "none", 260 | "& + &": { 261 | marginLeft: 24, 262 | }, 263 | }, 264 | }, 265 | }, 266 | MuiTableCell: { 267 | styleOverrides: { 268 | root: { 269 | borderBottomColor: palette.divider, 270 | padding: "15px 16px", 271 | borderBottom: "solid 2px #e7ebf0", 272 | }, 273 | }, 274 | }, 275 | MuiTableHead: { 276 | styleOverrides: { 277 | root: { 278 | borderBottom: "none", 279 | [`& .${tableCellClasses.root}`]: { 280 | borderBottom: "none", 281 | backgroundColor: "#e2e2e2", 282 | color: palette.neutral[700], 283 | fontSize: 12, 284 | fontWeight: 600, 285 | lineHeight: 1, 286 | letterSpacing: 0.5, 287 | textTransform: "uppercase", 288 | }, 289 | [`& .${tableCellClasses.paddingCheckbox}`]: { 290 | paddingTop: 4, 291 | paddingBottom: 4, 292 | }, 293 | }, 294 | }, 295 | }, 296 | MuiTextField: { 297 | defaultProps: { 298 | variant: "filled", 299 | }, 300 | }, 301 | }; 302 | } 303 | -------------------------------------------------------------------------------- /src/theme/create-palette.js: -------------------------------------------------------------------------------- 1 | import { common } from "@mui/material/colors"; 2 | import { alpha } from "@mui/material/styles"; 3 | import { error, indigo, info, neutral, success, warning } from "./colors"; 4 | 5 | export function createPalette() { 6 | return { 7 | action: { 8 | active: neutral[500], 9 | disabled: alpha(neutral[900], 0.38), 10 | disabledBackground: alpha(neutral[900], 0.12), 11 | focus: alpha(neutral[900], 0.16), 12 | hover: alpha(neutral[900], 0.04), 13 | selected: alpha(neutral[900], 0.12), 14 | }, 15 | background: { 16 | default: common.white, 17 | paper: common.white, 18 | }, 19 | divider: "#e7ebf0", 20 | error, 21 | info, 22 | mode: "light", 23 | neutral, 24 | primary: indigo, 25 | secondary: info, 26 | success, 27 | text: { 28 | primary: neutral[900], 29 | secondary: neutral[500], 30 | disabled: alpha(neutral[900], 0.38), 31 | }, 32 | warning, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/theme/create-shadows.js: -------------------------------------------------------------------------------- 1 | export const createShadows = () => { 2 | return [ 3 | 'none', 4 | '0px 1px 2px rgba(0, 0, 0, 0.08)', 5 | '0px 1px 5px rgba(0, 0, 0, 0.08)', 6 | '0px 1px 8px rgba(0, 0, 0, 0.08)', 7 | '0px 1px 10px rgba(0, 0, 0, 0.08)', 8 | '0px 1px 14px rgba(0, 0, 0, 0.08)', 9 | '0px 1px 18px rgba(0, 0, 0, 0.08)', 10 | '0px 2px 16px rgba(0, 0, 0, 0.08)', 11 | '0px 3px 14px rgba(0, 0, 0, 0.08)', 12 | '0px 3px 16px rgba(0, 0, 0, 0.08)', 13 | '0px 4px 18px rgba(0, 0, 0, 0.08)', 14 | '0px 4px 20px rgba(0, 0, 0, 0.08)', 15 | '0px 5px 22px rgba(0, 0, 0, 0.08)', 16 | '0px 5px 24px rgba(0, 0, 0, 0.08)', 17 | '0px 5px 26px rgba(0, 0, 0, 0.08)', 18 | '0px 6px 28px rgba(0, 0, 0, 0.08)', 19 | '0px 6px 30px rgba(0, 0, 0, 0.08)', 20 | '0px 6px 32px rgba(0, 0, 0, 0.08)', 21 | '0px 7px 34px rgba(0, 0, 0, 0.08)', 22 | '0px 7px 36px rgba(0, 0, 0, 0.08)', 23 | '0px 8px 38px rgba(0, 0, 0, 0.08)', 24 | '0px 8px 40px rgba(0, 0, 0, 0.08)', 25 | '0px 8px 42px rgba(0, 0, 0, 0.08)', 26 | '0px 9px 44px rgba(0, 0, 0, 0.08)', 27 | '0px 9px 46px rgba(0, 0, 0, 0.08)' 28 | ]; 29 | }; 30 | -------------------------------------------------------------------------------- /src/theme/create-typography.js: -------------------------------------------------------------------------------- 1 | import "@fontsource/roboto/300.css"; 2 | import "@fontsource/roboto/400.css"; 3 | import "@fontsource/roboto/500.css"; 4 | import "@fontsource/roboto/700.css"; 5 | export const createTypography = () => { 6 | return { 7 | fontFamily: "'Roboto', 'Helvetica', 'Arial', 'sans-serif'", 8 | body1: { 9 | fontSize: "1rem", 10 | fontWeight: 400, 11 | lineHeight: 1.5, 12 | }, 13 | body2: { 14 | fontSize: "0.875rem", 15 | fontWeight: 400, 16 | lineHeight: 1.57, 17 | }, 18 | button: { 19 | fontWeight: 600, 20 | }, 21 | caption: { 22 | fontSize: "0.75rem", 23 | fontWeight: 500, 24 | lineHeight: 1.66, 25 | }, 26 | subtitle1: { 27 | fontSize: "1rem", 28 | fontWeight: 500, 29 | lineHeight: 1.57, 30 | }, 31 | subtitle2: { 32 | fontSize: "0.875rem", 33 | fontWeight: 500, 34 | lineHeight: 1.57, 35 | }, 36 | overline: { 37 | fontSize: "0.75rem", 38 | fontWeight: 500, 39 | letterSpacing: "0.5px", 40 | lineHeight: 2.5, 41 | textTransform: "uppercase", 42 | }, 43 | h1: { 44 | fontWeight: 500, 45 | fontSize: 35, 46 | letterSpacing: "-0.24px", 47 | }, 48 | h2: { 49 | fontWeight: 500, 50 | fontSize: 29, 51 | letterSpacing: "-0.24px", 52 | }, 53 | h3: { 54 | fontWeight: 500, 55 | fontSize: 24, 56 | letterSpacing: "-0.06px", 57 | }, 58 | h4: { 59 | fontWeight: 500, 60 | fontSize: 20, 61 | letterSpacing: "-0.06px", 62 | }, 63 | h5: { 64 | fontWeight: 500, 65 | fontSize: 16, 66 | letterSpacing: "-0.05px", 67 | }, 68 | h6: { 69 | fontWeight: 500, 70 | fontSize: 14, 71 | letterSpacing: "-0.05px", 72 | }, 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /src/theme/index.js: -------------------------------------------------------------------------------- 1 | import { createTheme as createMuiTheme } from "@mui/material"; 2 | import { createPalette } from "./create-palette"; 3 | import { createComponents } from "./create-components"; 4 | import { createShadows } from "./create-shadows"; 5 | import { createTypography } from "./create-typography"; 6 | 7 | export function createTheme() { 8 | const palette = createPalette(); 9 | const components = createComponents({ palette }); 10 | const shadows = createShadows(); 11 | const typography = createTypography(); 12 | 13 | return createMuiTheme({ 14 | breakpoints: { 15 | values: { 16 | xs: 0, 17 | sm: 600, 18 | md: 900, 19 | lg: 1200, 20 | xl: 1440, 21 | }, 22 | }, 23 | components, 24 | palette, 25 | shadows, 26 | shape: { 27 | borderRadius: 8, 28 | }, 29 | typography, 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import store from "../store"; 3 | import { userLogOut } from "../store/authSlice"; 4 | 5 | // const isDev = process.env.NODE_ENV === "development"; 6 | export const MODEL_API_URL = 7 | window.location.hostname === "localhost" || 8 | window.location.hostname === "127.0.0.1" 9 | ? "http://127.0.0.1:8000/api/" 10 | : "https://smart-metrics-logbook.herokuapp.com/api/"; 11 | 12 | // Create an instance of axios 13 | const api = axios.create({ 14 | baseURL: MODEL_API_URL, 15 | headers: { 16 | Accept: "application/json", 17 | }, 18 | }); 19 | /* 20 | NOTE: intercept any error responses from the api 21 | and check if the token is no longer valid. 22 | ie. Token has expired or user is no longer 23 | authenticated. 24 | logout the user if the token has expired 25 | */ 26 | 27 | api.interceptors.response.use( 28 | (res) => res, 29 | (err) => { 30 | if (err.response.status === 401) { 31 | store.dispatch(userLogOut()); 32 | } 33 | return Promise.reject(err); 34 | } 35 | ); 36 | 37 | export default api; 38 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as api } from "./api"; 2 | export { default as setAuthToken } from "./setAuthToken"; 3 | export { default as toast } from "./toast"; 4 | -------------------------------------------------------------------------------- /src/utils/setAuthToken.js: -------------------------------------------------------------------------------- 1 | import api from "./api"; 2 | 3 | const setAuthToken = (token) => { 4 | if (token) { 5 | api.defaults.headers.common["x-auth-token"] = token; 6 | localStorage.setItem("smart-metrics-logbook", token); 7 | } else { 8 | delete api.defaults.headers["x-auth-token"]; 9 | localStorage.removeItem("smart-metrics-logbook"); 10 | } 11 | }; 12 | 13 | export default setAuthToken; 14 | -------------------------------------------------------------------------------- /src/utils/toast.js: -------------------------------------------------------------------------------- 1 | import { toast } from "react-toastify"; 2 | 3 | const options = { 4 | autoClose: 5000, 5 | hideProgressBar: false, 6 | position: toast.POSITION.TOP_RIGHT, 7 | pauseOnHover: true, 8 | }; 9 | 10 | const myToast = { 11 | success: (message) => { 12 | toast.success(message, options); 13 | }, 14 | info: (message) => { 15 | toast.info(message, options); 16 | }, 17 | error: (message) => { 18 | toast.error(message, options); 19 | }, 20 | warning: (message) => { 21 | toast.warn(message, options); 22 | }, 23 | }; 24 | 25 | export default myToast; 26 | --------------------------------------------------------------------------------