Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a7628f00a3 | ||
|
9856768d11 | ||
|
049e2fb57e | ||
|
6c8bbc6e3f | ||
|
8f72bfdb23 | ||
|
d3b1fa03fc | ||
|
8191799760 | ||
|
52dfb0522f | ||
|
26c4406401 | ||
|
1a611fbea1 | ||
|
bf835d2a89 | ||
|
c11dd4a8af | ||
|
8160437379 | ||
|
64ab7aef58 | ||
|
693c89c666 | ||
|
81277d0d75 | ||
0a4461bbe4 |
71
README.md
71
README.md
@ -1,70 +1 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `yarn start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
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)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
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)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
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)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
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)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `yarn build` fails to minify
|
||||
|
||||
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)
|
||||
#### FITWAVE - frontend
|
||||
|
@ -11,12 +11,12 @@
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"axios": "^0.21.0",
|
||||
"connected-react-router": "^6.8.0",
|
||||
"date-fns": "^2.9.0",
|
||||
"formik": "^2.2.6",
|
||||
"history": "4.10.1",
|
||||
"immer": "^8.0.0",
|
||||
"joi": "^17.3.0",
|
||||
"lodash": "^4.17.20",
|
||||
"prop-types": "^15.7.2",
|
||||
"quagga": "^0.12.1",
|
||||
@ -31,7 +31,8 @@
|
||||
"redux-saga": "^1.1.3",
|
||||
"reselect": "^4.0.0",
|
||||
"web-vitals": "^0.2.4",
|
||||
"yarn": "^1.22.10"
|
||||
"yarn": "^1.22.10",
|
||||
"yup": "^0.32.8"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
@ -1,4 +1,4 @@
|
||||
<svg width="35" height="23" viewBox="0 0 35 23">
|
||||
<path d="M33.7945 11.4338C34.2146 11.4338 34.516 11.5221 34.6986 11.6985C34.8995 11.875 35 12.1043 35 12.3867C35 12.8454 34.8539 13.2512 34.5616 13.6041C34.2877 13.957 33.8493 14.1423 33.2466 14.16C31.8037 14.1776 30.5069 14.0717 29.3562 13.8423C28.0776 16.4714 26.5342 18.6594 24.726 20.4062C22.9361 22.1354 21.1644 23 19.411 23C17.8037 23 16.5982 22.1795 15.7945 20.5386C14.9909 18.8799 14.5342 16.6655 14.4247 13.8953C13.3288 16.9655 12.0868 19.2505 10.6986 20.7503C9.32877 22.2501 7.86758 23 6.31507 23C4.56164 23 3.23744 21.9501 2.34247 19.8504C1.44749 17.733 1 14.8922 1 11.328C1 8.73418 1.23744 5.88454 1.71233 2.77906C1.84018 1.89682 2.06849 1.28807 2.39726 0.952818C2.74429 0.599922 3.28311 0.423475 4.0137 0.423475C4.56164 0.423475 4.98174 0.538166 5.27397 0.767548C5.58447 0.996931 5.73973 1.42041 5.73973 2.03797C5.73973 2.16149 5.72146 2.39969 5.68493 2.75259C5.13699 6.36977 4.86301 9.44879 4.86301 11.9896C4.86301 14.354 5.07306 16.1803 5.49315 17.4684C5.91324 18.7564 6.47945 19.4005 7.19178 19.4005C7.83105 19.4005 8.59817 18.7564 9.49315 17.4684C10.4064 16.1626 11.3105 14.2394 12.2055 11.6985C13.1005 9.14001 13.8584 6.09628 14.4795 2.56732C14.6256 1.75566 14.8904 1.19985 15.274 0.899884C15.6758 0.582278 16.2146 0.423475 16.8904 0.423475C17.4566 0.423475 17.8676 0.546988 18.1233 0.794015C18.3973 1.0234 18.5342 1.37629 18.5342 1.8527C18.5342 2.13502 18.516 2.35558 18.4795 2.51438C17.968 5.39049 17.7123 8.26659 17.7123 11.1427C17.7123 13.1013 17.7763 14.6628 17.9041 15.8274C18.0502 16.9919 18.3151 17.883 18.6986 18.5006C19.1005 19.1005 19.6758 19.4005 20.4247 19.4005C21.3014 19.4005 22.2785 18.7652 23.3562 17.4948C24.4338 16.2068 25.4201 14.6099 26.3151 12.7043C25.2009 12.0338 24.3607 11.1692 23.7945 10.1105C23.2283 9.03414 22.9452 7.799 22.9452 6.40506C22.9452 5.01112 23.1644 3.83774 23.6027 2.88492C24.0594 1.91446 24.6712 1.19102 25.4384 0.714615C26.2237 0.238205 27.0913 0 28.0411 0C29.21 0 30.1324 0.40583 30.8082 1.21749C31.5023 2.02915 31.8493 3.14077 31.8493 4.55236C31.8493 6.54622 31.4018 8.76064 30.5069 11.1956C31.4384 11.3544 32.5342 11.4338 33.7945 11.4338ZM25.7123 6.21979C25.7123 7.94898 26.2877 9.22823 27.4384 10.0575C28.1507 8.08132 28.5068 6.44918 28.5068 5.1611C28.5068 4.42002 28.4064 3.88186 28.2055 3.54661C28.0046 3.19371 27.7306 3.01726 27.3836 3.01726C26.8904 3.01726 26.4886 3.29958 26.1781 3.86421C25.8676 4.4112 25.7123 5.19639 25.7123 6.21979Z" fill="#411A9E"/>
|
||||
<path d="M32.7945 11.4338C33.2146 11.4338 33.516 11.5221 33.6986 11.6985C33.8995 11.875 34 12.1043 34 12.3867C34 12.8454 33.8539 13.2512 33.5616 13.6041C33.2877 13.957 32.8493 14.1423 32.2466 14.16C30.8037 14.1776 29.5069 14.0717 28.3562 13.8423C27.0776 16.4714 25.5342 18.6594 23.726 20.4062C21.9361 22.1354 20.1644 23 18.411 23C16.8037 23 15.5982 22.1795 14.7945 20.5386C13.9909 18.8799 13.5342 16.6655 13.4247 13.8953C12.3288 16.9655 11.0868 19.2505 9.69863 20.7503C8.32877 22.2501 6.86758 23 5.31507 23C3.56164 23 2.23744 21.9501 1.34247 19.8504C0.447489 17.733 0 14.8922 0 11.328C0 8.73418 0.237443 5.88454 0.712329 2.77906C0.840183 1.89682 1.06849 1.28807 1.39726 0.952818C1.74429 0.599922 2.28311 0.423475 3.0137 0.423475C3.56164 0.423475 3.98174 0.538166 4.27397 0.767548C4.58447 0.996931 4.73973 1.42041 4.73973 2.03797C4.73973 2.16149 4.72146 2.39969 4.68493 2.75259C4.13699 6.36977 3.86301 9.44879 3.86301 11.9896C3.86301 14.354 4.07306 16.1803 4.49315 17.4684C4.91324 18.7564 5.47945 19.4005 6.19178 19.4005C6.83105 19.4005 7.59817 18.7564 8.49315 17.4684C9.40639 16.1626 10.3105 14.2394 11.2055 11.6985C12.1005 9.14001 12.8584 6.09628 13.4795 2.56732C13.6256 1.75566 13.8904 1.19985 14.274 0.899884C14.6758 0.582278 15.2146 0.423475 15.8904 0.423475C16.4566 0.423475 16.8676 0.546988 17.1233 0.794015C17.3973 1.0234 17.5342 1.37629 17.5342 1.8527C17.5342 2.13502 17.516 2.35558 17.4795 2.51438C16.968 5.39049 16.7123 8.26659 16.7123 11.1427C16.7123 13.1013 16.7763 14.6628 16.9041 15.8274C17.0502 16.9919 17.3151 17.883 17.6986 18.5006C18.1005 19.1005 18.6758 19.4005 19.4247 19.4005C20.3014 19.4005 21.2785 18.7652 22.3562 17.4948C23.4338 16.2068 24.4201 14.6099 25.3151 12.7043C24.2009 12.0338 23.3607 11.1692 22.7945 10.1105C22.2283 9.03414 21.9452 7.799 21.9452 6.40506C21.9452 5.01112 22.1644 3.83774 22.6027 2.88492C23.0594 1.91446 23.6712 1.19102 24.4384 0.714615C25.2237 0.238205 26.0913 0 27.0411 0C28.21 0 29.1324 0.40583 29.8082 1.21749C30.5023 2.02915 30.8493 3.14077 30.8493 4.55236C30.8493 6.54622 30.4018 8.76064 29.5069 11.1956C30.4384 11.3544 31.5342 11.4338 32.7945 11.4338ZM24.7123 6.21979C24.7123 7.94898 25.2877 9.22823 26.4384 10.0575C27.1507 8.08132 27.5068 6.44918 27.5068 5.1611C27.5068 4.42002 27.4064 3.88186 27.2055 3.54661C27.0046 3.19371 26.7306 3.01726 26.3836 3.01726C25.8904 3.01726 25.4886 3.29958 25.1781 3.86421C24.8676 4.4112 24.7123 5.19639 24.7123 6.21979Z" fill="#512DA8"/>
|
||||
<svg width="36" height="36" viewBox="0 0 302 211" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M66.1447 169.653C59.9927 185.037 67.4767 202.495 82.8607 208.647C98.2448 214.799 115.703 207.315 121.855 191.931L66.1447 169.653ZM179.317 48.2403C185.469 32.8562 177.985 15.3978 162.601 9.2457C147.217 3.09365 129.758 10.5777 123.606 25.9617L179.317 48.2403ZM184.581 169.717C178.464 185.115 185.989 202.556 201.387 208.673C216.785 214.789 234.226 207.265 240.343 191.867L184.581 169.717ZM299.843 42.0748C305.959 26.6766 298.435 9.23549 283.037 3.11903C267.638 -2.99742 250.197 4.52695 244.081 19.9252L299.843 42.0748ZM122.731 108.946L94.8756 97.8071L122.731 108.946ZM240.343 191.867L299.843 42.0748L244.081 19.9252L184.581 169.717L240.343 191.867ZM121.855 191.931L150.586 120.086L94.8756 97.8071L66.1447 169.653L121.855 191.931ZM150.586 120.086L179.317 48.2403L123.606 25.9617L94.8756 97.8071L150.586 120.086Z" fill="#42D51F" />
|
||||
<path d="M58.6512 19.3633C52.2244 4.09198 34.6346 -3.07794 19.3633 3.34883C4.09198 9.7756 -3.07794 27.3654 3.34883 42.6367L58.6512 19.3633ZM66.3871 192.429C72.8139 207.7 90.4037 214.87 105.675 208.443C120.946 202.016 128.116 184.426 121.689 169.155L66.3871 192.429ZM179.115 25.3779C172.64 10.1268 155.028 3.01185 139.777 9.48631C124.526 15.9608 117.411 33.5729 123.885 48.824L179.115 25.3779ZM184.885 192.515C191.36 207.766 208.972 214.881 224.223 208.407C239.474 201.932 246.589 184.32 240.115 169.069L184.885 192.515ZM3.34883 42.6367L66.3871 192.429L121.689 169.155L58.6512 19.3633L3.34883 42.6367ZM123.885 48.824L184.885 192.515L240.115 169.069L179.115 25.3779L123.885 48.824Z" fill="#4FE02D"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 1.6 KiB |
@ -1,91 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Grid,
|
||||
IconButton,
|
||||
Typography,
|
||||
TextField,
|
||||
List,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
ListSubheader,
|
||||
ListItemSecondaryAction,
|
||||
DialogTitle
|
||||
} from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {Close, CropFree} from '@material-ui/icons';
|
||||
import Meal from "components/Meal";
|
||||
|
||||
import { MEALS_LIST } from 'utils/mock'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
},
|
||||
searchInput: {
|
||||
marginRight: theme.spacing(3),
|
||||
}
|
||||
}))
|
||||
|
||||
const AddMealModal = ({ isModalOpen, handleCloseModal }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
className={styles.root}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
>
|
||||
<DialogTitle className={styles.header}>
|
||||
<Typography variant="h6">Add product</Typography>
|
||||
<IconButton className={styles.closeButton} onClick={handleCloseModal}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid>
|
||||
<List
|
||||
dense
|
||||
subheader={
|
||||
<Grid container alignItems="center" justify="space-between" wrap="nowrap">
|
||||
<TextField
|
||||
label="Product name"
|
||||
name="product name"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
fullWidth
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
<IconButton size="medium" color="primary">
|
||||
<CropFree />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
{MEALS_LIST.map(({ name, macronutrients }, index) => (
|
||||
<Meal
|
||||
name={name}
|
||||
macronutrients={macronutrients}
|
||||
key={index}
|
||||
action={
|
||||
<ListItemSecondaryAction>
|
||||
<Checkbox
|
||||
edge="end"
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddMealModal;
|
@ -0,0 +1,14 @@
|
||||
import productFormModel from './productFormModel';
|
||||
const {
|
||||
formField: {
|
||||
quantity,
|
||||
unit,
|
||||
}
|
||||
} = productFormModel;
|
||||
|
||||
const formInitialValues = {
|
||||
[quantity.name]: 1,
|
||||
[unit.name]: 'portion',
|
||||
};
|
||||
|
||||
export default formInitialValues
|
@ -0,0 +1,17 @@
|
||||
const productFormModel = {
|
||||
formId: 'addProductsToMealForm',
|
||||
formField: {
|
||||
quantity: {
|
||||
name: 'quantity',
|
||||
label: 'Quantity*',
|
||||
requiredErrorMsg: 'Quantity is required'
|
||||
},
|
||||
unit: {
|
||||
name: 'unit',
|
||||
label: 'Unit*',
|
||||
requiredErrorMsg: 'Unit is required'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default productFormModel
|
@ -0,0 +1,15 @@
|
||||
import * as Yup from 'yup';
|
||||
import productFormModel from './productFormModel';
|
||||
const {
|
||||
formField: {
|
||||
unit,
|
||||
quantity,
|
||||
}
|
||||
} = productFormModel;
|
||||
|
||||
const validationSchema =
|
||||
Yup.object().shape({
|
||||
[unit.name]: Yup.string().required(`${unit.requiredErrorMsg}`),
|
||||
[quantity.name]: Yup.number().required(`${quantity.requiredErrorMsg}`),
|
||||
})
|
||||
export default validationSchema
|
@ -0,0 +1,111 @@
|
||||
import {Button, Dialog, DialogActions, Typography, FormControl, DialogContent, DialogTitle} from "@material-ui/core";
|
||||
import React from "react";
|
||||
import {Form, Formik} from "formik";
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect'
|
||||
import PropTypes from "prop-types";
|
||||
import SelectField from "components/SelectField";
|
||||
import InputField from "components/InputField";
|
||||
import formInitialValues from "../../FormModel/formInitialValues";
|
||||
import validationSchema from "../../FormModel/validationSchema";
|
||||
import productFormModel from "../../FormModel/productFormModel";
|
||||
import {createMealAction, addProductsToMealAction} from "pages/Home/actions";
|
||||
import { makeSelectMealId, makeSelectMealLabel } from 'pages/Home/selectors'
|
||||
import {format} from "date-fns";
|
||||
|
||||
const stateSelector = createStructuredSelector({
|
||||
mealId: makeSelectMealId(),
|
||||
mealLabel: makeSelectMealLabel()
|
||||
})
|
||||
|
||||
const { formId, formField } = productFormModel;
|
||||
|
||||
const ProductDetailsForm = ({
|
||||
isOpen,
|
||||
handleClose,
|
||||
id,
|
||||
label,
|
||||
unit,
|
||||
servingCapacity,
|
||||
macronutrients
|
||||
}) => {
|
||||
const dispatch = useDispatch()
|
||||
const { mealId, mealLabel } = useSelector(stateSelector)
|
||||
|
||||
const unitTypes = [
|
||||
{ label: 'None', value: undefined },
|
||||
{ label: 'portion', value: 'portion' },
|
||||
{ label: unit, value: unit },
|
||||
]
|
||||
|
||||
const today = format(new Date(), "yyyy-MM-dd");
|
||||
|
||||
const onSubmit = (values, actions) => {
|
||||
const products = [{
|
||||
...values,
|
||||
product: id,
|
||||
}]
|
||||
|
||||
if (mealId) {
|
||||
dispatch(addProductsToMealAction({ id: mealId, products }))
|
||||
} else {
|
||||
dispatch(createMealAction({ date: today, label: mealLabel, products }))
|
||||
}
|
||||
|
||||
actions.setSubmitting(false);
|
||||
handleClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
>
|
||||
<Formik
|
||||
initialValues={formInitialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form id={formId}>
|
||||
<DialogTitle>{label}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2">1 portion = {servingCapacity}${unit}</Typography>
|
||||
<FormControl>
|
||||
<InputField label={formField.quantity.label} name={formField.quantity.name} type="number" min="1" />
|
||||
</FormControl>
|
||||
<FormControl >
|
||||
<SelectField label={formField.unit.label} name={formField.unit.name} data={unitTypes} />
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>
|
||||
cancel
|
||||
</Button>
|
||||
<Button type="submit" color="primary" disabled={isSubmitting}>
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
ProductDetailsForm.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
unit: PropTypes.string.isRequired,
|
||||
servingCapacity: PropTypes.number.isRequired,
|
||||
macronutrients: PropTypes.shape({
|
||||
salt: PropTypes.number.isRequired,
|
||||
calories: PropTypes.number.isRequired,
|
||||
fat: PropTypes.number.isRequired,
|
||||
carbohydrates: PropTypes.number.isRequired,
|
||||
protein: PropTypes.number.isRequired,
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
export default ProductDetailsForm
|
68
src/components/AddProductToMealDialog/ProductItem.js
Normal file
68
src/components/AddProductToMealDialog/ProductItem.js
Normal file
@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types'
|
||||
import {Checkbox, ListItem, Typography, ListItemSecondaryAction, ListItemText} from "@material-ui/core";
|
||||
import ProductLabel from 'components/ProductLabel'
|
||||
import ProductDetailsForm from './Forms/ProductDetailsForm'
|
||||
|
||||
const ProductItem = ({ id, verified, eco, label, unit, servingCapacity, macronutrients }) => {
|
||||
const [isProductDetailsDialogOpen, setIsProductDetailsDialogOpen] = useState(false);
|
||||
const { calories } = macronutrients
|
||||
const handleOpenDialog = () => {
|
||||
setIsProductDetailsDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setIsProductDetailsDialogOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ListItem button onClick={handleOpenDialog}>
|
||||
<ListItemText
|
||||
primary={<ProductLabel text={label} verified={verified} eco={eco} />}
|
||||
secondary={
|
||||
<Typography variant="body2">1 portion = {Math.floor((servingCapacity / 100) * calories)}kcal</Typography>
|
||||
}
|
||||
disableTypography
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Checkbox
|
||||
value={id}
|
||||
name="product"
|
||||
edge="end"
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<ProductDetailsForm
|
||||
isOpen={isProductDetailsDialogOpen}
|
||||
id={id}
|
||||
handleClose={handleCloseDialog}
|
||||
verified={verified}
|
||||
eco={eco}
|
||||
label={label}
|
||||
unit={unit}
|
||||
servingCapacity={servingCapacity}
|
||||
macronutrients={macronutrients}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
ProductItem.propTypes = {
|
||||
selected: PropTypes.bool.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
verified: PropTypes.bool.isRequired,
|
||||
eco: PropTypes.bool.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
unit: PropTypes.string.isRequired,
|
||||
servingCapacity: PropTypes.number.isRequired,
|
||||
macronutrients: PropTypes.shape({
|
||||
salt: PropTypes.number.isRequired,
|
||||
calories: PropTypes.number.isRequired,
|
||||
fat: PropTypes.number.isRequired,
|
||||
carbohydrates: PropTypes.number.isRequired,
|
||||
protein: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default ProductItem;
|
106
src/components/AddProductToMealDialog/index.js
Normal file
106
src/components/AddProductToMealDialog/index.js
Normal file
@ -0,0 +1,106 @@
|
||||
import React, { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
Grid,
|
||||
Button,
|
||||
Typography,
|
||||
DialogActions,
|
||||
IconButton,
|
||||
Dialog,
|
||||
List,
|
||||
} from '@material-ui/core';
|
||||
import { useDispatch } from 'react-redux'
|
||||
import {Add as AddIcon} from "@material-ui/icons";
|
||||
import SearchInput from 'components/SearchInput'
|
||||
import BarcodeScanner from 'containers/BarcodeScanner'
|
||||
import { createStructuredSelector } from 'reselect'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { makeSelectProducts } from 'pages/Home/selectors'
|
||||
import ProductItem from './ProductItem'
|
||||
import { setSelectedMealAction } from 'pages/Home/actions'
|
||||
|
||||
const stateSelector = createStructuredSelector({
|
||||
products: makeSelectProducts(),
|
||||
})
|
||||
|
||||
const AddProductToMealDialog = ({ mealId, mealLabel }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { products } = useSelector(stateSelector)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const selectMeal = () => dispatch(setSelectedMealAction({ label: mealLabel, id: mealId }))
|
||||
|
||||
const handleClose = (event) => {
|
||||
event.stopPropagation();
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const handleOpen = (event) => {
|
||||
event.stopPropagation();
|
||||
setIsOpen(true)
|
||||
selectMeal();
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<IconButton onClick={handleOpen} onFocus={(event) => event.stopPropagation()}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<Dialog
|
||||
fullScreen
|
||||
open={isOpen}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<DialogTitle>Add products to {mealLabel}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justify="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<SearchInput />
|
||||
<BarcodeScanner />
|
||||
</Grid>
|
||||
<List>
|
||||
{products.length > 0 ? (
|
||||
products.map(({ id, verified, eco, salt, brand, servingCapacity, barcode, label, unit, calories, fat, carbohydrates, protein }) => (
|
||||
<ProductItem
|
||||
selected={false}
|
||||
key={id}
|
||||
id={id}
|
||||
verified={verified}
|
||||
eco={eco}
|
||||
label={label}
|
||||
unit={unit}
|
||||
servingCapacity={servingCapacity}
|
||||
macronutrients={{
|
||||
salt,
|
||||
calories,
|
||||
fat,
|
||||
carbohydrates,
|
||||
protein,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Typography variant="caption">Products not found</Typography>
|
||||
)}
|
||||
</List>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</React.Fragment>
|
||||
)
|
||||
};
|
||||
|
||||
AddProductToMealDialog.propTypes = {
|
||||
mealLabel: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default AddProductToMealDialog;
|
@ -1,56 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Box } from '@material-ui/core'
|
||||
import Quagga from 'quagga';
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import {ReactComponent as Overlay} from './overlay.svg'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
overlay: {
|
||||
fill: 'transparent',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}
|
||||
}))
|
||||
|
||||
const config = {
|
||||
inputStream: {
|
||||
name: 'Live',
|
||||
type : 'LiveStream',
|
||||
constraints: {
|
||||
width: {min: 'auto', max: 640},
|
||||
height: {min: 'auto', max: 480},
|
||||
aspectRatio: 447 / 250,
|
||||
facingMode: 'environment',
|
||||
}
|
||||
},
|
||||
locator: {
|
||||
patchSize: 'medium',
|
||||
halfSample: true
|
||||
},
|
||||
numOfWorkers: 4,
|
||||
frequency: 10,
|
||||
decoder: {
|
||||
readers : [ "ean_8_reader"]
|
||||
},
|
||||
locate: true
|
||||
}
|
||||
|
||||
const BarcodeScanner = ({ onDetected }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
useEffect(() => {
|
||||
Quagga.init(config, () => Quagga.start())
|
||||
return () => Quagga.offDetected(onDetected)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box id="interactive" className="viewport" position="relative">
|
||||
<Overlay className={styles.overlay}/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default BarcodeScanner;
|
@ -1,61 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {Grid, Paper, Typography} from "@material-ui/core";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import grey from '@material-ui/core/colors/grey';
|
||||
import MacronutrientsChart from 'components/MacronutrientsChart'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
subtitle: {
|
||||
color: grey[400]
|
||||
}
|
||||
}))
|
||||
|
||||
const DailyStats = ({ caloriesLeft, macronutrients }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle1" align="center" className={classes.subtitle}>
|
||||
Calories left
|
||||
</Typography>
|
||||
<Typography variant="h3">
|
||||
{caloriesLeft} kcal
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid
|
||||
container
|
||||
justify="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
{macronutrients.map(({ current, max, label, color }, index) => (
|
||||
<MacronutrientsChart current={current} max={max} label={label} color={color} key={index} />
|
||||
))}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
DailyStats.propTypes = {
|
||||
caloriesLeft: PropTypes.number.isRequired,
|
||||
macronutrients: PropTypes.arrayOf(PropTypes.shape({
|
||||
current: PropTypes.number.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
})).isRequired
|
||||
}
|
||||
|
||||
export default DailyStats
|
42
src/components/DrawerNav/index.js
Normal file
42
src/components/DrawerNav/index.js
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import {Drawer, Toolbar, List, Divider, ListItem, ListItemIcon, ListItemText} from '@material-ui/core';
|
||||
import { Link } from 'react-router-dom'
|
||||
import useStyles from './styles'
|
||||
|
||||
const DrawerNav = ({ isDrawerNavOpen, drawerRoutes }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className={classes.drawer}
|
||||
anchor="left"
|
||||
variant="persistent"
|
||||
open={isDrawerNavOpen}
|
||||
classes={{
|
||||
paper: classes.drawerPaper,
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
<div className={classes.drawerContainer}>
|
||||
<List>
|
||||
{drawerRoutes.map(({ label, path }) => (
|
||||
<Link to={path} color="inherit" key={label}>
|
||||
<ListItem button>
|
||||
<ListItemText primary={label} />
|
||||
</ListItem>
|
||||
</Link>
|
||||
))}
|
||||
</List>
|
||||
<Divider />
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
DrawerNav.propTypes = {
|
||||
isDrawerNavOpen: PropTypes.bool.isRequired,
|
||||
drawerRoutes: PropTypes.array.isRequired,
|
||||
}
|
||||
|
||||
export default DrawerNav
|
18
src/components/DrawerNav/styles.js
Normal file
18
src/components/DrawerNav/styles.js
Normal file
@ -0,0 +1,18 @@
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
|
||||
export const drawerWidth = 240;
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
drawer: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
},
|
||||
drawerPaper: {
|
||||
width: drawerWidth,
|
||||
},
|
||||
drawerContainer: {
|
||||
overflow: 'auto',
|
||||
},
|
||||
}));
|
||||
|
||||
export default useStyles
|
@ -4,9 +4,10 @@ import { useField } from 'formik';
|
||||
import { TextField } from '@material-ui/core';
|
||||
|
||||
const InputField = (props) => {
|
||||
const { errorText, ...rest } = props;
|
||||
const [field, meta] = useField(props);
|
||||
|
||||
const _renderHelperText = () => {
|
||||
const renderHelperText = () => {
|
||||
const [touched, error] = at(meta, 'touched', 'error');
|
||||
if (touched && error) {
|
||||
return error;
|
||||
@ -17,9 +18,9 @@ const InputField = (props) => {
|
||||
<TextField
|
||||
type="text"
|
||||
error={meta.touched && meta.error && true}
|
||||
helperText={_renderHelperText()}
|
||||
helperText={renderHelperText()}
|
||||
{...field}
|
||||
{...props}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
28
src/components/MacronutrientsCard/index.js
Normal file
28
src/components/MacronutrientsCard/index.js
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types'
|
||||
import {Card, CardContent, Grid, Typography} from "@material-ui/core";
|
||||
import RadialChart from "components/RadialChart";
|
||||
|
||||
const MacronutrientCard = ({ label, total, daily, color }) => {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Grid container alignItems="center" justify="center">
|
||||
<RadialChart progress={(total/daily) * 100} width={50} height={50} color={color} />
|
||||
<Typography gutterBottom>
|
||||
{Math.floor(daily - total)} {label} left
|
||||
</Typography>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
MacronutrientCard.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
total: PropTypes.number.isRequired,
|
||||
daily: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default MacronutrientCard;
|
@ -1,51 +0,0 @@
|
||||
import {Grid, Typography} from "@material-ui/core";
|
||||
import { makeStyles} from '@material-ui/core/styles'
|
||||
import PropTypes from "prop-types";
|
||||
import React from "react";
|
||||
import RadialChart from "../RadialChart";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
width: 'auto'
|
||||
},
|
||||
chart: {
|
||||
paddingRight: theme.spacing(1)
|
||||
}
|
||||
}))
|
||||
|
||||
const MacronutrientsChart = ({ max, current, label, color }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const progress = current / max * 100
|
||||
const macronutrientsLeft = max - current;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
className={classes.root}
|
||||
container
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
>
|
||||
<RadialChart className={classes.chart} progress={progress} color={color} width={35} height={35} />
|
||||
<Grid item>
|
||||
<Typography variant="subtitle2">
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
{macronutrientsLeft}g
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
MacronutrientsChart.propTypes = {
|
||||
max: PropTypes.number.isRequired,
|
||||
current: PropTypes.number.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default MacronutrientsChart
|
@ -1,40 +0,0 @@
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import {Chip, Grid} from "@material-ui/core";
|
||||
import PropTypes from "prop-types";
|
||||
import React from "react";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
chip: {
|
||||
marginTop: theme.spacing(1),
|
||||
'&:not(:last-child)': {
|
||||
marginRight: theme.spacing(1),
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const MacronutrientsDetails = ({ macronutrients }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Grid component="span" container alignItems="center">
|
||||
{macronutrients.map(({ value, unit }, index) => (
|
||||
<Chip
|
||||
component="span"
|
||||
className={classes.chip}
|
||||
key={index}
|
||||
size="small"
|
||||
label={`${value}${unit}`}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
MacronutrientsDetails.propTypes = {
|
||||
macronutrients: PropTypes.arrayOf(PropTypes.shape({
|
||||
value: PropTypes.number.isRequired,
|
||||
unit: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
}
|
||||
|
||||
export default MacronutrientsDetails
|
37
src/components/MacronutrientsLabel/index.js
Normal file
37
src/components/MacronutrientsLabel/index.js
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import {Grid, Typography} from "@material-ui/core";
|
||||
import PropTypes from 'prop-types'
|
||||
import {colors} from "utils/theme";
|
||||
|
||||
const MacronutrientsLabel = ({ unit, quantity, calories, fat, protein, carbohydrates }) => {
|
||||
return (
|
||||
<Grid component="span" container align="center" justify="space-between">
|
||||
<Grid item xs={1} component="span">
|
||||
<Typography>{quantity} {unit}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={2} component="span">
|
||||
<Typography style={{ color: colors.calories }}>{calories} kcal</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={2} component="span">
|
||||
<Typography style={{ color: colors.protein }}>{protein}g</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={2} component="span">
|
||||
<Typography style={{ color: colors.fat }}>{fat}g</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={2} component="span">
|
||||
<Typography style={{ color: colors.carbs }}>{carbohydrates}g</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
MacronutrientsLabel.propTypes = {
|
||||
unit: PropTypes.oneOf(['portion', 'g', 'ml']).isRequired,
|
||||
quantity: PropTypes.number.isRequired,
|
||||
calories: PropTypes.number.isRequired,
|
||||
fat: PropTypes.number.isRequired,
|
||||
protein: PropTypes.number.isRequired,
|
||||
carbohydrates: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
export default MacronutrientsLabel;
|
@ -1,37 +0,0 @@
|
||||
import {ListItem, ListItemText, ListItemSecondaryAction} from "@material-ui/core";
|
||||
import PropTypes from 'prop-types'
|
||||
import React from "react";
|
||||
import MacronutrientsDetails from "components/MacronutrientsDetails";
|
||||
|
||||
const Meal = ({ name, macronutrients, action }) => {
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
secondary={
|
||||
<MacronutrientsDetails
|
||||
macronutrients={macronutrients}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{action ? (
|
||||
<ListItemSecondaryAction>
|
||||
{action}
|
||||
</ListItemSecondaryAction>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
||||
Meal.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
macronutrients: PropTypes.arrayOf(PropTypes.shape({
|
||||
value: PropTypes.number.isRequired,
|
||||
unit: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
action: PropTypes.node,
|
||||
}
|
||||
|
||||
export default Meal
|
58
src/components/MealCard/index.js
Normal file
58
src/components/MealCard/index.js
Normal file
@ -0,0 +1,58 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid,
|
||||
List,
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types'
|
||||
import ProductCard from "components/ProductCard";
|
||||
import AddProductToMealDialog from 'components/AddProductToMealDialog';
|
||||
|
||||
|
||||
const MealCard = ({ id, label, products }) => {
|
||||
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary>
|
||||
<Grid container>
|
||||
<Grid container alignItems="center" justify="space-between">
|
||||
<Typography variant="h5">
|
||||
{label}
|
||||
</Typography>
|
||||
<AddProductToMealDialog mealId={id} mealLabel={label} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid item xs={12}>
|
||||
<List>
|
||||
{products.map(({ _id, product, quantity, unit }) => (
|
||||
<ProductCard
|
||||
mealId={id}
|
||||
id={_id}
|
||||
product={product}
|
||||
quantity={quantity}
|
||||
unit={unit}
|
||||
key={_id}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
MealCard.propTypes = {
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string.isRequired,
|
||||
products: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.arrayOf(PropTypes.object),
|
||||
]).isRequired,
|
||||
}
|
||||
|
||||
export default MealCard;
|
@ -1,59 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {List, ListItem, Collapse, ListItemText, IconButton } from '@material-ui/core';
|
||||
import {Close, ExpandLess, ExpandMore} from '@material-ui/icons';
|
||||
import MacronutrientsDetails from 'components/MacronutrientsDetails'
|
||||
import Meal from 'components/Meal'
|
||||
import AddMealModal from 'components/AddMealModal'
|
||||
|
||||
const MenuList = ({ list }) => {
|
||||
const [expandedListName, setExpandedListName] = useState({});
|
||||
const [isModalOpen, setIsModalOpen] = useState(true);
|
||||
|
||||
const expandList = (listName) => {
|
||||
const isListExpanded = expandedListName[listName]
|
||||
|
||||
setExpandedListName(prev => ({
|
||||
...prev,
|
||||
[listName]: !isListExpanded
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<List dense>
|
||||
{list.map(({ listName, macronutrients, meals }, index) => (
|
||||
<div key={index}>
|
||||
<ListItem button key={index} >
|
||||
<ListItemText onClick={() => setIsModalOpen(true)} primary={listName} secondary={<MacronutrientsDetails macronutrients={macronutrients} />} />
|
||||
<IconButton size="small" color="primary" onClick={() => expandList(listName)}>
|
||||
{expandedListName[listName] ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
{meals.length ? (
|
||||
<Collapse in={expandedListName[listName]} timeout="auto" unmountOnExit>
|
||||
{meals.map(({ name, macronutrients }, index) => (
|
||||
<Meal
|
||||
name={name}
|
||||
macronutrients={macronutrients}
|
||||
key={index}
|
||||
action={
|
||||
<IconButton size="small" onClick={() => console.log('elo')}>
|
||||
<Close fontSize="small"/>
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Collapse>
|
||||
) : null }
|
||||
</div>
|
||||
))}
|
||||
</List>
|
||||
{isModalOpen
|
||||
? <AddMealModal isModalOpen={isModalOpen} handleCloseModal={() => setIsModalOpen(false)} />
|
||||
: null
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MenuList
|
@ -1,37 +1,30 @@
|
||||
import React from 'react';
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import {AppBar, Button, IconButton, Toolbar, Typography} from "@material-ui/core";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
import Waves from "@material-ui/icons/Waves";
|
||||
import PropTypes from 'prop-types'
|
||||
import {AppBar, IconButton, Toolbar, Typography} from "@material-ui/core";
|
||||
import {Menu as MenuIcon, Waves as WavesIcon } from "@material-ui/icons";
|
||||
import useStyles from './styles'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
title: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
}));
|
||||
import {ReactComponent as LogoIcon} from 'assets/logo.svg'
|
||||
|
||||
const Navbar = () => {
|
||||
const Navbar = ({ toggleDrawerNav }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<AppBar position="static" color="inherit">
|
||||
<AppBar position="fixed" color="inherit" className={classes.appBar}>
|
||||
<Toolbar>
|
||||
<IconButton edge="start" className={classes.menuButton} aria-label="menu">
|
||||
<IconButton onClick={toggleDrawerNav} edge="start" className={classes.menuButton}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" className={classes.title}>
|
||||
<Waves />
|
||||
<LogoIcon />
|
||||
</Typography>
|
||||
<Button color="inherit">Login</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
Navbar.propTypes = {
|
||||
toggleDrawerNav: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default Navbar;
|
||||
|
15
src/components/Navbar/styles.js
Normal file
15
src/components/Navbar/styles.js
Normal file
@ -0,0 +1,15 @@
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
appBar: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
title: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
export default useStyles
|
@ -1,132 +0,0 @@
|
||||
import 'date-fns';
|
||||
import React from 'react';
|
||||
import {Typography, InputLabel, Box, Grid, InputAdornment, Select, FormControl, MenuItem, TextField, Slider} from '@material-ui/core';
|
||||
import DateFnsUtils from '@date-io/date-fns';
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
import {
|
||||
MuiPickersUtilsProvider,
|
||||
DatePicker,
|
||||
} from '@material-ui/pickers';
|
||||
import {useInjectReducer} from "redux-injectors";
|
||||
import { useSelector } from 'react-redux';
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import reducer from "pages/Profile/reducer";
|
||||
import {
|
||||
makeSelectActivities,
|
||||
makeSelectGenders,
|
||||
} from "pages/Profile/selectors";
|
||||
|
||||
const stateSelector = createStructuredSelector({
|
||||
activities: makeSelectActivities(),
|
||||
genders: makeSelectGenders(),
|
||||
});
|
||||
|
||||
const key = 'profilePage'
|
||||
const PersonalDetailsForm = () => {
|
||||
useInjectReducer({ key, reducer });
|
||||
const { activities, genders } = useSelector(stateSelector)
|
||||
const { register, control } = useFormContext()
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Personal details
|
||||
</Typography>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<FormControl variant="outlined" fullWidth>
|
||||
<InputLabel>Gender</InputLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name="gender"
|
||||
as={
|
||||
<Select
|
||||
label="Gender"
|
||||
name="gender"
|
||||
>
|
||||
{genders.map(label => (
|
||||
<MenuItem value={label} key={label}>{label}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Controller
|
||||
control={control}
|
||||
onChange={([selected]) => selected}
|
||||
name="birthday"
|
||||
render={({ onChange, value }) => (
|
||||
<MuiPickersUtilsProvider utils={DateFnsUtils}>
|
||||
<DatePicker
|
||||
disableFuture
|
||||
fullWidth
|
||||
openTo="year"
|
||||
format="dd/MM/yyyy"
|
||||
label="Date of birth"
|
||||
views={["year", "month", "date"]}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</MuiPickersUtilsProvider>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="height"
|
||||
as={
|
||||
<TextField
|
||||
type="number"
|
||||
label="Height"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">cm</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="weight.current"
|
||||
as={
|
||||
<TextField
|
||||
type="number"
|
||||
label="Current Weight"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">Kg</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="weight.goal"
|
||||
as={
|
||||
<TextField
|
||||
type="number"
|
||||
label="Goal Weight"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">Kg</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
export default PersonalDetailsForm
|
57
src/components/ProductCard/index.js
Normal file
57
src/components/ProductCard/index.js
Normal file
@ -0,0 +1,57 @@
|
||||
import {ListItem, ListItemText, ListItemSecondaryAction, IconButton} from "@material-ui/core";
|
||||
import PropTypes from 'prop-types'
|
||||
import React from "react";
|
||||
import {Delete as DeleteIcon} from "@material-ui/icons";
|
||||
import ProductLabel from 'components/ProductLabel'
|
||||
import {useDispatch } from 'react-redux'
|
||||
import {removeProductFromMealAction} from 'pages/Home/actions'
|
||||
import { calculateMacro } from 'utils/calculate'
|
||||
import MacronutrientsLabel from 'components/MacronutrientsLabel'
|
||||
|
||||
const ProductCard = ({ mealId, id, product, quantity, unit }) => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const { label, calories, fat, protein, carbohydrates, eco, verified } = product
|
||||
|
||||
const removeProductFormMeal = () => {
|
||||
dispatch(removeProductFromMealAction({ mealId, productId: id }))
|
||||
}
|
||||
|
||||
const calculatedCalories = calculateMacro(unit, calories, quantity)
|
||||
const calculateFats = calculateMacro(unit, fat, quantity)
|
||||
const calculatedProteins = calculateMacro(unit, protein, quantity)
|
||||
const calculatedCarbohydrates = calculateMacro(unit, carbohydrates, quantity)
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={<ProductLabel eco={eco} verified={verified} text={label}/>}
|
||||
disableTypography
|
||||
secondary={
|
||||
<MacronutrientsLabel
|
||||
unit={unit}
|
||||
quantity={quantity}
|
||||
calories={calculatedCalories}
|
||||
fat={calculateFats}
|
||||
protein={calculatedProteins}
|
||||
carbohydrates={calculatedCarbohydrates}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton onClick={removeProductFormMeal}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
||||
ProductCard.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
product: PropTypes.object.isRequired,
|
||||
quantity: PropTypes.number.isRequired,
|
||||
unit: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default ProductCard
|
18
src/components/ProductLabel/index.js
Normal file
18
src/components/ProductLabel/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types'
|
||||
import {Grid, Typography } from "@material-ui/core";
|
||||
import { Eco as EcoIcon, VerifiedUser as VerifiedIcon } from '@material-ui/icons'
|
||||
|
||||
const ProductLabel = ({ text, eco, verified }) => (
|
||||
<Grid container>
|
||||
<Typography variant="subtitle1">{text}</Typography> {eco && <EcoIcon size="small" />} {verified && <VerifiedIcon />}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
ProductLabel.propTypes = {
|
||||
verified: PropTypes.bool.isRequired,
|
||||
eco: PropTypes.bool.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default ProductLabel;
|
@ -1,62 +1,49 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import useStyles from './styles'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
position: `relative`,
|
||||
display: `inline-block`,
|
||||
transition: `all 0.3s ease-in`
|
||||
},
|
||||
total: {
|
||||
opacity: 0.3,
|
||||
},
|
||||
progress: {
|
||||
transform: `rotate(90deg)`,
|
||||
transformOrigin: `center`,
|
||||
transition: `all 0.6s cubic-bezier(0.58, 0.16, 0.5, 1.14)`,
|
||||
transitionDelay: `0.3s`,
|
||||
}
|
||||
}))
|
||||
|
||||
const RadialChart = ({ progress, color, width, height, className }) => {
|
||||
const RadialChart = ({ strokeWidth = 20, circleRadius = 50, progress, color, width, height }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const circleRadius = 80;
|
||||
const circumference = 2 * 3.14 * circleRadius;
|
||||
const circumference = 2 * Math.PI * circleRadius;
|
||||
const strokeLength = circumference / 100 * progress;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${classes.wrapper} ${className}`}
|
||||
>
|
||||
<svg viewBox="0 0 180 180" width={width} height={height}>
|
||||
<span className={classes.chartContainer}>
|
||||
<svg
|
||||
viewBox="0 0 180 180"
|
||||
className={classes.svg}
|
||||
width={width}
|
||||
height={height}
|
||||
>
|
||||
<circle
|
||||
className={classes.total}
|
||||
className={classes.chartTotal}
|
||||
stroke={color}
|
||||
strokeWidth="10"
|
||||
strokeWidth={strokeWidth}
|
||||
r={circleRadius}
|
||||
fill="none"
|
||||
cx="90"
|
||||
cy="90"
|
||||
r={circleRadius}
|
||||
/>
|
||||
<circle
|
||||
className={classes.progress}
|
||||
className={classes.chartProgress}
|
||||
stroke={color}
|
||||
strokeWidth="10"
|
||||
strokeWidth={strokeWidth}
|
||||
r={circleRadius}
|
||||
strokeDasharray={`${strokeLength},${circumference}`}
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
cx="90"
|
||||
cy="90"
|
||||
r={circleRadius}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
RadialChart.propTypes = {
|
||||
strokeWidth: PropTypes.number,
|
||||
circleRadius: PropTypes.number,
|
||||
progress: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
|
23
src/components/RadialChart/styles.js
Normal file
23
src/components/RadialChart/styles.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
chartContainer: {
|
||||
position: `relative`,
|
||||
display: `inline-block`,
|
||||
transition: `all 0.3s ease-in`,
|
||||
},
|
||||
svg: {
|
||||
marginLeft: theme.spacing(-1),
|
||||
},
|
||||
chartTotal: {
|
||||
opacity: 0.3,
|
||||
},
|
||||
chartProgress: {
|
||||
transform: `rotate(90deg)`,
|
||||
transformOrigin: `center`,
|
||||
transition: `all 0.6s cubic-bezier(0.58, 0.16, 0.5, 1.14)`,
|
||||
transitionDelay: `0.3s`,
|
||||
}
|
||||
}))
|
||||
|
||||
export default useStyles
|
@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {Typography, List, ListItem, ListItemText} from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
listItem: {
|
||||
padding: theme.spacing(1, 0),
|
||||
},
|
||||
total: {
|
||||
fontWeight: 700,
|
||||
},
|
||||
title: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
const ReviewProfileForm = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Profile summary
|
||||
</Typography>
|
||||
<List disablePadding>
|
||||
TODO
|
||||
{/*{profile.map(({ label, value }, index) => (*/}
|
||||
{/* <ListItem className={classes.listItem} key={index}>*/}
|
||||
{/* <ListItemText primary={label} />*/}
|
||||
{/* <Typography variant="body2">{value}</Typography>*/}
|
||||
{/* </ListItem>*/}
|
||||
{/*))}*/}
|
||||
</List>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReviewProfileForm
|
@ -1,67 +0,0 @@
|
||||
import React, {useState} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Tab, Tabs } from "@material-ui/core";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
|
||||
import TabLabel from 'components/TabLabel';
|
||||
import TabPanel from 'components/TabPanel';
|
||||
import MenuList from 'components/MenuList'
|
||||
|
||||
import { MENU_LIST } from 'utils/mock'
|
||||
|
||||
const a11yProps = (index) => {
|
||||
return {
|
||||
id: `scrollable-force-tab-${index}`,
|
||||
'aria-controls': `scrollable-force-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
tab: {
|
||||
width: `100%`
|
||||
}
|
||||
}))
|
||||
|
||||
const ScrollableTabs = ({ tabs }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
const handleChangeDay = (event, newDay) => {
|
||||
setValue(newDay);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChangeDay}
|
||||
variant="scrollable"
|
||||
indicatorColor="primary"
|
||||
scrollButtons="on"
|
||||
>
|
||||
{tabs.map((label, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
className={classes.tab}
|
||||
label={<TabLabel label={label} />}
|
||||
{...a11yProps(index)}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<Box>
|
||||
{tabs.map((_, index) => (
|
||||
<TabPanel value={value} index={index} key={index}>
|
||||
<MenuList list={MENU_LIST} />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
ScrollableTabs.propTypes = {
|
||||
tabs: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
}
|
||||
|
||||
export default ScrollableTabs
|
27
src/components/SearchInput/index.js
Normal file
27
src/components/SearchInput/index.js
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import {TextField, Grid} from '@material-ui/core'
|
||||
import { throttle } from 'lodash'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { searchProductByLabelAction } from 'pages/Home/actions'
|
||||
|
||||
const SearchInput = () => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const searchByLabel = ({ target: { value }}) => {
|
||||
dispatch(searchProductByLabelAction({ label: value }))
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid item xs={10}>
|
||||
<TextField
|
||||
type="text"
|
||||
label="Search product"
|
||||
onChange={throttle(searchByLabel, 1000)}
|
||||
name="label"
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchInput;
|
@ -16,7 +16,7 @@ const SelectField = ({ label, data, ...rest }) => {
|
||||
const [touched, error] = at(meta, 'touched', 'error');
|
||||
const isError = touched && error && true;
|
||||
|
||||
const _renderHelperText = () => {
|
||||
const renderHelperText = () => {
|
||||
if (isError) {
|
||||
return <FormHelperText>{error}</FormHelperText>;
|
||||
}
|
||||
@ -32,16 +32,13 @@ const SelectField = ({ label, data, ...rest }) => {
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{_renderHelperText()}
|
||||
{renderHelperText()}
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
|
||||
SelectField.defaultProps = {
|
||||
data: []
|
||||
};
|
||||
|
||||
SelectField.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
data: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { at } from 'lodash';
|
||||
import { useField } from 'formik';
|
||||
import {
|
||||
Slider,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormHelperText
|
||||
} from '@material-ui/core';
|
||||
|
||||
const SliderField = ({ label, ...rest }) => {
|
||||
const [field, meta, helper] = useField({ label, ...rest });
|
||||
const { setValue } = helper;
|
||||
|
||||
const _onChange = (e) => {
|
||||
setValue(e.target.checked);
|
||||
}
|
||||
|
||||
return (
|
||||
<Slider onChange={_onChange} />
|
||||
);
|
||||
}
|
||||
|
||||
export default SliderField
|
@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from "@material-ui/core";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const TabLabel = ({ label }) => {
|
||||
console.log('TabLabel - todo')
|
||||
return (
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Typography component="span" variant="caption">
|
||||
30 lis
|
||||
</Typography>
|
||||
<Typography component="span" variant="caption">
|
||||
poniedziałek
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
TabLabel.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default TabLabel;
|
@ -1,29 +0,0 @@
|
||||
import {Box} from "@material-ui/core";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const TabPanel = ({ children, value, index, ...other }) => {
|
||||
return (
|
||||
<Box
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`scrollable-force-tabpanel-${index}`}
|
||||
aria-labelledby={`scrollable-force-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box p={3}>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
TabPanel.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
index: PropTypes.any.isRequired,
|
||||
value: PropTypes.any.isRequired,
|
||||
};
|
||||
|
||||
export default TabPanel
|
@ -0,0 +1,5 @@
|
||||
import { LOGOUT } from './constants'
|
||||
|
||||
export const logoutAction = () => ({
|
||||
type: LOGOUT
|
||||
})
|
@ -1,4 +1,2 @@
|
||||
export const LOGOUT_REQUEST = 'app/App/LOGOUT_REQUEST';
|
||||
export const LOGOUT_ERROR = 'app/App/LOGOUT_ERROR';
|
||||
export const LOGOUT_SUCCESS = 'app/App/LOGOUT_SUCCESS';
|
||||
export const LOGOUT = 'app/App/LOGOUT';
|
||||
export const LOGIN_SUCCESS = 'app/App/LOGIN_SUCCESS';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { ThemeProvider } from '@material-ui/core/styles'
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
@ -6,21 +6,46 @@ import { theme } from 'utils'
|
||||
import {makeSelectIsLogged} from "./selectors";
|
||||
import { useSelector } from 'react-redux';
|
||||
import Navbar from 'components/Navbar'
|
||||
|
||||
import DrawerNav from 'components/DrawerNav'
|
||||
import Routes from 'containers/Routes'
|
||||
import { routes } from 'utils'
|
||||
|
||||
const stateSelector = createStructuredSelector({
|
||||
isLogged: makeSelectIsLogged(),
|
||||
});
|
||||
|
||||
const drawerRoutes = [
|
||||
{
|
||||
label: 'Home',
|
||||
path: routes.dashboard.path,
|
||||
},
|
||||
{
|
||||
label: 'Profile',
|
||||
path: routes.profile.path,
|
||||
},
|
||||
{
|
||||
label: 'Logout',
|
||||
path: routes.logout.path,
|
||||
},
|
||||
]
|
||||
|
||||
const App = () => {
|
||||
const { isLogged } = useSelector(stateSelector)
|
||||
const [isDrawerNavOpen, setIsDrawerNavOpen] = useState(false)
|
||||
|
||||
const toggleDrawerNav = () => {
|
||||
setIsDrawerNavOpen(!isDrawerNavOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{isLogged && <Navbar /> }
|
||||
{/*<Navbar />*/}
|
||||
{isLogged &&
|
||||
<React.Fragment>
|
||||
<Navbar toggleDrawerNav={toggleDrawerNav} />
|
||||
<DrawerNav isDrawerNavOpen={isDrawerNavOpen} drawerRoutes={drawerRoutes} />
|
||||
</React.Fragment>
|
||||
}
|
||||
<Routes />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -1,29 +1,28 @@
|
||||
import produce from 'immer';
|
||||
import {
|
||||
LOGOUT_ERROR,
|
||||
LOGOUT_SUCCESS,
|
||||
LOGOUT,
|
||||
} from './constants';
|
||||
|
||||
import { LOGIN_SUCCESS } from 'pages/Login/constants'
|
||||
import { REGISTER_SUCCESS } from 'pages/Register/constants'
|
||||
|
||||
export const initialState = {
|
||||
isLogged: true,
|
||||
isLogged: false,
|
||||
notifications: [],
|
||||
tokens: {
|
||||
access: {
|
||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZmQxMDExNGM3NDQ0MTJlZDQ3Y2IzZDEiLCJpYXQiOjE2MDc1MzI4MzUsImV4cCI6MTYwNzUzNDYzNSwidHlwZSI6ImFjY2VzcyJ9.S19wRggAiJYYK35dFWM_gIWuf5ULajJ2cOaA2V2vwtY',
|
||||
expires: '2020-12-09T17:23:55.763Z'
|
||||
token: '',
|
||||
expires: ''
|
||||
},
|
||||
refresh: {
|
||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZmQxMDExNGM3NDQ0MTJlZDQ3Y2IzZDEiLCJpYXQiOjE2MDc1MzI4MzUsImV4cCI6MTYxMDEyNDgzNSwidHlwZSI6InJlZnJlc2gifQ.NC6BJUDKR3WBUVxo62Swytx4nkc6QtUQ7oYdJHqgDY0',
|
||||
expires: '2021-01-08T16:53:55.763Z'
|
||||
token: '',
|
||||
expires: ''
|
||||
}
|
||||
},
|
||||
user: {
|
||||
role: 'admin',
|
||||
email: 'admin@admin.com',
|
||||
id: '5fd10114c744412ed47cb3d1'
|
||||
role: '',
|
||||
email: '',
|
||||
id: ''
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,9 +35,11 @@ const appReducer = produce((draft, action) => {
|
||||
draft.user = action.user;
|
||||
break;
|
||||
|
||||
case LOGOUT_ERROR:
|
||||
case LOGOUT_SUCCESS:
|
||||
return initialState;
|
||||
case LOGOUT:
|
||||
draft.isLogged = false;
|
||||
draft.tokens = {};
|
||||
draft.user = {};
|
||||
break;
|
||||
}
|
||||
}, initialState);
|
||||
|
||||
|
329
src/containers/BarcodeScanner/Scanner.js
Normal file
329
src/containers/BarcodeScanner/Scanner.js
Normal file
@ -0,0 +1,329 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Paper, IconButton} from "@material-ui/core";
|
||||
import { Close as CloseIcon } from '@material-ui/icons'
|
||||
import PropTypes from 'prop-types'
|
||||
import useStyles from "./styles";
|
||||
|
||||
const Scanner = ({ handleClose, setBarcode }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const onClose = () => {
|
||||
stop()
|
||||
handleClose()
|
||||
}
|
||||
|
||||
let localMediaStream = null;
|
||||
const dimensions = {
|
||||
height: 0,
|
||||
width: 0,
|
||||
start: 0,
|
||||
end: 0
|
||||
}
|
||||
|
||||
const elements = {
|
||||
video: null,
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
canvasg: null,
|
||||
ctxg: null
|
||||
}
|
||||
|
||||
const upc = {
|
||||
'0': [3, 2, 1, 1],
|
||||
'1': [2, 2, 2, 1],
|
||||
'2': [2, 1, 2, 2],
|
||||
'3': [1, 4, 1, 1],
|
||||
'4': [1, 1, 3, 2],
|
||||
'5': [1, 2, 3, 1],
|
||||
'6': [1, 1, 1, 4],
|
||||
'7': [1, 3, 1, 2],
|
||||
'8': [1, 2, 1, 3],
|
||||
'9': [3, 1, 1, 2]
|
||||
};
|
||||
|
||||
const check = {
|
||||
'oooooo': '0',
|
||||
'ooeoee': '1',
|
||||
'ooeeoe': '2',
|
||||
'ooeeeo': '3',
|
||||
'oeooee': '4',
|
||||
'oeeooe': '5',
|
||||
'oeeeoo': '6',
|
||||
'oeoeoe': '7',
|
||||
'oeoeeo': '8',
|
||||
'oeeoeo': '9'
|
||||
}
|
||||
|
||||
const config = {
|
||||
strokeColor: '#f00',
|
||||
start: 0.1,
|
||||
end: 0.9,
|
||||
threshold: 160,
|
||||
quality: 0.45,
|
||||
delay: 100,
|
||||
video: '#barcodevideo',
|
||||
canvas: '#barcodecanvas',
|
||||
canvasg: '#barcodecanvasg'
|
||||
}
|
||||
|
||||
const play = () => {
|
||||
dimensions.height = elements.video.videoHeight;
|
||||
dimensions.width = elements.video.videoWidth;
|
||||
|
||||
dimensions.start = dimensions.width * config.start;
|
||||
dimensions.end = dimensions.width * config.end;
|
||||
|
||||
elements.canvas.width = dimensions.width;
|
||||
elements.canvas.height = dimensions.height;
|
||||
elements.canvasg.width = dimensions.width;
|
||||
elements.canvasg.height = dimensions.height;
|
||||
|
||||
drawLine(elements.ctxg);
|
||||
|
||||
setInterval(() => {
|
||||
const snapshot = takeSnapshot(elements.ctx, elements.video)
|
||||
processSnapshot(snapshot)
|
||||
}, config.delay);
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
elements.video.removeEventListener('canplay', play)
|
||||
elements.video.pause();
|
||||
localMediaStream.getTracks()[0].stop();
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
|
||||
|
||||
elements.video = document.querySelector(config.video);
|
||||
elements.canvas = document.querySelector(config.canvas);
|
||||
elements.ctx = elements.canvas.getContext('2d');
|
||||
elements.canvasg = document.querySelector(config.canvasg);
|
||||
elements.ctxg = elements.canvasg.getContext('2d');
|
||||
|
||||
if (navigator.getUserMedia) {
|
||||
navigator.getUserMedia({audio: false, video: true}, (stream) => {
|
||||
elements.video.srcObject = stream;
|
||||
localMediaStream = stream;
|
||||
}, (error) => {
|
||||
// console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
elements.video.addEventListener('canplay', play)
|
||||
}
|
||||
|
||||
const takeSnapshot = (canvasElement, videoElement) => {
|
||||
canvasElement.drawImage(videoElement, 0, 0, dimensions.width, dimensions.height);
|
||||
return canvasElement.getImageData(dimensions.start, dimensions.height * 0.5, dimensions.end - dimensions.start, 1).data;
|
||||
}
|
||||
|
||||
const processSnapshot = (snapshot) => {
|
||||
|
||||
const bars = [];
|
||||
const pixels = [];
|
||||
let pixelBars = [];
|
||||
|
||||
// convert to grayscale
|
||||
for (let i = 0; i < snapshot.length; i += 4) {
|
||||
pixels.push(Math.round(snapshot[i] * 0.2126 + snapshot[i + 1] * 0.7152 + snapshot[ i + 2] * 0.0722));
|
||||
}
|
||||
|
||||
// normalize and convert to binary
|
||||
const minPixelValue = Math.min(...pixels);
|
||||
const maxPixelValue = Math.max(...pixels);
|
||||
|
||||
const binary = pixels.reduce((arr, val) => {
|
||||
const binaryValue = Math.round((val - minPixelValue) / (maxPixelValue - minPixelValue) * 255) > config.threshold ? 1 : 0
|
||||
return [...arr, binaryValue]
|
||||
}, []);
|
||||
|
||||
// determine bar widths
|
||||
let current = binary[0];
|
||||
let count = 0;
|
||||
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
if (binary[i] === current) {
|
||||
count++;
|
||||
} else {
|
||||
pixelBars.push(count);
|
||||
count = 1;
|
||||
current = binary[i]
|
||||
}
|
||||
}
|
||||
pixelBars.push(count);
|
||||
|
||||
// quality check
|
||||
if (pixelBars.length < (3 + 24 + 5 + 24 + 3 + 1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find starting sequence
|
||||
const minFactor = 0.5;
|
||||
const maxFactor = 1.5;
|
||||
let startIndex = 0;
|
||||
|
||||
for (let i = 3; i < pixelBars.length; i++) {
|
||||
const refLength = (pixelBars[i] + pixelBars[i - 1] + pixelBars[i - 2]) / 3;
|
||||
if (
|
||||
(pixelBars[i] > (minFactor * refLength) || pixelBars[i] < (maxFactor * refLength))
|
||||
&& (pixelBars[i-1] > (minFactor * refLength) || pixelBars[i-1] < (maxFactor * refLength))
|
||||
&& (pixelBars[i-2] > (minFactor * refLength) || pixelBars[i-2] < (maxFactor * refLength))
|
||||
&& (pixelBars[i-3] > 3 * refLength)
|
||||
) {
|
||||
startIndex = i - 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// console.log("startIndex: " + startIndex );
|
||||
// return if no starting sequence found
|
||||
if (startIndex === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// discard leading and trailing patterns
|
||||
pixelBars = pixelBars.slice(startIndex, startIndex + 3 + 24 + 5 + 24 + 3);
|
||||
|
||||
// console.log("pixelBars: " + pixelBars );
|
||||
|
||||
// calculate relative widths
|
||||
const ref = (pixelBars[0] + pixelBars[1] + pixelBars[2]) / 3;
|
||||
|
||||
for (let i = 0; i < pixelBars.length; i++) {
|
||||
bars.push(Math.round(pixelBars[i] / ref * 100) / 100);
|
||||
}
|
||||
|
||||
// analyze pattern
|
||||
analyzePattern(bars);
|
||||
}
|
||||
|
||||
const analyzePattern = (bars) => {
|
||||
|
||||
// console.clear();
|
||||
// console.log("analyzing");
|
||||
// determine parity first digit and reverse sequence if necessary
|
||||
const first = normalize(bars.slice(3, 3 + 4), 7);
|
||||
if (!isOdd(Math.round(first[1] + first[3]))) {
|
||||
bars = bars.reverse();
|
||||
}
|
||||
|
||||
// split into digits
|
||||
const digits = [
|
||||
normalize(bars.slice(3, 3 + 4), 7),
|
||||
normalize(bars.slice(7, 7 + 4), 7),
|
||||
normalize(bars.slice(11, 11 + 4), 7),
|
||||
normalize(bars.slice(15, 15 + 4), 7),
|
||||
normalize(bars.slice(19, 19 + 4), 7),
|
||||
normalize(bars.slice(23, 23 + 4), 7),
|
||||
normalize(bars.slice(32, 32 + 4), 7),
|
||||
normalize(bars.slice(36, 36 + 4), 7),
|
||||
normalize(bars.slice(40, 40 + 4), 7),
|
||||
normalize(bars.slice(44, 44 + 4), 7),
|
||||
normalize(bars.slice(48, 48 + 4), 7),
|
||||
normalize(bars.slice(52, 52 + 4), 7)
|
||||
]
|
||||
|
||||
// console.log("digits: " + digits);
|
||||
|
||||
// determine parity and reverse if necessary
|
||||
const parities = [];
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (parity(digits[i])) {
|
||||
parities.push('o');
|
||||
} else {
|
||||
parities.push('e');
|
||||
digits[i] = digits[i].reverse();
|
||||
}
|
||||
}
|
||||
|
||||
// identify digits
|
||||
const result = [];
|
||||
let quality = 0;
|
||||
|
||||
for (let i = 0; i < digits.length; i++) {
|
||||
|
||||
let distance = 9;
|
||||
let bestKey = '';
|
||||
|
||||
for (let key in upc) {
|
||||
if (maxDistance(digits[i], upc[key]) < distance) {
|
||||
distance = maxDistance(digits[i], upc[key]);
|
||||
bestKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(bestKey);
|
||||
if (distance > quality) {
|
||||
quality = distance;
|
||||
}
|
||||
|
||||
}
|
||||
// console.log("result: " + result);
|
||||
// check digit
|
||||
const checkDigit = check[parities.join('')];
|
||||
|
||||
// console.log("quality: " + quality);
|
||||
// output
|
||||
if(quality < config.quality) {
|
||||
const [barcode] = (checkDigit + result.join('')).match(/\d+/g)
|
||||
console.log(barcode)
|
||||
onClose()
|
||||
setBarcode(barcode)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const normalize = (input, total) => {
|
||||
const sum = input.reduce((acc, val) => acc + val, 0);
|
||||
return input.reduce((acc, val) => [...acc, val / sum * total], []);
|
||||
}
|
||||
|
||||
const isOdd = (num) => num % 2;
|
||||
|
||||
const maxDistance = (a, b) =>
|
||||
a.reduce((max, value, index) => {
|
||||
const current = Math.abs(value - b[index])
|
||||
return current > max ? current : max
|
||||
}, 0)
|
||||
|
||||
const parity = (digit) => isOdd(Math.round(digit[1] + digit[3]))
|
||||
|
||||
const drawLine = (canvasContext) => {
|
||||
canvasContext.strokeStyle = config.strokeColor;
|
||||
canvasContext.lineWidth = 3;
|
||||
canvasContext.beginPath();
|
||||
canvasContext.moveTo(dimensions.start, dimensions.height * 0.5);
|
||||
canvasContext.lineTo(dimensions.end, dimensions.height * 0.5);
|
||||
canvasContext.stroke();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
init()
|
||||
return () => elements.video.removeEventListener('canplay', play, false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Paper className={classes.container}>
|
||||
<div className={classes.close}>
|
||||
<IconButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className={classes.inner}>
|
||||
<video className={classes.video} id="barcodevideo" autoPlay />
|
||||
<canvas className={classes.canvasg} id="barcodecanvasg" />
|
||||
</div>
|
||||
<canvas className={classes.canvas} id="barcodecanvas" />
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
Scanner.propTypes ={
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
setBarcode: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default Scanner;
|
39
src/containers/BarcodeScanner/index.js
Normal file
39
src/containers/BarcodeScanner/index.js
Normal file
@ -0,0 +1,39 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {IconButton} from "@material-ui/core";
|
||||
import {CropFree as CropFreeIcon} from '@material-ui/icons'
|
||||
import Scanner from './Scanner'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import {searchProductByBarcodeAction} from 'pages/Home/actions'
|
||||
|
||||
const BarcodeScanner = () => {
|
||||
const [isScannerOpen, setIsScannerOpen] = useState(false)
|
||||
const [barcode, setBarcode] = useState('')
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const handleOpen = () => {
|
||||
setIsScannerOpen(true)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setIsScannerOpen(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (barcode) {
|
||||
dispatch(searchProductByBarcodeAction({ barcode }))
|
||||
}
|
||||
}, [barcode])
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<IconButton onClick={handleOpen}>
|
||||
<CropFreeIcon />
|
||||
</IconButton>
|
||||
{isScannerOpen && !barcode ? (
|
||||
<Scanner handleClose={handleClose} setBarcode={setBarcode} />
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default BarcodeScanner;
|
37
src/containers/BarcodeScanner/styles.js
Normal file
37
src/containers/BarcodeScanner/styles.js
Normal file
@ -0,0 +1,37 @@
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
zIndex: theme.zIndex.modal,
|
||||
width: `100%`,
|
||||
height: `100%`,
|
||||
position: `absolute`,
|
||||
left: 0,
|
||||
top: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
inner: {
|
||||
position: `relative`,
|
||||
},
|
||||
close: {
|
||||
position: `absolute`,
|
||||
top: 20,
|
||||
right: 20,
|
||||
},
|
||||
video: {
|
||||
height: 400,
|
||||
},
|
||||
canvas: {
|
||||
display: "none",
|
||||
},
|
||||
canvasg: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: 400,
|
||||
},
|
||||
}))
|
||||
|
||||
export default useStyles
|
@ -13,21 +13,22 @@ const stateSelector = createStructuredSelector({
|
||||
isLogged: makeSelectIsLogged()
|
||||
})
|
||||
|
||||
const PrivateRoute = ({ component: Component, ...rest }) => {
|
||||
const PrivateRoute = ({ component: Component, ...rest }) => {
|
||||
const { isLogged } = useSelector(stateSelector)
|
||||
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={props => (
|
||||
isLogged()
|
||||
? <Component {...props} />
|
||||
: <Redirect to={routes.login.path} />
|
||||
)} />
|
||||
isLogged
|
||||
? <Component {...props} />
|
||||
: <Redirect to={routes.login.path} />
|
||||
)} />
|
||||
);
|
||||
};
|
||||
|
||||
PrivateRoute.propTypes = {
|
||||
component: PropTypes.node.isRequired
|
||||
component: PropTypes.elementType.isRequired,
|
||||
}
|
||||
|
||||
export default PrivateRoute;
|
||||
|
@ -1,19 +1,37 @@
|
||||
import React from 'react';
|
||||
import { Container } from '@material-ui/core'
|
||||
import {Route, Switch} from "react-router-dom";
|
||||
import {routes} from "utils";
|
||||
import PrivateRoute from 'containers/PrivateRoute'
|
||||
import useStyles from './styles'
|
||||
|
||||
const Routes = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
{Object.values(routes).map(({ exact, path, component }, index) => (
|
||||
<Route
|
||||
key={index}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
</Switch>
|
||||
<Container className={classes.container}>
|
||||
<Switch>
|
||||
<React.Fragment>
|
||||
{Object.values(routes).map(({privateRoute, exact, path, component, }, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{privateRoute ? (
|
||||
<PrivateRoute
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
) : (
|
||||
<Route
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</React.Fragment>
|
||||
</Switch>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
12
src/containers/Routes/styles.js
Normal file
12
src/containers/Routes/styles.js
Normal file
@ -0,0 +1,12 @@
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
marginTop: theme.spacing(10),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}))
|
||||
|
||||
export default useStyles
|
26
src/pages/CreateProfile/FormModel/formInitialValues.js
Normal file
26
src/pages/CreateProfile/FormModel/formInitialValues.js
Normal file
@ -0,0 +1,26 @@
|
||||
import checkoutFormModel from './profileFormModel';
|
||||
const {
|
||||
formField: {
|
||||
gender,
|
||||
goal,
|
||||
birthday,
|
||||
height,
|
||||
currentWeight,
|
||||
goalWeight,
|
||||
rateOfChange,
|
||||
activity
|
||||
}
|
||||
} = checkoutFormModel;
|
||||
|
||||
const formInitialValues = {
|
||||
[gender.name]: '',
|
||||
[goal.name]: '',
|
||||
[birthday.name]: '',
|
||||
[height.name]: '',
|
||||
[currentWeight.name]: '',
|
||||
[goalWeight.name]: '',
|
||||
[rateOfChange.name]: '',
|
||||
[activity.name]: '',
|
||||
};
|
||||
|
||||
export default formInitialValues
|
51
src/pages/CreateProfile/FormModel/profileFormModel.js
Normal file
51
src/pages/CreateProfile/FormModel/profileFormModel.js
Normal file
@ -0,0 +1,51 @@
|
||||
const profileFormModel = {
|
||||
formId: 'createProfileForm',
|
||||
formField: {
|
||||
gender: {
|
||||
name: 'gender',
|
||||
label: 'Gender*',
|
||||
requiredErrorMsg: 'Gender is required'
|
||||
},
|
||||
goal: {
|
||||
name: 'goal',
|
||||
label: 'Goal*',
|
||||
requiredErrorMsg: 'Goal is required'
|
||||
},
|
||||
birthday: {
|
||||
name: 'birthday',
|
||||
label: 'Birthday*',
|
||||
requiredErrorMsg: 'Birthday date is required',
|
||||
invalidErrorMsg: 'Birthday date is not valid'
|
||||
},
|
||||
height: {
|
||||
name: 'height',
|
||||
label: 'Height*',
|
||||
requiredErrorMsg: 'Height is required',
|
||||
invalidErrorMsg: 'Height is invalid (e.g. 175)'
|
||||
},
|
||||
currentWeight: {
|
||||
name: 'currentWeight',
|
||||
label: 'Current weight*',
|
||||
requiredErrorMsg: 'Current weight is required',
|
||||
invalidErrorMsg: 'Current weight is invalid (e.g. 75)'
|
||||
},
|
||||
goalWeight: {
|
||||
name: 'goalWeight',
|
||||
label: 'Goal weight*',
|
||||
requiredErrorMsg: 'Goal weight is required',
|
||||
invalidErrorMsg: 'Goal weight is invalid (e.g. 70)'
|
||||
},
|
||||
rateOfChange: {
|
||||
name: 'rateOfChange',
|
||||
label: 'Rate of change*',
|
||||
requiredErrorMsg: 'Rate of change is required'
|
||||
},
|
||||
activity: {
|
||||
name: 'activity',
|
||||
label: 'Activity*',
|
||||
requiredErrorMsg: 'Activity is required'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default profileFormModel
|
46
src/pages/CreateProfile/FormModel/validationSchema.js
Normal file
46
src/pages/CreateProfile/FormModel/validationSchema.js
Normal file
@ -0,0 +1,46 @@
|
||||
import * as Yup from 'yup';
|
||||
import checkoutFormModel from './profileFormModel';
|
||||
const {
|
||||
formField: {
|
||||
gender,
|
||||
goal,
|
||||
birthday,
|
||||
height,
|
||||
currentWeight,
|
||||
goalWeight,
|
||||
rateOfChange,
|
||||
activity
|
||||
}
|
||||
} = checkoutFormModel;
|
||||
|
||||
const validationSchema = [
|
||||
Yup.object().shape({
|
||||
[goal.name]: Yup.string().required(`${goal.requiredErrorMsg}`),
|
||||
[rateOfChange.name]: Yup.string().required(`${goal.requiredErrorMsg}`),
|
||||
[activity.name]: Yup.string().required(`${activity.requiredErrorMsg}`),
|
||||
}),
|
||||
Yup.object().shape({
|
||||
[gender.name]: Yup.string().required(`${gender.requiredErrorMsg}`),
|
||||
[birthday.name]: Yup.string().required(`${birthday.requiredErrorMsg}`),
|
||||
[height.name]: Yup.string().required(`${height.requiredErrorMsg}`)
|
||||
.test(
|
||||
'len',
|
||||
`${height.invalidErrorMsg}`,
|
||||
value => value && value.toString().length === 3
|
||||
),
|
||||
[currentWeight.name]: Yup.string().required(`${currentWeight.requiredErrorMsg}`)
|
||||
.test(
|
||||
'len',
|
||||
`${currentWeight.invalidErrorMsg}`,
|
||||
value => value && value.toString().length >= 2 && value.toString().length <= 3
|
||||
),
|
||||
[goalWeight.name]: Yup.string().required(`${goalWeight.requiredErrorMsg}`)
|
||||
.test(
|
||||
'len',
|
||||
`${goalWeight.invalidErrorMsg}`,
|
||||
value => value && value.toString().length >= 2 && value.toString().length <= 3
|
||||
),
|
||||
})
|
||||
];
|
||||
|
||||
export default validationSchema
|
@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import {Grid, Typography, Box} from '@material-ui/core';
|
||||
import {Grid, Typography} from '@material-ui/core';
|
||||
import {useInjectReducer} from "redux-injectors";
|
||||
import { useSelector } from 'react-redux';
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import reducer from "pages/Profile/reducer";
|
||||
import reducer from "pages/CreateProfile/reducer";
|
||||
import {
|
||||
makeSelectGoals,
|
||||
makeSelectActivities,
|
||||
makeSelectRatesOfChange,
|
||||
} from "pages/Profile/selectors";
|
||||
} from "pages/CreateProfile/selectors";
|
||||
|
||||
import SelectField from 'components/SelectField';
|
||||
|
||||
@ -19,7 +19,13 @@ const stateSelector = createStructuredSelector({
|
||||
});
|
||||
|
||||
const key = 'profilePage'
|
||||
const GoalForm = () => {
|
||||
const GoalForm = ({
|
||||
formField: {
|
||||
rateOfChange,
|
||||
goal,
|
||||
activity
|
||||
}
|
||||
}) => {
|
||||
useInjectReducer({ key, reducer });
|
||||
const { goals, activities, ratesOfChange } = useSelector(stateSelector)
|
||||
|
||||
@ -30,19 +36,19 @@ const GoalForm = () => {
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<SelectField label="goal" name="goal" data={goals} fullWidth />
|
||||
<SelectField label={goal.label} name={goal.name} data={goals} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Activity
|
||||
</Typography>
|
||||
<SelectField label="activity" name="activity" data={activities} fullWidth />
|
||||
<SelectField label={activity.label} name={activity.name} data={activities} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Rate of change
|
||||
</Typography>
|
||||
<SelectField label="Rate of Change" name="rateOfChange" data={ratesOfChange} fullWidth />
|
||||
<SelectField label={rateOfChange.label} name={rateOfChange.name} data={ratesOfChange} fullWidth />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
@ -1,17 +1,17 @@
|
||||
import 'date-fns';
|
||||
import React from 'react';
|
||||
import {Typography, InputLabel, Grid, InputAdornment, Select, FormControl, MenuItem, TextField, Slider} from '@material-ui/core';
|
||||
import {Typography, Grid, InputAdornment} from '@material-ui/core';
|
||||
import DatePickerField from 'components/DatePickerField'
|
||||
import InputField from 'components/InputField'
|
||||
import {useInjectReducer} from "redux-injectors";
|
||||
import { useSelector } from 'react-redux';
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import reducer from "pages/Profile/reducer";
|
||||
import reducer from "pages/CreateProfile/reducer";
|
||||
import {
|
||||
makeSelectActivities,
|
||||
makeSelectGenders,
|
||||
} from "pages/Profile/selectors";
|
||||
import SelectField from "../../../components/SelectField";
|
||||
} from "pages/CreateProfile/selectors";
|
||||
import SelectField from "components/SelectField";
|
||||
|
||||
const stateSelector = createStructuredSelector({
|
||||
activities: makeSelectActivities(),
|
||||
@ -19,7 +19,15 @@ const stateSelector = createStructuredSelector({
|
||||
});
|
||||
|
||||
const key = 'profilePage'
|
||||
const PersonalDetailsForm = () => {
|
||||
const Index = ({
|
||||
formField: {
|
||||
gender,
|
||||
birthday,
|
||||
height,
|
||||
currentWeight,
|
||||
goalWeight,
|
||||
}
|
||||
}) => {
|
||||
useInjectReducer({ key, reducer });
|
||||
const { genders } = useSelector(stateSelector)
|
||||
|
||||
@ -30,12 +38,12 @@ const PersonalDetailsForm = () => {
|
||||
</Typography>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<SelectField label="gender" name="gender" data={genders} fullWidth />
|
||||
<SelectField label={gender.label} name={gender.name} data={genders} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<DatePickerField
|
||||
name="birthday"
|
||||
label="birthday"
|
||||
label={birthday.label}
|
||||
name={birthday.name}
|
||||
format="MM/yy"
|
||||
views={['year', 'month']}
|
||||
minDate={new Date('1900/01/01')}
|
||||
@ -46,8 +54,8 @@ const PersonalDetailsForm = () => {
|
||||
<Grid item xs={12}>
|
||||
<InputField
|
||||
type="number"
|
||||
name="height"
|
||||
label="Height"
|
||||
label={height.label}
|
||||
name={height.name}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
@ -58,8 +66,8 @@ const PersonalDetailsForm = () => {
|
||||
<Grid item xs={12}>
|
||||
<InputField
|
||||
type="number"
|
||||
label="Current Weight"
|
||||
name="weight.current"
|
||||
label={currentWeight.label}
|
||||
name={currentWeight.name}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
@ -70,8 +78,8 @@ const PersonalDetailsForm = () => {
|
||||
<Grid item xs={12}>
|
||||
<InputField
|
||||
type="number"
|
||||
label="Goal Weight"
|
||||
name="weight.goal"
|
||||
label={goalWeight.label}
|
||||
name={goalWeight.name}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
@ -83,4 +91,4 @@ const PersonalDetailsForm = () => {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
export default PersonalDetailsForm
|
||||
export default Index
|
98
src/pages/CreateProfile/actions.js
Normal file
98
src/pages/CreateProfile/actions.js
Normal file
@ -0,0 +1,98 @@
|
||||
import {
|
||||
CREATE_PROFILE_REQUEST,
|
||||
CREATE_PROFILE_SUCCESS,
|
||||
CREATE_PROFILE_ERROR,
|
||||
PROFILE_INPUT_CHANGE,
|
||||
GET_PROFILE_REQUEST,
|
||||
GET_PROFILE_SUCCESS,
|
||||
GET_PROFILE_ERROR,
|
||||
UPDATE_PROFILE_REQUEST,
|
||||
UPDATE_PROFILE_SUCCESS,
|
||||
UPDATE_PROFILE_ERROR
|
||||
} from './constants';
|
||||
|
||||
export const profileInputChange = ({name, value}) => ({
|
||||
type: PROFILE_INPUT_CHANGE,
|
||||
name,
|
||||
value,
|
||||
})
|
||||
|
||||
export const getProfileAction = () => ({
|
||||
type: GET_PROFILE_REQUEST,
|
||||
})
|
||||
|
||||
export const getProfileSuccessAction = ({birthday, gender, height, weight, goalWeight, rateOfChange, activity}) => ({
|
||||
type: GET_PROFILE_SUCCESS,
|
||||
birthday,
|
||||
gender,
|
||||
height,
|
||||
weight,
|
||||
goalWeight,
|
||||
rateOfChange,
|
||||
activity
|
||||
})
|
||||
|
||||
export const getProfileErrorAction = ({error}) => ({
|
||||
type: GET_PROFILE_ERROR,
|
||||
error,
|
||||
})
|
||||
|
||||
export const updateProfileAction = () => ({
|
||||
type: UPDATE_PROFILE_REQUEST,
|
||||
})
|
||||
|
||||
export const updateProfileSuccessAction = ({birthday, goal, gender, height, currentWeight, goalWeight, rateOfChange, activity}) => ({
|
||||
type: UPDATE_PROFILE_SUCCESS,
|
||||
birthday,
|
||||
gender,
|
||||
goal,
|
||||
height,
|
||||
currentWeight,
|
||||
goalWeight,
|
||||
rateOfChange,
|
||||
activity
|
||||
})
|
||||
|
||||
export const updateProfileErrorAction = ({error}) => ({
|
||||
type: UPDATE_PROFILE_ERROR,
|
||||
error,
|
||||
})
|
||||
|
||||
export const createProfileAction = ({
|
||||
birthday,
|
||||
gender,
|
||||
goal,
|
||||
height,
|
||||
currentWeight,
|
||||
goalWeight,
|
||||
rateOfChange,
|
||||
activity
|
||||
}) => ({
|
||||
type: CREATE_PROFILE_REQUEST,
|
||||
birthday,
|
||||
gender,
|
||||
goal,
|
||||
height,
|
||||
currentWeight,
|
||||
goalWeight,
|
||||
rateOfChange,
|
||||
activity
|
||||
})
|
||||
|
||||
export const createProfileSuccessAction = ({birthday, goal, gender, height, currentWeight, goalWeight, rateOfChange, activity}) => ({
|
||||
type: CREATE_PROFILE_SUCCESS,
|
||||
birthday,
|
||||
gender,
|
||||
goal,
|
||||
height,
|
||||
currentWeight,
|
||||
goalWeight,
|
||||
rateOfChange,
|
||||
activity
|
||||
})
|
||||
|
||||
export const createProfileErrorAction = ({error}) => ({
|
||||
type: CREATE_PROFILE_ERROR,
|
||||
error,
|
||||
})
|
||||
|
13
src/pages/CreateProfile/constants.js
Normal file
13
src/pages/CreateProfile/constants.js
Normal file
@ -0,0 +1,13 @@
|
||||
export const GET_PROFILE_REQUEST = 'app/CreateProfilePage/GET_PROFILE_REQUEST';
|
||||
export const GET_PROFILE_SUCCESS = 'app/CreateProfilePage/GET_PROFILE_SUCCESS';
|
||||
export const GET_PROFILE_ERROR = 'app/CreateProfilePage/GET_PROFILE_ERROR';
|
||||
|
||||
export const CREATE_PROFILE_REQUEST = 'app/CreateProfilePage/CREATE_PROFILE_REQUEST';
|
||||
export const CREATE_PROFILE_SUCCESS = 'app/CreateProfilePage/CREATE_PROFILE_SUCCESS';
|
||||
export const CREATE_PROFILE_ERROR = 'app/CreateProfilePage/CREATE_PROFILE_ERROR';
|
||||
|
||||
export const UPDATE_PROFILE_REQUEST = 'app/CreateProfilePage/UPDATE_PROFILE_REQUEST';
|
||||
export const UPDATE_PROFILE_SUCCESS = 'app/CreateProfilePage/UPDATE_PROFILE_SUCCESS';
|
||||
export const UPDATE_PROFILE_ERROR = 'app/CreateProfilePage/UPDATE_PROFILE_ERROR';
|
||||
|
||||
export const PROFILE_INPUT_CHANGE = 'app/CreateProfilePage/PROFILE_INPUT_CHANGE';
|
120
src/pages/CreateProfile/index.js
Normal file
120
src/pages/CreateProfile/index.js
Normal file
@ -0,0 +1,120 @@
|
||||
import React, { useState } from 'react';
|
||||
import {Paper, Stepper, Step, StepLabel, Button, Typography } from '@material-ui/core';
|
||||
import {useInjectReducer, useInjectSaga} from "redux-injectors";
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Formik, Form } from 'formik';
|
||||
import GoalForm from './Forms/GoalForm';
|
||||
import PersonalDetailsForm from './Forms/PersonalDetailsForm';
|
||||
import reducer from "./reducer";
|
||||
import saga from "./saga";
|
||||
import { createProfileAction } from './actions'
|
||||
import useStyles from './styles';
|
||||
import validationSchema from './FormModel/validationSchema'
|
||||
import formInitialValues from './FormModel/formInitialValues'
|
||||
import profileFormModel from './FormModel/profileFormModel'
|
||||
|
||||
const steps = ['Your goal', 'Personal details'];
|
||||
const { formId, formField } = profileFormModel;
|
||||
|
||||
const renderStepContent = (step) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return <GoalForm formField={formField} />;
|
||||
case 1:
|
||||
return <PersonalDetailsForm formField={formField} />;
|
||||
default:
|
||||
throw new Error('Unknown step');
|
||||
}
|
||||
}
|
||||
|
||||
const key = 'createProfilePage'
|
||||
const ProfilePage = () => {
|
||||
useInjectReducer({ key, reducer });
|
||||
useInjectSaga({ key, saga });
|
||||
|
||||
const classes = useStyles();
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const currentValidationSchema = validationSchema[activeStep];
|
||||
const isLastStep = activeStep === steps.length - 1;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const submitForm = (values, actions) => {
|
||||
dispatch(createProfileAction(values))
|
||||
actions.setSubmitting(false);
|
||||
setActiveStep(activeStep + 1);
|
||||
}
|
||||
|
||||
const handleSubmit = (values, actions) => {
|
||||
if (isLastStep) {
|
||||
submitForm(values, actions);
|
||||
} else {
|
||||
setActiveStep(activeStep + 1);
|
||||
actions.setTouched({});
|
||||
actions.setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
setActiveStep(activeStep - 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<Typography component="h1" variant="h4" align="center">
|
||||
Profile
|
||||
</Typography>
|
||||
<Stepper activeStep={activeStep} className={classes.stepper}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
<React.Fragment>
|
||||
{activeStep === steps.length ? (
|
||||
<React.Fragment>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Thank you for your order.
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">
|
||||
Your order number is #2001539. We have emailed your order confirmation, and will
|
||||
send you an update when your order has shipped.
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Formik
|
||||
initialValues={formInitialValues}
|
||||
validationSchema={currentValidationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form id={formId}>
|
||||
{renderStepContent(activeStep)}
|
||||
<div className={classes.buttons}>
|
||||
{activeStep !== 0 && (
|
||||
<Button onClick={handleBack} className={classes.button}>
|
||||
back
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
>
|
||||
{isLastStep ? 'Create profile' : 'Next'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</Paper>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfilePage
|
79
src/pages/CreateProfile/reducer.js
Normal file
79
src/pages/CreateProfile/reducer.js
Normal file
@ -0,0 +1,79 @@
|
||||
import produce from 'immer';
|
||||
import {
|
||||
CREATE_PROFILE_ERROR,
|
||||
GET_PROFILE_REQUEST,
|
||||
GET_PROFILE_SUCCESS,
|
||||
GET_PROFILE_ERROR,
|
||||
UPDATE_PROFILE_REQUEST,
|
||||
CREATE_PROFILE_REQUEST,
|
||||
UPDATE_PROFILE_ERROR
|
||||
} from './constants';
|
||||
|
||||
export const initialState = {
|
||||
activities: [
|
||||
{ label: 'None', value: undefined },
|
||||
{ label: 'very low', value: 1.2 },
|
||||
{ label: 'low', value: 1.375 },
|
||||
{ label: 'medium', value: 1.55 },
|
||||
{ label: 'high', value: 1.725 },
|
||||
{ label: 'very high', value: 1.9 },
|
||||
],
|
||||
goals: [
|
||||
{ label: 'None', value: undefined, },
|
||||
{ label: 'lose weight', value: 'lose_weight', },
|
||||
{ label: 'maintain weight', value: 'maintain_weight', },
|
||||
{ label: 'put on weight', value: 'put_on_weight', }
|
||||
],
|
||||
ratesOfChange: [
|
||||
{ label: 'None', value: undefined },
|
||||
{ value: 1, label: '0 kg' },
|
||||
{ value: 2, label: '0.5 kg' },
|
||||
{ value: 3, label: '1 kg' },
|
||||
],
|
||||
genders: [
|
||||
{ label: 'None', value: undefined },
|
||||
{value: 'male', label: 'male' },
|
||||
{value: 'female', label: 'female' },
|
||||
],
|
||||
isLoading: false,
|
||||
error: {},
|
||||
gender: '',
|
||||
goal: 0,
|
||||
birthday: new Date(),
|
||||
height: 0,
|
||||
currentWeight: 0,
|
||||
goalWeight: 0,
|
||||
rateOfChange: 0,
|
||||
activity: 0,
|
||||
};
|
||||
|
||||
const loginPageReducer = produce((draft, action) => {
|
||||
switch(action.type) {
|
||||
case CREATE_PROFILE_REQUEST:
|
||||
case GET_PROFILE_SUCCESS:
|
||||
draft.birthday = action.birthday;
|
||||
draft.gender = action.gender;
|
||||
draft.goal = action.goal;
|
||||
draft.height = action.height;
|
||||
draft.currentWeight = action.currentWeight;
|
||||
draft.goalWeight = action.goalWeight;
|
||||
draft.rateOfChange = action.rateOfChange;
|
||||
draft.activity = action.activity;
|
||||
draft.isLoading = false;
|
||||
break;
|
||||
|
||||
case GET_PROFILE_REQUEST:
|
||||
case UPDATE_PROFILE_REQUEST:
|
||||
draft.isLoading = true;
|
||||
break;
|
||||
|
||||
case GET_PROFILE_ERROR:
|
||||
case CREATE_PROFILE_ERROR:
|
||||
case UPDATE_PROFILE_ERROR:
|
||||
draft.isLoading = false;
|
||||
draft.error = action.error;
|
||||
break;
|
||||
}
|
||||
}, initialState);
|
||||
|
||||
export default loginPageReducer;
|
99
src/pages/CreateProfile/saga.js
Normal file
99
src/pages/CreateProfile/saga.js
Normal file
@ -0,0 +1,99 @@
|
||||
import { takeLatest, call, put, select } from 'redux-saga/effects';
|
||||
import {api, request, routes} from 'utils';
|
||||
import { GET_PROFILE_REQUEST, UPDATE_PROFILE_REQUEST, CREATE_PROFILE_REQUEST } from './constants';
|
||||
import {
|
||||
makeSelectBirthday,
|
||||
makeSelectHeight,
|
||||
makeSelectCurrentWeight,
|
||||
makeSelectGoalWeight,
|
||||
makeSelectRateOfChange,
|
||||
makeSelectActivity,
|
||||
makeSelectGoal,
|
||||
makeSelectGender
|
||||
} from './selectors';
|
||||
import { createProfileSuccessAction, createProfileErrorAction, updateProfileErrorAction, updateProfileSuccessAction, getProfileErrorAction, getProfileSuccessAction } from './actions';
|
||||
|
||||
import { makeSelectTokens } from 'containers/App/selectors'
|
||||
import {push} from "connected-react-router";
|
||||
|
||||
export function* getProfile() {
|
||||
const { access } = yield select(makeSelectTokens());
|
||||
|
||||
const requestURL = api.profile;
|
||||
|
||||
const requestParameters = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${access.token}`,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const { birthday, gender, height, currentWeight, goalWeight, rateOfChange, activity } = yield call(request, requestURL, requestParameters);
|
||||
yield put(getProfileSuccessAction({birthday, gender, height, currentWeight, goalWeight, rateOfChange, activity}));
|
||||
} catch (error) {
|
||||
yield put(getProfileErrorAction({ error: error.message }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* updateProfile() {
|
||||
const { access } = yield select(makeSelectTokens());
|
||||
|
||||
const gender = yield select(makeSelectGender());
|
||||
const goal = yield select(makeSelectGoal());
|
||||
const birthday = yield select(makeSelectBirthday());
|
||||
const height = yield select(makeSelectHeight());
|
||||
const currentWeight = yield select(makeSelectCurrentWeight());
|
||||
const goalWeight = yield select(makeSelectGoalWeight());
|
||||
const rateOfChange = yield select(makeSelectRateOfChange());
|
||||
const activity = yield select(makeSelectActivity());
|
||||
|
||||
const requestURL = api.profile;
|
||||
|
||||
const requestParameters = {
|
||||
method: 'PUT',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${access.token}`, },
|
||||
body: JSON.stringify({ birthday, gender, height, currentWeight, goalWeight, goal, rateOfChange, activity }),
|
||||
};
|
||||
|
||||
try {
|
||||
const { birthday, gender, height, currentWeight, goalWeight, rateOfChange, activity } = yield call(request, requestURL, requestParameters);
|
||||
yield put(updateProfileSuccessAction({birthday, gender, height, currentWeight, goalWeight, rateOfChange, activity}));
|
||||
} catch (error) {
|
||||
yield put(updateProfileErrorAction({error: error.message}));
|
||||
}
|
||||
}
|
||||
|
||||
export function* createProfile() {
|
||||
const { access } = yield select(makeSelectTokens());
|
||||
const gender = yield select(makeSelectGender());
|
||||
const goal = yield select(makeSelectGoal());
|
||||
const birthday = yield select(makeSelectBirthday());
|
||||
const height = yield select(makeSelectHeight());
|
||||
const currentWeight = yield select(makeSelectCurrentWeight());
|
||||
const goalWeight = yield select(makeSelectGoalWeight());
|
||||
const rateOfChange = yield select(makeSelectRateOfChange());
|
||||
const activity = yield select(makeSelectActivity());
|
||||
|
||||
const requestURL = api.profile;
|
||||
|
||||
const requestParameters = {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${access.token}`, },
|
||||
body: JSON.stringify({ birthday, gender, height, currentWeight, goalWeight, goal, rateOfChange, activity }),
|
||||
};
|
||||
|
||||
try {
|
||||
const { birthday, gender, height, currentWeight, goalWeight, rateOfChange, activity } = yield call(request, requestURL, requestParameters);
|
||||
yield put(createProfileSuccessAction({birthday, gender, height, currentWeight, goalWeight, rateOfChange, activity}));
|
||||
yield put(push(routes.dashboard.path));
|
||||
} catch (error) {
|
||||
yield put(createProfileErrorAction({error: error.message}));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* profilePageSaga() {
|
||||
yield takeLatest(GET_PROFILE_REQUEST, getProfile);
|
||||
yield takeLatest(UPDATE_PROFILE_REQUEST, updateProfile);
|
||||
yield takeLatest(CREATE_PROFILE_REQUEST, createProfile);
|
||||
}
|
65
src/pages/CreateProfile/selectors.js
Normal file
65
src/pages/CreateProfile/selectors.js
Normal file
@ -0,0 +1,65 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { initialState } from './reducer';
|
||||
|
||||
const selectProfilePageDomain = (state) => state.profilePage || initialState;
|
||||
|
||||
const makeSelectError = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.error);
|
||||
|
||||
const makeSelectBirthday = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.birthday);
|
||||
|
||||
const makeSelectHeight = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.height)
|
||||
|
||||
const makeSelectCurrentWeight = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.currentWeight);
|
||||
|
||||
const makeSelectGoalWeight = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.goalWeight);
|
||||
|
||||
const makeSelectRateOfChange = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.rateOfChange);
|
||||
|
||||
const makeSelectActivity = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.activity);
|
||||
|
||||
const makeSelectGoal = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.goal);
|
||||
|
||||
const makeSelectGender = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.gender);
|
||||
|
||||
const makeSelectIsLoading = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.isLoading);
|
||||
|
||||
const makeSelectGoals = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.goals);
|
||||
|
||||
const makeSelectActivities = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.activities);
|
||||
|
||||
const makeSelectGenders = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.genders);
|
||||
|
||||
const makeSelectRatesOfChange = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.ratesOfChange);
|
||||
|
||||
|
||||
export {
|
||||
selectProfilePageDomain,
|
||||
makeSelectError,
|
||||
makeSelectBirthday,
|
||||
makeSelectHeight,
|
||||
makeSelectCurrentWeight,
|
||||
makeSelectGoalWeight,
|
||||
makeSelectRateOfChange,
|
||||
makeSelectActivity,
|
||||
makeSelectGoal,
|
||||
makeSelectGender,
|
||||
makeSelectIsLoading,
|
||||
makeSelectGoals,
|
||||
makeSelectActivities,
|
||||
makeSelectGenders,
|
||||
makeSelectRatesOfChange
|
||||
};
|
37
src/pages/CreateProfile/styles.js
Normal file
37
src/pages/CreateProfile/styles.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
layout: {
|
||||
width: 'auto',
|
||||
marginLeft: theme.spacing(2),
|
||||
marginRight: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(2) * 2)]: {
|
||||
width: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
padding: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(3) * 2)]: {
|
||||
marginTop: theme.spacing(6),
|
||||
marginBottom: theme.spacing(6),
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
},
|
||||
stepper: {
|
||||
padding: theme.spacing(3, 0, 5),
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
button: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default useStyles
|
142
src/pages/Home/actions.js
Normal file
142
src/pages/Home/actions.js
Normal file
@ -0,0 +1,142 @@
|
||||
import {
|
||||
CREATE_MEAL_REQUEST,
|
||||
CREATE_MEAL_SUCCESS,
|
||||
CREATE_MEAL_ERROR,
|
||||
GET_MEALS_REQUEST,
|
||||
GET_MEALS_SUCCESS,
|
||||
GET_MEALS_ERROR,
|
||||
UPDATE_MEAL_REQUEST,
|
||||
UPDATE_MEAL_SUCCESS,
|
||||
UPDATE_MEAL_ERROR,
|
||||
ADD_PRODUCTS_TO_MEAL_REQUEST,
|
||||
ADD_PRODUCTS_TO_MEAL_SUCCESS,
|
||||
ADD_PRODUCTS_TO_MEAL_ERROR,
|
||||
SEARCH_PRODUCT_BY_LABEL_REQUEST,
|
||||
SEARCH_PRODUCT_BY_BARCODE_REQUEST,
|
||||
SEARCH_PRODUCT_BY_BARCODE_SUCCESS,
|
||||
SEARCH_PRODUCT_BY_BARCODE_ERROR,
|
||||
SEARCH_PRODUCT_BY_LABEL_SUCCESS,
|
||||
SEARCH_PRODUCT_BY_LABEL_ERROR,
|
||||
REMOVE_PRODUCT_FROM_MEAL_REQUEST,
|
||||
REMOVE_PRODUCT_FROM_MEAL_SUCCESS,
|
||||
REMOVE_PRODUCT_FROM_MEAL_ERROR,
|
||||
SET_SELECTED_MEAL,
|
||||
} from './constants';
|
||||
|
||||
export const setSelectedMealAction = ({ label, id }) => ({
|
||||
type: SET_SELECTED_MEAL,
|
||||
label,
|
||||
id,
|
||||
})
|
||||
|
||||
export const searchProductByLabelAction = ({ label }) => ({
|
||||
type: SEARCH_PRODUCT_BY_LABEL_REQUEST,
|
||||
label,
|
||||
})
|
||||
|
||||
export const searchProductByLabelSuccessAction = ({ products }) => ({
|
||||
type: SEARCH_PRODUCT_BY_LABEL_SUCCESS,
|
||||
products,
|
||||
})
|
||||
|
||||
export const searchProductByLabelErrorAction = ({ error }) => ({
|
||||
type: SEARCH_PRODUCT_BY_LABEL_ERROR,
|
||||
error,
|
||||
})
|
||||
|
||||
export const searchProductByBarcodeAction = ({ barcode }) => ({
|
||||
type: SEARCH_PRODUCT_BY_BARCODE_REQUEST,
|
||||
barcode,
|
||||
})
|
||||
|
||||
export const searchProductByBarcodeSuccessAction = ({ products }) => ({
|
||||
type: SEARCH_PRODUCT_BY_BARCODE_SUCCESS,
|
||||
products,
|
||||
})
|
||||
|
||||
export const searchProductByBarcodeErrorAction = ({ error }) => ({
|
||||
type: SEARCH_PRODUCT_BY_BARCODE_ERROR,
|
||||
error,
|
||||
})
|
||||
|
||||
export const getMealsAction = ({ date }) => ({
|
||||
type: GET_MEALS_REQUEST,
|
||||
date,
|
||||
})
|
||||
|
||||
export const getMealsSuccessAction = ({ meals, dailyCalories, dailyFats, dailyProteins, dailyCarbs }) => ({
|
||||
type: GET_MEALS_SUCCESS,
|
||||
meals,
|
||||
dailyCalories,
|
||||
dailyFats,
|
||||
dailyProteins,
|
||||
dailyCarbs,
|
||||
})
|
||||
|
||||
export const getMealsErrorAction = ({ error }) => ({
|
||||
type: GET_MEALS_ERROR,
|
||||
error,
|
||||
})
|
||||
|
||||
export const updateMealAction = () => ({
|
||||
type: UPDATE_MEAL_REQUEST,
|
||||
})
|
||||
|
||||
export const updateMealSuccessAction = ({ meal }) => ({
|
||||
type: UPDATE_MEAL_SUCCESS,
|
||||
meal
|
||||
})
|
||||
|
||||
export const updateMealErrorAction = ({error}) => ({
|
||||
type: UPDATE_MEAL_ERROR,
|
||||
error,
|
||||
})
|
||||
|
||||
export const createMealAction = ({ label, products, date }) => ({
|
||||
type: CREATE_MEAL_REQUEST,
|
||||
label,
|
||||
products,
|
||||
date,
|
||||
})
|
||||
|
||||
export const createMealSuccessAction = ({ meal }) => ({
|
||||
type: CREATE_MEAL_SUCCESS,
|
||||
meal,
|
||||
})
|
||||
|
||||
export const createMealErrorAction = ({error}) => ({
|
||||
type: CREATE_MEAL_ERROR,
|
||||
error,
|
||||
})
|
||||
|
||||
export const addProductsToMealAction = ({ id, products }) => ({
|
||||
type: ADD_PRODUCTS_TO_MEAL_REQUEST,
|
||||
products,
|
||||
id,
|
||||
})
|
||||
|
||||
export const addProductsToSuccessAction = ({ meal }) => ({
|
||||
type: ADD_PRODUCTS_TO_MEAL_SUCCESS,
|
||||
meal
|
||||
})
|
||||
|
||||
export const addProductsToMealErrorAction = ({error}) => ({
|
||||
type: ADD_PRODUCTS_TO_MEAL_ERROR,
|
||||
error,
|
||||
})
|
||||
|
||||
export const removeProductFromMealAction = ({ mealId, productId }) => ({
|
||||
type: REMOVE_PRODUCT_FROM_MEAL_REQUEST,
|
||||
mealId,
|
||||
productId
|
||||
})
|
||||
|
||||
export const removeProductFromMealSuccessAction = ({ meal }) => ({
|
||||
type: REMOVE_PRODUCT_FROM_MEAL_SUCCESS,
|
||||
meal,
|
||||
})
|
||||
|
||||
export const removeProductFromMealErrorAction = ({error}) => ({
|
||||
type: REMOVE_PRODUCT_FROM_MEAL_ERROR,
|
||||
error,
|
||||
})
|
29
src/pages/Home/constants.js
Normal file
29
src/pages/Home/constants.js
Normal file
@ -0,0 +1,29 @@
|
||||
export const GET_MEALS_REQUEST = 'app/HomePage/GET_MEALS_REQUEST'
|
||||
export const GET_MEALS_SUCCESS = 'app/HomePage/GET_MEALS_SUCCESS';
|
||||
export const GET_MEALS_ERROR = 'app/HomePage/GET_MEALS_ERROR';
|
||||
|
||||
export const CREATE_MEAL_REQUEST = 'app/HomePage/CREATE_MEAL_REQUEST'
|
||||
export const CREATE_MEAL_SUCCESS = 'app/HomePage/CREATE_MEAL_SUCCESS';
|
||||
export const CREATE_MEAL_ERROR = 'app/HomePage/CREATE_MEAL_ERROR';
|
||||
|
||||
export const UPDATE_MEAL_REQUEST = 'app/HomePage/UPDATE_MEAL_REQUEST'
|
||||
export const UPDATE_MEAL_SUCCESS = 'app/HomePage/UPDATE_MEAL_SUCCESS';
|
||||
export const UPDATE_MEAL_ERROR = 'app/HomePage/UPDATE_MEAL_ERROR';
|
||||
|
||||
export const ADD_PRODUCTS_TO_MEAL_REQUEST = 'app/HomePage/ADD_PRODUCTS_TO_MEAL_REQUEST'
|
||||
export const ADD_PRODUCTS_TO_MEAL_SUCCESS = 'app/HomePage/ADD_PRODUCTS_TO_MEAL_SUCCESS';
|
||||
export const ADD_PRODUCTS_TO_MEAL_ERROR = 'app/HomePage/ADD_PRODUCTS_TO_MEAL_ERROR';
|
||||
|
||||
export const REMOVE_PRODUCT_FROM_MEAL_REQUEST = 'app/HomePage/REMOVE_PRODUCT_FROM_MEAL_REQUEST'
|
||||
export const REMOVE_PRODUCT_FROM_MEAL_SUCCESS = 'app/HomePage/REMOVE_PRODUCT_FROM_MEAL_SUCCESS';
|
||||
export const REMOVE_PRODUCT_FROM_MEAL_ERROR = 'app/HomePage/REMOVE_PRODUCT_FROM_MEAL_ERROR';
|
||||
|
||||
export const SEARCH_PRODUCT_BY_LABEL_REQUEST = 'app/HomePage/SEARCH_PRODUCT_BY_LABEL_REQUEST'
|
||||
export const SEARCH_PRODUCT_BY_LABEL_SUCCESS = 'app/HomePage/SEARCH_PRODUCT_BY_LABEL_SUCCESS';
|
||||
export const SEARCH_PRODUCT_BY_LABEL_ERROR = 'app/HomePage/SEARCH_PRODUCT_BY_LABEL_ERROR';
|
||||
|
||||
export const SEARCH_PRODUCT_BY_BARCODE_REQUEST = 'app/HomePage/SEARCH_PRODUCT_BY_BARCODE_REQUEST'
|
||||
export const SEARCH_PRODUCT_BY_BARCODE_SUCCESS = 'app/HomePage/SEARCH_PRODUCT_BY_BARCODE_SUCCESS';
|
||||
export const SEARCH_PRODUCT_BY_BARCODE_ERROR = 'app/HomePage/SEARCH_PRODUCT_BY_BARCODE_ERROR';
|
||||
|
||||
export const SET_SELECTED_MEAL = 'app/HomePage/SET_SELECTED_MEAL'
|
@ -1,38 +1,62 @@
|
||||
import React from 'react';
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import { Container, Grid, Paper } from '@material-ui/core';
|
||||
import ScrollableTabs from 'components/ScrollableTabs';
|
||||
import DailyStats from 'components/DailyStats'
|
||||
|
||||
import { TABS, CALORIESLEFT, MACRONUTRIENTS } from 'utils/mock'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
marginTop: theme.spacing(2),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
}));
|
||||
import React, { useEffect } from 'react';
|
||||
import { Container, Grid, Box } from '@material-ui/core';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {useInjectSaga, useInjectReducer} from "redux-injectors";
|
||||
import {format} from "date-fns";
|
||||
import {makeSelectMeals, makeSelectDailyCalories, makeSelectTotalProteins, makeSelectTotalFats, makeSelectTotalCalories, makeSelectTotalCarbohydrates, makeSelectDailyFats, makeSelectDailyCarbohydrates, makeSelectDailyProteins} from "./selectors";
|
||||
import MacronutrientsCard from 'components/MacronutrientsCard'
|
||||
import { colors } from 'utils/theme'
|
||||
import saga from "./saga";
|
||||
import reducer from "./reducer";
|
||||
import { getMealsAction } from './actions'
|
||||
import MealCard from "components/MealCard";
|
||||
import { createStructuredSelector } from 'reselect'
|
||||
|
||||
const stateSelector = createStructuredSelector({
|
||||
meals: makeSelectMeals(),
|
||||
dailyCalories: makeSelectDailyCalories(),
|
||||
dailyFats: makeSelectDailyFats(),
|
||||
dailyProteins: makeSelectDailyProteins(),
|
||||
dailyCarbohydrates: makeSelectDailyCarbohydrates(),
|
||||
totalCalories: makeSelectTotalCalories(),
|
||||
totalCarbohydrates: makeSelectTotalCarbohydrates(),
|
||||
totalFats: makeSelectTotalFats(),
|
||||
totalProteins: makeSelectTotalProteins(),
|
||||
})
|
||||
|
||||
const key = 'homePage'
|
||||
const HomePage = () => {
|
||||
const classes = useStyles()
|
||||
useInjectSaga({ key, saga });
|
||||
useInjectReducer({key, reducer });
|
||||
const { meals, dailyCalories, dailyFats, totalFats, totalProteins, dailyProteins, dailyCarbohydrates, totalCalories, totalCarbohydrates } = useSelector(stateSelector);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const today = format(new Date(), "yyyy-MM-dd");
|
||||
dispatch(getMealsAction({ date: today }));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container className={classes.root}>
|
||||
<Grid xs={12} container item spacing={2} justify="center">
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper>
|
||||
<ScrollableTabs tabs={TABS} />
|
||||
</Paper >
|
||||
<Container>
|
||||
<Box padding={2}>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justify="space-evenly"
|
||||
alignItems="center"
|
||||
>
|
||||
<MacronutrientsCard label="calories" color={colors.calories} total={totalCalories} daily={dailyCalories} />
|
||||
<MacronutrientsCard label="proteins" color={colors.protein} total={totalProteins} daily={dailyProteins} />
|
||||
<MacronutrientsCard label="fats" color={colors.fat} total={totalFats} daily={dailyFats} />
|
||||
<MacronutrientsCard label="carbohydrates" color={colors.carbs} total={totalCarbohydrates} daily={dailyCarbohydrates} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<DailyStats caloriesLeft={CALORIESLEFT} macronutrients={MACRONUTRIENTS} />
|
||||
<Paper>
|
||||
<span>Hello world</span>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
<Box padding={2}>
|
||||
{meals.map(({ id, products, label }, index) => (
|
||||
<MealCard id={id} label={label} products={products} key={index} />
|
||||
))}
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
193
src/pages/Home/reducer.js
Normal file
193
src/pages/Home/reducer.js
Normal file
@ -0,0 +1,193 @@
|
||||
import produce from 'immer';
|
||||
import {
|
||||
CREATE_MEAL_REQUEST,
|
||||
CREATE_MEAL_SUCCESS,
|
||||
CREATE_MEAL_ERROR,
|
||||
GET_MEALS_REQUEST,
|
||||
GET_MEALS_SUCCESS,
|
||||
GET_MEALS_ERROR,
|
||||
UPDATE_MEAL_REQUEST,
|
||||
UPDATE_MEAL_SUCCESS,
|
||||
UPDATE_MEAL_ERROR,
|
||||
ADD_PRODUCTS_TO_MEAL_REQUEST,
|
||||
ADD_PRODUCTS_TO_MEAL_SUCCESS,
|
||||
ADD_PRODUCTS_TO_MEAL_ERROR,
|
||||
SEARCH_PRODUCT_BY_BARCODE_REQUEST,
|
||||
SEARCH_PRODUCT_BY_BARCODE_SUCCESS,
|
||||
SEARCH_PRODUCT_BY_BARCODE_ERROR,
|
||||
SEARCH_PRODUCT_BY_LABEL_REQUEST,
|
||||
SEARCH_PRODUCT_BY_LABEL_SUCCESS,
|
||||
REMOVE_PRODUCT_FROM_MEAL_SUCCESS,
|
||||
SEARCH_PRODUCT_BY_LABEL_ERROR,
|
||||
REMOVE_PRODUCT_FROM_MEAL_ERROR,
|
||||
SET_SELECTED_MEAL, REMOVE_PRODUCT_FROM_MEAL_REQUEST,
|
||||
} from './constants';
|
||||
|
||||
import { sortMeals } from 'utils/sort';
|
||||
import { sumMacro } from 'utils/calculate';
|
||||
|
||||
const defaultMeals = [
|
||||
{
|
||||
id: null,
|
||||
label: 'breakfast',
|
||||
products: [],
|
||||
},
|
||||
{
|
||||
id: null,
|
||||
label: 'lunch',
|
||||
products: [],
|
||||
},
|
||||
{
|
||||
id: null,
|
||||
label: 'dinner',
|
||||
products: [],
|
||||
},
|
||||
{
|
||||
id: null,
|
||||
label: 'supper',
|
||||
products: [],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
export const initialState = {
|
||||
isLoading: false,
|
||||
error: {},
|
||||
label: '',
|
||||
products: [],
|
||||
dailyCalories: 0,
|
||||
dailyFats: 0,
|
||||
dailyProteins: 0,
|
||||
dailyCarbohydrates: 0,
|
||||
totalCalories: 0,
|
||||
totalFats: 0,
|
||||
totalCarbohydrates: 0,
|
||||
totalProteins: 0,
|
||||
barcode: '',
|
||||
product: {},
|
||||
date: '',
|
||||
form: {
|
||||
id: '',
|
||||
label: '',
|
||||
products: [],
|
||||
date: null,
|
||||
productId: null,
|
||||
},
|
||||
meals: defaultMeals
|
||||
};
|
||||
|
||||
const homePageReducer = produce((draft, action) => {
|
||||
switch(action.type) {
|
||||
case REMOVE_PRODUCT_FROM_MEAL_REQUEST:
|
||||
draft.form.id = action.mealId;
|
||||
draft.form.productId = action.productId;
|
||||
break;
|
||||
|
||||
case REMOVE_PRODUCT_FROM_MEAL_SUCCESS:
|
||||
draft.meals = sortMeals(draft.meals, action.meal, [action.meal.label])
|
||||
draft.totalCalories = sumMacro(draft.meals, 'calories')
|
||||
draft.totalFats = sumMacro(draft.meals, 'fat')
|
||||
draft.totalCarbohydrates = sumMacro(draft.meals, 'carbohydrates')
|
||||
draft.totalProteins = sumMacro(draft.meals, 'protein')
|
||||
draft.isLoading = false;
|
||||
break;
|
||||
|
||||
|
||||
case SET_SELECTED_MEAL:
|
||||
draft.form.id = action.id;
|
||||
draft.form.label = action.label.toLowerCase();
|
||||
break;
|
||||
|
||||
case CREATE_MEAL_REQUEST:
|
||||
draft.isLoading = true;
|
||||
draft.form.label = action.label.toLowerCase();
|
||||
draft.form.products = action.products;
|
||||
draft.form.date = action.date;
|
||||
break;
|
||||
|
||||
case GET_MEALS_SUCCESS:
|
||||
const labels = action.meals.map(({ label }) => label)
|
||||
draft.meals = sortMeals(defaultMeals, action.meals, labels)
|
||||
draft.dailyCalories = action.dailyCalories;
|
||||
draft.dailyFats = action.dailyFats;
|
||||
draft.dailyProteins = action.dailyProteins;
|
||||
draft.dailyCarbohydrates = action.dailyCarbs;
|
||||
draft.totalCalories = sumMacro(draft.meals, 'calories')
|
||||
draft.totalFats = sumMacro(draft.meals, 'fat')
|
||||
draft.totalCarbohydrates = sumMacro(draft.meals, 'carbohydrates')
|
||||
draft.totalProteins = sumMacro(draft.meals, 'protein')
|
||||
draft.isLoading = false;
|
||||
break;
|
||||
|
||||
case SEARCH_PRODUCT_BY_BARCODE_REQUEST:
|
||||
draft.barcode = action.barcode;
|
||||
draft.isLoading = true;
|
||||
break;
|
||||
|
||||
case SEARCH_PRODUCT_BY_BARCODE_SUCCESS:
|
||||
draft.products = action.products;
|
||||
draft.isLoading = false;
|
||||
break;
|
||||
|
||||
case SEARCH_PRODUCT_BY_LABEL_REQUEST:
|
||||
draft.label = action.label;
|
||||
draft.isLoading = true;
|
||||
break;
|
||||
|
||||
case SEARCH_PRODUCT_BY_LABEL_SUCCESS:
|
||||
draft.products = action.products;
|
||||
draft.isLoading = false;
|
||||
break;
|
||||
|
||||
case GET_MEALS_REQUEST:
|
||||
draft.date = action.date;
|
||||
draft.isLoading = true;
|
||||
break;
|
||||
|
||||
case UPDATE_MEAL_SUCCESS:
|
||||
draft.isLoading = false;
|
||||
break;
|
||||
|
||||
case ADD_PRODUCTS_TO_MEAL_SUCCESS:
|
||||
draft.meals = sortMeals(draft.meals, action.meal, [action.meal.label])
|
||||
draft.totalCalories = sumMacro(draft.meals, 'calories')
|
||||
draft.totalFats = sumMacro(draft.meals, 'fat')
|
||||
draft.totalCarbohydrates = sumMacro(draft.meals, 'carbohydrates')
|
||||
draft.totalProteins = sumMacro(draft.meals, 'protein')
|
||||
draft.isLoading = false;
|
||||
break;
|
||||
|
||||
case CREATE_MEAL_SUCCESS:
|
||||
draft.meals = sortMeals(defaultMeals, action.meal, [action.meal.label])
|
||||
draft.totalCalories = sumMacro(draft.meals, 'calories')
|
||||
draft.totalFats = sumMacro(draft.meals, 'fat')
|
||||
draft.totalCarbohydrates = sumMacro(draft.meals, 'carbohydrates')
|
||||
draft.totalProteins = sumMacro(draft.meals, 'protein')
|
||||
|
||||
draft.isLoading = false;
|
||||
break;
|
||||
|
||||
case ADD_PRODUCTS_TO_MEAL_REQUEST:
|
||||
draft.form.id = action.id;
|
||||
draft.form.products = action.products;
|
||||
draft.isLoading = true;
|
||||
break;
|
||||
|
||||
case UPDATE_MEAL_REQUEST:
|
||||
draft.isLoading = true;
|
||||
break;
|
||||
|
||||
case CREATE_MEAL_ERROR:
|
||||
case GET_MEALS_ERROR:
|
||||
case UPDATE_MEAL_ERROR:
|
||||
case ADD_PRODUCTS_TO_MEAL_ERROR:
|
||||
case SEARCH_PRODUCT_BY_LABEL_ERROR:
|
||||
case SEARCH_PRODUCT_BY_BARCODE_ERROR:
|
||||
case REMOVE_PRODUCT_FROM_MEAL_ERROR:
|
||||
draft.isLoading = false;
|
||||
draft.error = action.error;
|
||||
break;
|
||||
}
|
||||
}, initialState);
|
||||
|
||||
export default homePageReducer;
|
146
src/pages/Home/saga.js
Normal file
146
src/pages/Home/saga.js
Normal file
@ -0,0 +1,146 @@
|
||||
import { takeLatest, call, put, select } from 'redux-saga/effects';
|
||||
import {api, request, routes} from 'utils';
|
||||
import { GET_MEALS_REQUEST, REMOVE_PRODUCT_FROM_MEAL_REQUEST, SEARCH_PRODUCT_BY_BARCODE_REQUEST, SEARCH_PRODUCT_BY_LABEL_REQUEST, CREATE_MEAL_REQUEST, ADD_PRODUCTS_TO_MEAL_REQUEST } from './constants';
|
||||
import { searchProductByLabelErrorAction, removeProductFromMealSuccessAction, removeProductFromMealErrorAction, addProductsToSuccessAction, addProductsToMealErrorAction, searchProductByLabelSuccessAction, createMealSuccessAction, createMealErrorAction, searchProductByBarcodeSuccessAction, searchProductByBarcodeErrorAction, updateMealErrorAction, updateMealSuccessAction, getMealsSuccessAction, getMealsErrorAction } from './actions';
|
||||
import { makeSelectLabel, makeSelectFormProducts, makeSelectMealLabel, makeSelectMealId, makeSelectBarcode, makeSelectProductId, makeSelectDate, } from './selectors'
|
||||
import { makeSelectTokens } from 'containers/App/selectors'
|
||||
import {push} from "connected-react-router";
|
||||
|
||||
export function* getMeals() {
|
||||
const { access } = yield select(makeSelectTokens());
|
||||
const date = yield select(makeSelectDate());
|
||||
|
||||
const requestURL = `${api.meals}?date=${date}`;
|
||||
|
||||
const requestParameters = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${access.token}`,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const {meals, dailyCalories, dailyFats, dailyProteins, dailyCarbs} = yield call(request, requestURL, requestParameters);
|
||||
yield put(getMealsSuccessAction({ meals, dailyCalories, dailyFats, dailyProteins, dailyCarbs }));
|
||||
} catch (error) {
|
||||
yield put(getMealsErrorAction({ error: error.message }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* removeProductFromMeal() {
|
||||
const { access } = yield select(makeSelectTokens());
|
||||
const productId = yield select(makeSelectProductId());
|
||||
const mealId = yield select(makeSelectMealId());
|
||||
|
||||
const requestURL = `${api.meals}/${mealId}/products`;
|
||||
|
||||
const requestParameters = {
|
||||
method: 'PATCH',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${access.token}`, },
|
||||
body: JSON.stringify({ productId }),
|
||||
};
|
||||
|
||||
try {
|
||||
const meal = yield call(request, requestURL, requestParameters);
|
||||
yield put(removeProductFromMealSuccessAction({ meal }));
|
||||
} catch (error) {
|
||||
yield put(removeProductFromMealErrorAction({error: error.message}));
|
||||
}
|
||||
}
|
||||
|
||||
export function* createMeal() {
|
||||
const { access } = yield select(makeSelectTokens());
|
||||
const label = yield select(makeSelectMealLabel());
|
||||
const products = yield select(makeSelectFormProducts());
|
||||
const date = yield select(makeSelectDate());
|
||||
|
||||
const requestURL = api.meals;
|
||||
|
||||
const requestParameters = {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${access.token}`, },
|
||||
body: JSON.stringify({ label, products, date }),
|
||||
};
|
||||
|
||||
try {
|
||||
const meal = yield call(request, requestURL, requestParameters);
|
||||
yield put(createMealSuccessAction({meal}));
|
||||
yield put(push(routes.dashboard.path));
|
||||
} catch (error) {
|
||||
yield put(createMealErrorAction({error: error.message}));
|
||||
}
|
||||
}
|
||||
|
||||
export function* addProductsToMeal() {
|
||||
const { access } = yield select(makeSelectTokens());
|
||||
const products = yield select(makeSelectFormProducts());
|
||||
const mealId = yield select(makeSelectMealId());
|
||||
|
||||
const requestURL = `${api.meals}/${mealId}`;
|
||||
|
||||
const requestParameters = {
|
||||
method: 'PATCH',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${access.token}`, },
|
||||
body: JSON.stringify({ products }),
|
||||
};
|
||||
|
||||
try {
|
||||
const meal = yield call(request, requestURL, requestParameters);
|
||||
yield put(addProductsToSuccessAction({ meal }));
|
||||
yield put(push(routes.dashboard.path));
|
||||
} catch (error) {
|
||||
yield put(addProductsToMealErrorAction({error: error.message}));
|
||||
}
|
||||
}
|
||||
|
||||
export function* searchProductByBarcode() {
|
||||
const { access } = yield select(makeSelectTokens());
|
||||
const barcode = yield select(makeSelectBarcode());
|
||||
|
||||
const requestURL = `${api.products}?barcode=${barcode}`;
|
||||
|
||||
const requestParameters = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${access.token}`,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const products = yield call(request, requestURL, requestParameters);
|
||||
yield put(searchProductByBarcodeSuccessAction({ products }));
|
||||
} catch (error) {
|
||||
yield put(searchProductByBarcodeErrorAction({error: error.message}));
|
||||
}
|
||||
}
|
||||
|
||||
export function* searchProductByLabel() {
|
||||
const { access } = yield select(makeSelectTokens());
|
||||
const label = yield select(makeSelectLabel());
|
||||
|
||||
const requestURL = `${api.products}?label=${label}`;
|
||||
|
||||
const requestParameters = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${access.token}`,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const products = yield call(request, requestURL, requestParameters);
|
||||
yield put(searchProductByLabelSuccessAction({products}));
|
||||
} catch (error) {
|
||||
yield put(searchProductByLabelErrorAction({error: error.message}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default function* MealPageSaga() {
|
||||
yield takeLatest(SEARCH_PRODUCT_BY_BARCODE_REQUEST, searchProductByBarcode);
|
||||
yield takeLatest(SEARCH_PRODUCT_BY_LABEL_REQUEST, searchProductByLabel);
|
||||
yield takeLatest(GET_MEALS_REQUEST, getMeals);
|
||||
yield takeLatest(REMOVE_PRODUCT_FROM_MEAL_REQUEST, removeProductFromMeal);
|
||||
yield takeLatest(CREATE_MEAL_REQUEST, createMeal);
|
||||
yield takeLatest(ADD_PRODUCTS_TO_MEAL_REQUEST, addProductsToMeal);
|
||||
}
|
90
src/pages/Home/selectors.js
Normal file
90
src/pages/Home/selectors.js
Normal file
@ -0,0 +1,90 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { initialState } from './reducer';
|
||||
|
||||
const selectHomePageDomain = (state) => state.homePage || initialState;
|
||||
|
||||
const makeSelectError = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.error);
|
||||
|
||||
const makeSelectIsLoading = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.isLoading);
|
||||
|
||||
const makeSelectMeals = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.meals);
|
||||
|
||||
const makeSelectDate = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.date);
|
||||
|
||||
const makeSelectLabel = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.label);
|
||||
|
||||
const makeSelectProducts = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.products);
|
||||
|
||||
const makeSelectProduct = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.product);
|
||||
|
||||
const makeSelectBarcode = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.barcode);
|
||||
|
||||
const makeSelectMealLabel = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.form.label);
|
||||
|
||||
const makeSelectMealId = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.form.id);
|
||||
|
||||
const makeSelectProductId = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.form.productId);
|
||||
|
||||
const makeSelectFormProducts = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.form.products);
|
||||
|
||||
const makeSelectDailyCalories = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.dailyCalories);
|
||||
|
||||
const makeSelectDailyCarbohydrates = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.dailyCarbohydrates);
|
||||
|
||||
const makeSelectDailyFats = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.dailyFats);
|
||||
|
||||
const makeSelectDailyProteins = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.dailyProteins);
|
||||
|
||||
const makeSelectTotalCarbohydrates = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.totalCarbohydrates);
|
||||
|
||||
const makeSelectTotalProteins = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.totalProteins);
|
||||
|
||||
const makeSelectTotalFats = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.totalFats);
|
||||
|
||||
const makeSelectTotalCalories = () =>
|
||||
createSelector(selectHomePageDomain, (substate) => substate.totalCalories);
|
||||
|
||||
|
||||
export {
|
||||
selectHomePageDomain,
|
||||
makeSelectTotalFats,
|
||||
makeSelectTotalCalories,
|
||||
makeSelectTotalCarbohydrates,
|
||||
makeSelectTotalProteins,
|
||||
makeSelectDailyCarbohydrates,
|
||||
makeSelectDailyProteins,
|
||||
makeSelectDailyFats,
|
||||
makeSelectDailyCalories,
|
||||
makeSelectFormProducts,
|
||||
makeSelectMealLabel,
|
||||
makeSelectMealId,
|
||||
makeSelectProductId,
|
||||
makeSelectDate,
|
||||
makeSelectLabel,
|
||||
makeSelectProducts,
|
||||
makeSelectError,
|
||||
makeSelectIsLoading,
|
||||
makeSelectMeals,
|
||||
makeSelectProduct,
|
||||
makeSelectBarcode,
|
||||
|
||||
};
|
9
src/pages/Home/styles.js
Normal file
9
src/pages/Home/styles.js
Normal file
@ -0,0 +1,9 @@
|
||||
import {makeStyles } from '@material-ui/core/styles'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
title: {
|
||||
margin: theme.spacing(4, 0, 2),
|
||||
},
|
||||
}))
|
||||
|
||||
export default useStyles
|
14
src/pages/Login/FormModel/formInitialValues.js
Normal file
14
src/pages/Login/FormModel/formInitialValues.js
Normal file
@ -0,0 +1,14 @@
|
||||
import checkoutFormModel from './loginFormModel';
|
||||
const {
|
||||
formField: {
|
||||
email,
|
||||
password
|
||||
}
|
||||
} = checkoutFormModel;
|
||||
|
||||
const formInitialValues = {
|
||||
[email.name]: 'admin@admin.com',
|
||||
[password.name]: 'Kox32113@#$',
|
||||
};
|
||||
|
||||
export default formInitialValues
|
19
src/pages/Login/FormModel/loginFormModel.js
Normal file
19
src/pages/Login/FormModel/loginFormModel.js
Normal file
@ -0,0 +1,19 @@
|
||||
const loginFormModel = {
|
||||
formId: 'loginForm',
|
||||
formField: {
|
||||
email: {
|
||||
name: 'email',
|
||||
label: 'Email*',
|
||||
requiredErrorMsg: 'Email is required',
|
||||
invalidErrorMsg: 'Email is not valid'
|
||||
},
|
||||
password: {
|
||||
name: 'password',
|
||||
label: 'Password*',
|
||||
requiredErrorMsg: 'Password is required',
|
||||
invalidErrorMsg: 'Password is to short'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default loginFormModel
|
21
src/pages/Login/FormModel/validationSchema.js
Normal file
21
src/pages/Login/FormModel/validationSchema.js
Normal file
@ -0,0 +1,21 @@
|
||||
import * as Yup from 'yup';
|
||||
import checkoutFormModel from './loginFormModel';
|
||||
const {
|
||||
formField: {
|
||||
email,
|
||||
password
|
||||
}
|
||||
} = checkoutFormModel;
|
||||
|
||||
const validationSchema =
|
||||
Yup.object().shape({
|
||||
[email.name]: Yup.string().required(`${email.requiredErrorMsg}`).email(`${email.invalidErrorMsg}`),
|
||||
[password.name]: Yup.string().required(`${password.requiredErrorMsg}`)
|
||||
.test(
|
||||
'len',
|
||||
`${password.invalidErrorMsg}`,
|
||||
value => value && value.toString().length >= 8
|
||||
),
|
||||
})
|
||||
|
||||
export default validationSchema
|
@ -6,8 +6,10 @@ export const loginInputChange = (name, value) => ({
|
||||
value,
|
||||
})
|
||||
|
||||
export const loginAction = () => ({
|
||||
export const loginAction = ({ email, password }) => ({
|
||||
type: LOGIN_REQUEST,
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
export const loginSuccessAction = ({user, tokens}) => ({
|
||||
|
@ -1,63 +1,42 @@
|
||||
import React from 'react';
|
||||
import { useInjectReducer, useInjectSaga } from 'redux-injectors';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
Container,
|
||||
Grid,
|
||||
Box,
|
||||
Paper,
|
||||
Button,
|
||||
Typography,
|
||||
TextField,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Link
|
||||
Link,
|
||||
} from '@material-ui/core'
|
||||
import reducer from './reducer';
|
||||
import saga from './saga';
|
||||
import { makeSelectEmail, makeSelectPassword, makeSelectLoading } from './selectors'
|
||||
import { loginInputChange, loginAction } from './actions'
|
||||
import {routes} from "../../utils";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import {routes} from "utils";
|
||||
import InputField from 'components/InputField'
|
||||
import { Formik, Form } from 'formik';
|
||||
import { loginAction } from './actions'
|
||||
import useStyles from './styles'
|
||||
import validationSchema from './FormModel/validationSchema'
|
||||
import formInitialValues from './FormModel/formInitialValues'
|
||||
import loginFormModel from './FormModel/loginFormModel'
|
||||
import saga from "./saga";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
paper: {
|
||||
marginTop: theme.spacing(8),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
form: {
|
||||
width: '100%',
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
submit: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
}));
|
||||
|
||||
const stateSelector = createStructuredSelector({
|
||||
email: makeSelectEmail(),
|
||||
password: makeSelectPassword(),
|
||||
loading: makeSelectLoading(),
|
||||
});
|
||||
const {
|
||||
formId,
|
||||
formField: {
|
||||
email,
|
||||
password
|
||||
}
|
||||
} = loginFormModel;
|
||||
|
||||
const key = 'loginPage'
|
||||
|
||||
const Login = () => {
|
||||
const classes = useStyles()
|
||||
useInjectReducer({ key, reducer });
|
||||
useInjectSaga({ key, saga });
|
||||
const { email, password, loading } = useSelector(stateSelector)
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onChangeInput = ({target: { name, value }}) => {
|
||||
dispatch(loginInputChange(name, value))
|
||||
}
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault()
|
||||
dispatch(loginAction())
|
||||
const handleSubmit = (values, actions) => {
|
||||
dispatch(loginAction(values))
|
||||
}
|
||||
|
||||
return (
|
||||
@ -66,55 +45,53 @@ const Login = () => {
|
||||
<Typography component="h1" variant="h5">
|
||||
Login to Account
|
||||
</Typography>
|
||||
<form className={classes.form} onSubmit={handleSubmit} noValidate>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
id="email"
|
||||
label="Email Address"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
onChange={onChangeInput}
|
||||
value={email}
|
||||
/>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
onChange={onChangeInput}
|
||||
value={password}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox value="remember" color="primary" />}
|
||||
label="Remember me"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
disabled={loading}
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
<Grid container alignItems="flex-end">
|
||||
<Grid item>
|
||||
<Link href={routes.register.path} color="secondary" variant="body2">
|
||||
Don't have an account? Sign Up
|
||||
</Link>
|
||||
</Grid>
|
||||
<Formik
|
||||
initialValues={formInitialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form id={formId}>
|
||||
<InputField
|
||||
type="text"
|
||||
label={email.label}
|
||||
name={email.name}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
className={classes.input}
|
||||
/>
|
||||
<InputField
|
||||
type="password"
|
||||
label={password.label}
|
||||
name={password.name}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
className={classes.input}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox value="remember" color="primary" />}
|
||||
label="Remember me"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<Grid container alignItems="flex-end">
|
||||
<Grid item>
|
||||
<Link href={routes.register.path} color="secondary" variant="body2">
|
||||
Don't have an account? Sign Up
|
||||
</Link>
|
||||
</Grid>
|
||||
</form>
|
||||
</Grid>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
|
@ -19,7 +19,7 @@ export function* login() {
|
||||
try {
|
||||
const { tokens, user } = yield call(request, requestURL, requestParameters);
|
||||
yield put(loginSuccessAction({user, tokens}));
|
||||
yield put(push(routes.profile.path));
|
||||
yield put(push(routes.dashboard.path));
|
||||
} catch (error) {
|
||||
yield put(loginErrorAction(error.message));
|
||||
}
|
||||
|
18
src/pages/Login/styles.js
Normal file
18
src/pages/Login/styles.js
Normal file
@ -0,0 +1,18 @@
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
paper: {
|
||||
marginTop: theme.spacing(8),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
submit: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
}))
|
||||
|
||||
export default useStyles
|
22
src/pages/Logout/index.js
Normal file
22
src/pages/Logout/index.js
Normal file
@ -0,0 +1,22 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Redirect
|
||||
} from "react-router-dom";
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { routes } from 'utils'
|
||||
import { logoutAction} from 'containers/App/actions'
|
||||
|
||||
const Logout = () => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(logoutAction())
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Redirect to={routes.login.path} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Logout;
|
@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const ReviewProfileForm = () => {
|
||||
return (
|
||||
<div>
|
||||
ReviewProfileForm
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReviewProfileForm;
|
@ -1,29 +1,23 @@
|
||||
import {
|
||||
PROFILE_INPUT_CHANGE,
|
||||
GET_PROFILE_REQUEST,
|
||||
GET_PROFILE_SUCCESS,
|
||||
GET_PROFILE_ERROR,
|
||||
UPDATE_PROFILE_REQUEST,
|
||||
UPDATE_PROFILE_SUCCESS,
|
||||
UPDATE_PROFILE_ERROR
|
||||
} from './constants';
|
||||
|
||||
export const profileInputChange = ({name, value}) => ({
|
||||
type: PROFILE_INPUT_CHANGE,
|
||||
name,
|
||||
value,
|
||||
})
|
||||
|
||||
export const getProfileAction = () => ({
|
||||
type: GET_PROFILE_REQUEST,
|
||||
})
|
||||
|
||||
export const getProfileSuccessAction = ({birthday, gender, height, weight, goalWeight, rateOfChange, activity}) => ({
|
||||
export const getProfileSuccessAction = ({ weeksToGoal, currentWeight, goal, dailyCalories, birthday, gender, height, weight, goalWeight, rateOfChange, activity}) => ({
|
||||
type: GET_PROFILE_SUCCESS,
|
||||
birthday,
|
||||
weeksToGoal,
|
||||
dailyCalories,
|
||||
gender,
|
||||
height,
|
||||
weight,
|
||||
goal,
|
||||
currentWeight,
|
||||
goalWeight,
|
||||
rateOfChange,
|
||||
activity
|
||||
@ -33,25 +27,3 @@ export const getProfileErrorAction = ({error}) => ({
|
||||
type: GET_PROFILE_ERROR,
|
||||
error,
|
||||
})
|
||||
|
||||
export const updateProfileAction = () => ({
|
||||
type: UPDATE_PROFILE_REQUEST,
|
||||
})
|
||||
|
||||
export const updateProfileSuccessAction = ({birthday, goal, gender, height, weight, goalWeight, rateOfChange, activity}) => ({
|
||||
type: UPDATE_PROFILE_SUCCESS,
|
||||
birthday,
|
||||
gender,
|
||||
goal,
|
||||
height,
|
||||
weight,
|
||||
goalWeight,
|
||||
rateOfChange,
|
||||
activity
|
||||
})
|
||||
|
||||
export const updateProfileErrorAction = ({error}) => ({
|
||||
type: UPDATE_PROFILE_ERROR,
|
||||
error,
|
||||
})
|
||||
|
||||
|
@ -1,9 +1,3 @@
|
||||
export const GET_PROFILE_REQUEST = 'app/ProfilePage/GET_PROFILE_REQUEST';
|
||||
export const GET_PROFILE_SUCCESS = 'app/ProfilePage/GET_PROFILE_SUCCESS';
|
||||
export const GET_PROFILE_ERROR = 'app/ProfilePage/GET_PROFILE_ERROR';
|
||||
|
||||
export const UPDATE_PROFILE_REQUEST = 'app/ProfilePage/UPDATE_PROFILE_REQUEST';
|
||||
export const UPDATE_PROFILE_SUCCESS = 'app/ProfilePage/UPDATE_PROFILE_SUCCESS';
|
||||
export const UPDATE_PROFILE_ERROR = 'app/ProfilePage/UPDATE_PROFILE_ERROR';
|
||||
|
||||
export const PROFILE_INPUT_CHANGE = 'app/ProfilePage/PROFILE_INPUT_CHANGE';
|
||||
|
@ -1,173 +1,60 @@
|
||||
import React, { useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {Paper, Stepper, Step, StepLabel, Button, Typography } from '@material-ui/core';
|
||||
import {useInjectReducer, useInjectSaga} from "redux-injectors";
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Formik, Form } from 'formik';
|
||||
import GoalForm from './GoalForm';
|
||||
import PersonalDetailsForm from './PersonalDetailsForm';
|
||||
import ReviewProfileForm from './ReviewProfileForm';
|
||||
import reducer from "./reducer";
|
||||
import React, { useEffect } from 'react';
|
||||
import {useInjectSaga, useInjectReducer} from "redux-injectors";
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Grid, List, ListItem, ListItemText } from '@material-ui/core'
|
||||
import { startCase } from 'lodash'
|
||||
import saga from "./saga";
|
||||
import reducer from "./reducer";
|
||||
import {getProfileAction} from './actions'
|
||||
import { createStructuredSelector } from 'reselect'
|
||||
import {
|
||||
makeSelectBirthday,
|
||||
makeSelectHeight,
|
||||
makeSelectCurrentWeight,
|
||||
makeSelectGoalWeight,
|
||||
makeSelectRateOfChange,
|
||||
makeSelectActivity,
|
||||
makeSelectGoal,
|
||||
makeSelectGender,
|
||||
makeSelectDailyCalories,
|
||||
makeSelectWeeksToGoal,
|
||||
} from './selectors'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
layout: {
|
||||
width: 'auto',
|
||||
marginLeft: theme.spacing(2),
|
||||
marginRight: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(2) * 2)]: {
|
||||
width: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
padding: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(3) * 2)]: {
|
||||
marginTop: theme.spacing(6),
|
||||
marginBottom: theme.spacing(6),
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
},
|
||||
stepper: {
|
||||
padding: theme.spacing(3, 0, 5),
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
button: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const steps = ['Your goal', 'Personal details', 'Review your profile'];
|
||||
|
||||
const renderStepContent = (step) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return <GoalForm/>;
|
||||
case 1:
|
||||
return <PersonalDetailsForm/>;
|
||||
case 2:
|
||||
return <ReviewProfileForm/>;
|
||||
default:
|
||||
throw new Error('Unknown step');
|
||||
}
|
||||
}
|
||||
|
||||
const formInitialValues = {
|
||||
gender: '',
|
||||
goal: '',
|
||||
birthday: '',
|
||||
height: '',
|
||||
weight: {
|
||||
current: '',
|
||||
goal: '',
|
||||
},
|
||||
rateOfChange: '',
|
||||
activity: '',
|
||||
}
|
||||
const stateSelector = createStructuredSelector({
|
||||
birthday: makeSelectBirthday(),
|
||||
height: makeSelectHeight(),
|
||||
currentWeight: makeSelectCurrentWeight(),
|
||||
goalWeight: makeSelectGoalWeight(),
|
||||
rateOfChange: makeSelectRateOfChange(),
|
||||
activity: makeSelectActivity(),
|
||||
goal: makeSelectGoal(),
|
||||
gender: makeSelectGender(),
|
||||
dailyCalories: makeSelectDailyCalories(),
|
||||
weeksToGoal: makeSelectWeeksToGoal(),
|
||||
})
|
||||
|
||||
const key = 'profilePage'
|
||||
const ProfilePage = () => {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const isLastStep = activeStep === steps.length - 1;
|
||||
const isFirstStep = activeStep === 0;
|
||||
const classes = useStyles();
|
||||
|
||||
useInjectReducer({ key, reducer });
|
||||
useInjectSaga({ key, saga });
|
||||
useInjectReducer({ key, reducer })
|
||||
const dispatch = useDispatch()
|
||||
const userProfileDetails = useSelector(stateSelector)
|
||||
|
||||
const sleep = (ms) => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const submitForm = async (values, actions) => {
|
||||
console.log(JSON.stringify(values, null, 2));
|
||||
actions.setSubmitting(false);
|
||||
|
||||
setActiveStep(activeStep + 1);
|
||||
}
|
||||
|
||||
const handleSubmit = (values, actions) => {
|
||||
if (isLastStep) {
|
||||
submitForm(values, actions);
|
||||
} else {
|
||||
setActiveStep(activeStep + 1);
|
||||
actions.setTouched({});
|
||||
actions.setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
setActiveStep(activeStep - 1);
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
setActiveStep(activeStep + 1);
|
||||
}
|
||||
useEffect(() => {
|
||||
dispatch(getProfileAction())
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<Typography component="h1" variant="h4" align="center">
|
||||
Profile
|
||||
</Typography>
|
||||
<Stepper activeStep={activeStep} className={classes.stepper}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
<React.Fragment>
|
||||
{activeStep === steps.length ? (
|
||||
<React.Fragment>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Thank you for your order.
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">
|
||||
Your order number is #2001539. We have emailed your order confirmation, and will
|
||||
send you an update when your order has shipped.
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Formik
|
||||
initialValues={formInitialValues}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form id="createProfileForm">
|
||||
{renderStepContent(activeStep)}
|
||||
<div className={classes.buttons}>
|
||||
{activeStep !== 0 && (
|
||||
<Button onClick={handleBack} className={classes.button}>
|
||||
back
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={isSubmitting}
|
||||
onClick={handleNext}
|
||||
type={isLastStep ? 'submit' : 'button'}
|
||||
className={classes.button}
|
||||
>
|
||||
{isLastStep ? 'Create profile' : 'Next'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</Paper>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
<Grid container>
|
||||
<List>
|
||||
{Object.keys(userProfileDetails).map((key, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemText primary={`${startCase(key)}: ${userProfileDetails[key]}`} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage
|
||||
export default ProfilePage;
|
||||
|
@ -1,102 +1,50 @@
|
||||
import produce from 'immer';
|
||||
import {
|
||||
PROFILE_INPUT_CHANGE,
|
||||
GET_PROFILE_REQUEST,
|
||||
GET_PROFILE_SUCCESS,
|
||||
GET_PROFILE_ERROR,
|
||||
UPDATE_PROFILE_REQUEST,
|
||||
UPDATE_PROFILE_SUCCESS,
|
||||
UPDATE_PROFILE_ERROR
|
||||
} from './constants';
|
||||
import {format} from "date-fns";
|
||||
|
||||
export const initialState = {
|
||||
activities: [
|
||||
{
|
||||
label: 'very low',
|
||||
value: 1.2
|
||||
},
|
||||
{
|
||||
label: 'low',
|
||||
value: 1.375
|
||||
},
|
||||
{
|
||||
label: 'medium',
|
||||
value: 1.55
|
||||
},{
|
||||
label: 'high',
|
||||
value: 1.725
|
||||
},
|
||||
{
|
||||
label: 'very high',
|
||||
value: 1.9
|
||||
},
|
||||
],
|
||||
goals: [
|
||||
{
|
||||
label: 'lose weight',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'maintain weight',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: 'put on weight',
|
||||
value: 3,
|
||||
}
|
||||
],
|
||||
ratesOfChange: [
|
||||
{ value: 1, label: '0 kg'},
|
||||
{ value: 2, label: '0.5 kg'},
|
||||
{ value: 3, label: '1 kg'},
|
||||
],
|
||||
genders: [
|
||||
{value: 1, label: 'male'},
|
||||
{value: 2, label: 'female'},
|
||||
],
|
||||
isLoading: false,
|
||||
error: {},
|
||||
gender: '',
|
||||
goal: 0,
|
||||
birthday: new Date(),
|
||||
birthday: '',
|
||||
height: 0,
|
||||
weight: {
|
||||
current: 0,
|
||||
goal: 0,
|
||||
},
|
||||
currentWeight: 0,
|
||||
goalWeight: 0,
|
||||
rateOfChange: 0,
|
||||
activity: 0,
|
||||
dailyCalories: 0,
|
||||
weeksToGoal: 0,
|
||||
};
|
||||
|
||||
const loginPageReducer = produce((draft, action) => {
|
||||
switch(action.type) {
|
||||
case GET_PROFILE_SUCCESS:
|
||||
case UPDATE_PROFILE_SUCCESS:
|
||||
draft.birthday = action.birthday;
|
||||
draft.gender = action.gender;
|
||||
draft.goal = action.goal;
|
||||
draft.height = action.height;
|
||||
draft.weight.current = action.weight.current;
|
||||
draft.weight.goal = action.weight.goal;
|
||||
draft.currentWeight = action.currentWeight;
|
||||
draft.goalWeight = action.goalWeight;
|
||||
draft.rateOfChange = action.rateOfChange;
|
||||
draft.activity = action.activity;
|
||||
draft.weeksToGoal = action.weeksToGoal;
|
||||
draft.dailyCalories = action.dailyCalories;
|
||||
draft.isLoading = false;
|
||||
break;
|
||||
|
||||
case GET_PROFILE_REQUEST:
|
||||
case UPDATE_PROFILE_REQUEST:
|
||||
draft.isLoading = true;
|
||||
break;
|
||||
|
||||
case GET_PROFILE_ERROR:
|
||||
case UPDATE_PROFILE_ERROR:
|
||||
draft.isLoading = false;
|
||||
draft.error = action.error;
|
||||
break;
|
||||
|
||||
case PROFILE_INPUT_CHANGE:
|
||||
draft[action.name] = action.value;
|
||||
break;
|
||||
}
|
||||
}, initialState);
|
||||
|
||||
|
@ -1,17 +1,7 @@
|
||||
import { takeLatest, call, put, select } from 'redux-saga/effects';
|
||||
import { api, request } from 'utils';
|
||||
import { GET_PROFILE_REQUEST, UPDATE_PROFILE_REQUEST } from './constants';
|
||||
import {
|
||||
makeSelectBirthday,
|
||||
makeSelectHeight,
|
||||
makeSelectWeight,
|
||||
makeSelectRateOfChange,
|
||||
makeSelectActivity,
|
||||
makeSelectGoal,
|
||||
makeSelectGender
|
||||
} from './selectors';
|
||||
import { updateProfileErrorAction, updateProfileSuccessAction, getProfileErrorAction, getProfileSuccessAction } from './actions';
|
||||
|
||||
import {api, request} from 'utils';
|
||||
import { GET_PROFILE_REQUEST } from './constants';
|
||||
import { getProfileErrorAction, getProfileSuccessAction } from './actions';
|
||||
import { makeSelectTokens } from 'containers/App/selectors'
|
||||
|
||||
export function* getProfile() {
|
||||
@ -27,41 +17,24 @@ export function* getProfile() {
|
||||
};
|
||||
|
||||
try {
|
||||
const { birthday, gender, height, currentWeight, goalWeight, rateOfChange, activity } = yield call(request, requestURL, requestParameters);
|
||||
yield put(getProfileSuccessAction({birthday, gender, height, currentWeight, goalWeight, rateOfChange, activity}));
|
||||
const {
|
||||
birthday,
|
||||
gender,
|
||||
height,
|
||||
currentWeight,
|
||||
goalWeight,
|
||||
rateOfChange,
|
||||
goal,
|
||||
activity,
|
||||
dailyCalories,
|
||||
weeksToGoal,
|
||||
} = yield call(request, requestURL, requestParameters);
|
||||
yield put(getProfileSuccessAction({ weeksToGoal, goal, dailyCalories, birthday, gender, height, currentWeight, goalWeight, rateOfChange, activity}));
|
||||
} catch (error) {
|
||||
yield put(getProfileErrorAction({ error: error.message }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* updateProfile() {
|
||||
const { access } = yield select(makeSelectTokens());
|
||||
|
||||
const gender = yield select(makeSelectGender());
|
||||
const goal = yield select(makeSelectGoal());
|
||||
const birthday = yield select(makeSelectBirthday());
|
||||
const height = yield select(makeSelectHeight());
|
||||
const weight = yield select(makeSelectWeight());
|
||||
const rateOfChange = yield select(makeSelectRateOfChange());
|
||||
const activity = yield select(makeSelectActivity());
|
||||
|
||||
const requestURL = api.profile;
|
||||
|
||||
const requestParameters = {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${access.token}`, },
|
||||
body: JSON.stringify({ birthday, gender, height, weight, goal, rateOfChange, activity }),
|
||||
};
|
||||
|
||||
try {
|
||||
const { birthday, gender, height, weight, goalWeight, rateOfChange, activity } = yield call(request, requestURL, requestParameters);
|
||||
yield put(updateProfileSuccessAction({birthday, gender, height, weight, goalWeight, rateOfChange, activity}));
|
||||
} catch (error) {
|
||||
yield put(updateProfileErrorAction({error: error.message}));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* profilePageSaga() {
|
||||
yield takeLatest(GET_PROFILE_REQUEST, getProfile);
|
||||
yield takeLatest(UPDATE_PROFILE_REQUEST, updateProfile);
|
||||
}
|
||||
|
@ -10,10 +10,13 @@ const makeSelectBirthday = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.birthday);
|
||||
|
||||
const makeSelectHeight = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.height);
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.height)
|
||||
|
||||
const makeSelectWeight = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.weight);
|
||||
const makeSelectCurrentWeight = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.currentWeight);
|
||||
|
||||
const makeSelectGoalWeight = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.goalWeight);
|
||||
|
||||
const makeSelectRateOfChange = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.rateOfChange);
|
||||
@ -30,17 +33,11 @@ const makeSelectGender = () =>
|
||||
const makeSelectIsLoading = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.isLoading);
|
||||
|
||||
const makeSelectGoals = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.goals);
|
||||
const makeSelectDailyCalories = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.dailyCalories);
|
||||
|
||||
const makeSelectActivities = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.activities);
|
||||
|
||||
const makeSelectGenders = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.genders);
|
||||
|
||||
const makeSelectRatesOfChange = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.ratesOfChange);
|
||||
const makeSelectWeeksToGoal = () =>
|
||||
createSelector(selectProfilePageDomain, (substate) => substate.weeksToGoal);
|
||||
|
||||
|
||||
export {
|
||||
@ -48,14 +45,13 @@ export {
|
||||
makeSelectError,
|
||||
makeSelectBirthday,
|
||||
makeSelectHeight,
|
||||
makeSelectWeight,
|
||||
makeSelectCurrentWeight,
|
||||
makeSelectGoalWeight,
|
||||
makeSelectRateOfChange,
|
||||
makeSelectActivity,
|
||||
makeSelectGoal,
|
||||
makeSelectGender,
|
||||
makeSelectWeeksToGoal,
|
||||
makeSelectDailyCalories,
|
||||
makeSelectIsLoading,
|
||||
makeSelectGoals,
|
||||
makeSelectActivities,
|
||||
makeSelectGenders,
|
||||
makeSelectRatesOfChange
|
||||
};
|
||||
|
14
src/pages/Register/FormModel/formInitialValues.js
Normal file
14
src/pages/Register/FormModel/formInitialValues.js
Normal file
@ -0,0 +1,14 @@
|
||||
import checkoutFormModel from './registerFormModel';
|
||||
const {
|
||||
formField: {
|
||||
email,
|
||||
password
|
||||
}
|
||||
} = checkoutFormModel;
|
||||
|
||||
const formInitialValues = {
|
||||
[email.name]: 'admin@admin.com',
|
||||
[password.name]: 'Kox32113@#$',
|
||||
};
|
||||
|
||||
export default formInitialValues
|
19
src/pages/Register/FormModel/registerFormModel.js
Normal file
19
src/pages/Register/FormModel/registerFormModel.js
Normal file
@ -0,0 +1,19 @@
|
||||
const registerFormModel = {
|
||||
formId: 'loginForm',
|
||||
formField: {
|
||||
email: {
|
||||
name: 'email',
|
||||
label: 'Email*',
|
||||
requiredErrorMsg: 'Email is required',
|
||||
invalidErrorMsg: 'Email is not valid'
|
||||
},
|
||||
password: {
|
||||
name: 'password',
|
||||
label: 'Password*',
|
||||
requiredErrorMsg: 'Password is required',
|
||||
invalidErrorMsg: 'Password is to short'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default registerFormModel
|
21
src/pages/Register/FormModel/validationSchema.js
Normal file
21
src/pages/Register/FormModel/validationSchema.js
Normal file
@ -0,0 +1,21 @@
|
||||
import * as Yup from 'yup';
|
||||
import checkoutFormModel from './registerFormModel';
|
||||
const {
|
||||
formField: {
|
||||
email,
|
||||
password
|
||||
}
|
||||
} = checkoutFormModel;
|
||||
|
||||
const validationSchema =
|
||||
Yup.object().shape({
|
||||
[email.name]: Yup.string().required(`${email.requiredErrorMsg}`).email(`${email.invalidErrorMsg}`),
|
||||
[password.name]: Yup.string().required(`${password.requiredErrorMsg}`)
|
||||
.test(
|
||||
'len',
|
||||
`${password.invalidErrorMsg}`,
|
||||
value => value && value.toString().length >= 8
|
||||
),
|
||||
})
|
||||
|
||||
export default validationSchema
|
@ -1,13 +1,9 @@
|
||||
import { REGISTER_INPUT_CHANGE, REGISTER_REQUEST, REGISTER_SUCCESS, REGISTER_ERROR } from './constants';
|
||||
import { REGISTER_REQUEST, REGISTER_SUCCESS, REGISTER_ERROR } from './constants';
|
||||
|
||||
export const registerInputChange = ({name, value}) => ({
|
||||
type: REGISTER_INPUT_CHANGE,
|
||||
name,
|
||||
value,
|
||||
})
|
||||
|
||||
export const registerAction = () => ({
|
||||
export const registerAction = ({ email, password }) => ({
|
||||
type: REGISTER_REQUEST,
|
||||
email,
|
||||
password
|
||||
})
|
||||
|
||||
export const registerSuccessAction = ({user, tokens}) => ({
|
||||
|
@ -1,5 +1,3 @@
|
||||
export const REGISTER_REQUEST = 'app/registerPage/REGISTER_REQUEST';
|
||||
export const REGISTER_SUCCESS = 'app/registerPage/REGISTER_SUCCESS';
|
||||
export const REGISTER_ERROR = 'app/registerPage/REGISTER_ERROR';
|
||||
|
||||
export const REGISTER_INPUT_CHANGE = 'app/registerPage/REGISTER_INPUT_CHANGE';
|
||||
|
@ -1,108 +1,87 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {Container, Typography, Grid, Button, TextField, Link} from '@material-ui/core';
|
||||
import {Container, Typography, Grid, Button, Link, FormControlLabel, Checkbox} from '@material-ui/core';
|
||||
import { routes } from 'utils';
|
||||
import {useInjectReducer, useInjectSaga} from "redux-injectors";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import reducer from "./reducer";
|
||||
import saga from "./saga";
|
||||
import {registerAction, registerInputChange} from "./actions";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {makeSelectEmail, makeSelectIsLoading, makeSelectPassword} from "./selectors";
|
||||
import {useDispatch} from "react-redux";
|
||||
import InputField from "components/InputField";
|
||||
import useStyles from './styles'
|
||||
import {Form, Formik} from "formik";
|
||||
import {registerAction} from "./actions";
|
||||
import formInitialValues from "./FormModel/formInitialValues";
|
||||
import validationSchema from "./FormModel/validationSchema";
|
||||
import registerFormModel from './FormModel/registerFormModel'
|
||||
import {useInjectSaga} from "redux-injectors";
|
||||
import saga from './saga'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
paper: {
|
||||
marginTop: theme.spacing(8),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
form: {
|
||||
width: '100%',
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
submit: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
}));
|
||||
|
||||
const stateSelector = createStructuredSelector({
|
||||
email: makeSelectEmail(),
|
||||
password: makeSelectPassword(),
|
||||
isLoading: makeSelectIsLoading(),
|
||||
});
|
||||
const {
|
||||
formId,
|
||||
formField: {
|
||||
email,
|
||||
password
|
||||
}
|
||||
} = registerFormModel;
|
||||
|
||||
const key = 'registerPage'
|
||||
const RegisterPage = () => {
|
||||
const classes = useStyles();
|
||||
useInjectReducer({ key, reducer });
|
||||
useInjectSaga({ key, saga });
|
||||
const { email, password, isLoading } = useSelector(stateSelector)
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onChangeInput = ({target: { name, value }}) => {
|
||||
dispatch(registerInputChange({name, value}))
|
||||
}
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault()
|
||||
dispatch(registerAction())
|
||||
const onSubmit = (values, actions) => {
|
||||
dispatch(registerAction(values))
|
||||
}
|
||||
|
||||
return (
|
||||
<Container component="main" maxWidth="xs">
|
||||
<div className={classes.paper}>
|
||||
<Typography component="h1" variant="h5">
|
||||
Create Account
|
||||
Create account
|
||||
</Typography>
|
||||
<form className={classes.form} onSubmit={handleSubmit} noValidate>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
id="email"
|
||||
label="Email Address"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
value={email}
|
||||
onChange={onChangeInput}
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={onChangeInput}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
<Grid container alignItems="flex-end">
|
||||
<Grid item>
|
||||
<Link href={routes.login.path} color="secondary" variant="body2">
|
||||
Have already account? Sign In
|
||||
</Link>
|
||||
</Grid>
|
||||
<Formik
|
||||
initialValues={formInitialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form id={formId}>
|
||||
<InputField
|
||||
type="text"
|
||||
label={email.label}
|
||||
name={email.name}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
className={classes.input}
|
||||
/>
|
||||
<InputField
|
||||
type="password"
|
||||
label={password.label}
|
||||
name={password.name}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
className={classes.input}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<Grid container alignItems="flex-end">
|
||||
<Grid item>
|
||||
<Link href={routes.login.path} color="secondary" variant="body2">
|
||||
Have already account? Sign In
|
||||
</Link>
|
||||
</Grid>
|
||||
</form>
|
||||
</Grid>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RegisterPage
|
||||
|
@ -1,5 +1,5 @@
|
||||
import produce from 'immer';
|
||||
import {REGISTER_INPUT_CHANGE, REGISTER_ERROR, REGISTER_REQUEST, REGISTER_SUCCESS} from './constants';
|
||||
import {REGISTER_ERROR, REGISTER_REQUEST, REGISTER_SUCCESS} from './constants';
|
||||
|
||||
export const initialState = {
|
||||
isLoading: false,
|
||||
@ -16,16 +16,14 @@ const registerPageReducer = produce((draft, action) => {
|
||||
|
||||
case REGISTER_REQUEST:
|
||||
draft.isLoading = true;
|
||||
draft.email = action.email;
|
||||
draft.password = action.password;
|
||||
break;
|
||||
|
||||
case REGISTER_ERROR:
|
||||
draft.isLoading = false;
|
||||
draft.error = action.error;
|
||||
break;
|
||||
|
||||
case REGISTER_INPUT_CHANGE:
|
||||
draft[action.name] = action.value;
|
||||
break;
|
||||
}
|
||||
}, initialState);
|
||||
|
||||
|
@ -19,7 +19,7 @@ export function* register() {
|
||||
try {
|
||||
const { user, tokens } = yield call(request, requestURL, requestParameters);
|
||||
yield put(registerSuccessAction({user, tokens}));
|
||||
yield put(push(routes.profile.path));
|
||||
yield put(push(routes.createProfile.path));
|
||||
} catch (error) {
|
||||
yield put(registerErrorAction({ error: error.message }));
|
||||
}
|
||||
|
18
src/pages/Register/styles.js
Normal file
18
src/pages/Register/styles.js
Normal file
@ -0,0 +1,18 @@
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
paper: {
|
||||
marginTop: theme.spacing(8),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
submit: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
}))
|
||||
|
||||
export default useStyles
|
@ -1,13 +1,18 @@
|
||||
const API_BASE_URL = 'http://localhost:3001/v1'
|
||||
const AUTH = 'auth';
|
||||
const PROFILE = 'profiles';
|
||||
const MEALS = 'meals';
|
||||
const PRODUCTS = 'products'
|
||||
|
||||
const urls = {
|
||||
auth: {
|
||||
login: `${API_BASE_URL}/${AUTH}/login`,
|
||||
register: `${API_BASE_URL}/${AUTH}/register`,
|
||||
refreshToken: `${API_BASE_URL}/${AUTH}/refresh-tokens`
|
||||
},
|
||||
profile: `${API_BASE_URL}/${PROFILE}`,
|
||||
meals: `${API_BASE_URL}/${MEALS}`,
|
||||
products: `${API_BASE_URL}/${PRODUCTS}`,
|
||||
}
|
||||
|
||||
export default urls
|
||||
|
14
src/utils/calculate.js
Normal file
14
src/utils/calculate.js
Normal file
@ -0,0 +1,14 @@
|
||||
export const calculateMacro = (unit, portion, quantity) => {
|
||||
const isUnitPortion = unit === 'portion'
|
||||
|
||||
if (isUnitPortion) {
|
||||
return portion * quantity
|
||||
}
|
||||
|
||||
return (portion / 100) * quantity
|
||||
}
|
||||
|
||||
export const sumMacro = (meals, key) => meals.reduce(
|
||||
(total, meal) => total += meal.products.reduce(
|
||||
(mealTotal, { product }) => mealTotal += product[key], 0)
|
||||
, 0)
|
@ -1,24 +1,26 @@
|
||||
import purple from "@material-ui/core/colors/purple";
|
||||
import amber from "@material-ui/core/colors/amber";
|
||||
import blue from "@material-ui/core/colors/blue";
|
||||
import {green, deepPurple, amber, blue} from "@material-ui/core/colors";
|
||||
|
||||
export const MEALS_LIST = [
|
||||
{
|
||||
label: 'eggs',
|
||||
macronutrients: [
|
||||
{
|
||||
label: 'Calories',
|
||||
value: 1245,
|
||||
unit: ' kcal',
|
||||
unit: 'kcal',
|
||||
},
|
||||
{
|
||||
label: 'Carbs',
|
||||
value: 228.6,
|
||||
unit: 'g',
|
||||
},
|
||||
{
|
||||
label: 'Fat',
|
||||
value: 227.0,
|
||||
unit: 'g',
|
||||
},
|
||||
{
|
||||
label: 'Protein',
|
||||
value: 127.0,
|
||||
unit: 'g',
|
||||
},
|
||||
@ -28,18 +30,22 @@ export const MEALS_LIST = [
|
||||
label: 'bread',
|
||||
macronutrients: [
|
||||
{
|
||||
label: 'Calories',
|
||||
value: 1245,
|
||||
unit: ' kcal',
|
||||
unit: 'kcal',
|
||||
},
|
||||
{
|
||||
label: 'Carbs',
|
||||
value: 228.6,
|
||||
unit: 'g',
|
||||
},
|
||||
{
|
||||
label: 'Fat',
|
||||
value: 227.0,
|
||||
unit: 'g',
|
||||
},
|
||||
{
|
||||
label: 'Protein',
|
||||
value: 127.0,
|
||||
unit: 'g',
|
||||
},
|
||||
@ -49,18 +55,22 @@ export const MEALS_LIST = [
|
||||
label: 'corn flakes',
|
||||
macronutrients: [
|
||||
{
|
||||
label: 'Calories',
|
||||
value: 1245,
|
||||
unit: ' kcal',
|
||||
unit: 'kcal',
|
||||
},
|
||||
{
|
||||
label: 'Carbs',
|
||||
value: 228.6,
|
||||
unit: 'g',
|
||||
},
|
||||
{
|
||||
label: 'Fat',
|
||||
value: 227.0,
|
||||
unit: 'g',
|
||||
},
|
||||
{
|
||||
label: 'Protein',
|
||||
value: 127.0,
|
||||
unit: 'g',
|
||||
},
|
||||
@ -72,18 +82,22 @@ export const MENU_LIST = [
|
||||
{
|
||||
macronutrients: [
|
||||
{
|
||||
label: 'Calories',
|
||||
value: 1245,
|
||||
unit: ' kcal',
|
||||
},
|
||||
{
|
||||
label: 'Carbs',
|
||||
value: 228.6,
|
||||
unit: 'g',
|
||||
},
|
||||
{
|
||||
label: 'Fat',
|
||||
value: 227.0,
|
||||
unit: 'g',
|
||||
},
|
||||
{
|
||||
label: 'Protein',
|
||||
value: 127.0,
|
||||
unit: 'g',
|
||||
},
|
||||
@ -111,22 +125,32 @@ export const MENU_LIST = [
|
||||
export const CALORIESLEFT = 1234
|
||||
|
||||
export const MACRONUTRIENTS = [
|
||||
{
|
||||
current: 123,
|
||||
max: 250,
|
||||
label: 'Calories',
|
||||
unit: 'kcal',
|
||||
color: green[600],
|
||||
},
|
||||
{
|
||||
current: 123,
|
||||
max: 250,
|
||||
label: 'Carbs',
|
||||
color: purple[500],
|
||||
unit: 'g',
|
||||
color: deepPurple[500],
|
||||
},
|
||||
{
|
||||
current: 35,
|
||||
max: 250,
|
||||
label: 'Fat',
|
||||
unit: 'g',
|
||||
color: amber[500],
|
||||
},
|
||||
{
|
||||
current: 210,
|
||||
max: 250,
|
||||
label: 'Protein',
|
||||
unit: 'g',
|
||||
color: blue[500],
|
||||
},
|
||||
]
|
||||
|
@ -1,29 +1,47 @@
|
||||
import HomePage from "pages/Home";
|
||||
import CreateProfilePage from "pages/CreateProfile";
|
||||
import ProfilePage from "pages/Profile";
|
||||
import LoginPage from "pages/Login";
|
||||
import RegisterPage from "pages/Register";
|
||||
import LogoutPage from "pages/Logout";
|
||||
|
||||
const routes = {
|
||||
dashboard: {
|
||||
path: '/',
|
||||
exact: true,
|
||||
privateRoute: true,
|
||||
component: HomePage,
|
||||
},
|
||||
createProfile: {
|
||||
path: '/create-profile',
|
||||
exact: true,
|
||||
privateRoute: true,
|
||||
component: CreateProfilePage,
|
||||
},
|
||||
profile: {
|
||||
path: '/profile',
|
||||
exact: true,
|
||||
privateRoute: true,
|
||||
component: ProfilePage,
|
||||
},
|
||||
login: {
|
||||
path: '/login',
|
||||
exact: true,
|
||||
privateRoute: false,
|
||||
component: LoginPage,
|
||||
},
|
||||
register: {
|
||||
path: '/register',
|
||||
exact: true,
|
||||
privateRoute: false,
|
||||
component: RegisterPage,
|
||||
},
|
||||
logout: {
|
||||
path: '/logout',
|
||||
exact: true,
|
||||
privateRoute: false,
|
||||
component: LogoutPage,
|
||||
},
|
||||
}
|
||||
|
||||
export default routes
|
||||
|
5
src/utils/sort.js
Normal file
5
src/utils/sort.js
Normal file
@ -0,0 +1,5 @@
|
||||
export const sortMeals = (defaultMeals, meals, labels) =>
|
||||
defaultMeals
|
||||
.filter((candidate) => !labels.includes(candidate.label))
|
||||
.concat(meals)
|
||||
.sort(({ label: aLabel}, { label: bLabel }) => aLabel.localeCompare(bLabel))
|
@ -1,4 +1,12 @@
|
||||
import { createMuiTheme } from '@material-ui/core/styles';
|
||||
import {green, deepPurple, amber, blue} from "@material-ui/core/colors";
|
||||
|
||||
export const colors = {
|
||||
calories: green[600],
|
||||
carbs: deepPurple[500],
|
||||
fat: amber[500],
|
||||
protein: blue[500],
|
||||
}
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
|
89
yarn.lock
89
yarn.lock
@ -970,7 +970,7 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
version "7.12.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
|
||||
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
|
||||
@ -1081,11 +1081,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06"
|
||||
integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==
|
||||
|
||||
"@hapi/hoek@^9.0.0":
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6"
|
||||
integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==
|
||||
|
||||
"@hapi/joi@^15.1.0":
|
||||
version "15.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7"
|
||||
@ -1103,13 +1098,6 @@
|
||||
dependencies:
|
||||
"@hapi/hoek" "^8.3.0"
|
||||
|
||||
"@hapi/topo@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7"
|
||||
integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||
@ -1523,23 +1511,6 @@
|
||||
estree-walker "^1.0.1"
|
||||
picomatch "^2.2.2"
|
||||
|
||||
"@sideway/address@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.0.tgz#0b301ada10ac4e0e3fa525c90615e0b61a72b78d"
|
||||
integrity sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@sideway/formula@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
||||
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
||||
|
||||
"@sideway/pinpoint@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
|
||||
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
|
||||
|
||||
"@sinonjs/commons@^1.7.0":
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217"
|
||||
@ -1826,6 +1797,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||
|
||||
"@types/lodash@^4.14.165":
|
||||
version "4.14.165"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f"
|
||||
integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
@ -2581,6 +2557,13 @@ axe-core@^3.5.4:
|
||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227"
|
||||
integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==
|
||||
|
||||
axios@^0.21.0:
|
||||
version "0.21.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.0.tgz#26df088803a2350dff2c27f96fef99fe49442aca"
|
||||
integrity sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==
|
||||
dependencies:
|
||||
follow-redirects "^1.10.0"
|
||||
|
||||
axobject-query@^2.1.2:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
||||
@ -5214,6 +5197,11 @@ follow-redirects@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
|
||||
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
|
||||
|
||||
follow-redirects@^1.10.0:
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
|
||||
integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
|
||||
|
||||
for-in@^0.1.3:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
|
||||
@ -6916,17 +6904,6 @@ jest@26.6.0:
|
||||
import-local "^3.0.2"
|
||||
jest-cli "^26.6.0"
|
||||
|
||||
joi@^17.3.0:
|
||||
version "17.3.0"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2"
|
||||
integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
"@hapi/topo" "^5.0.0"
|
||||
"@sideway/address" "^4.1.0"
|
||||
"@sideway/formula" "^3.0.0"
|
||||
"@sideway/pinpoint" "^2.0.0"
|
||||
|
||||
jpeg-js@^0.3.2:
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.3.7.tgz#471a89d06011640592d314158608690172b1028d"
|
||||
@ -7324,7 +7301,7 @@ locate-path@^5.0.0:
|
||||
dependencies:
|
||||
p-locate "^4.1.0"
|
||||
|
||||
lodash-es@^4.17.14:
|
||||
lodash-es@^4.17.11, lodash-es@^4.17.14:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
||||
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
|
||||
@ -7764,6 +7741,11 @@ nan@^2.12.1:
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
|
||||
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
|
||||
|
||||
nanoclone@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
|
||||
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
|
||||
|
||||
nanoid@^3.1.15:
|
||||
version "3.1.15"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.15.tgz#28e7c4ce56aff2d0c2d37814c7aef9d6c5b3e6f3"
|
||||
@ -9302,6 +9284,11 @@ prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.8.1"
|
||||
|
||||
property-expr@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910"
|
||||
integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==
|
||||
|
||||
proxy-addr@~2.0.5:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
||||
@ -11205,6 +11192,11 @@ toidentifier@1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||
|
||||
toposort@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
|
||||
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
|
||||
|
||||
tough-cookie@^2.3.3, tough-cookie@~2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
||||
@ -12157,3 +12149,16 @@ yarn@^1.22.10:
|
||||
version "1.22.10"
|
||||
resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.10.tgz#c99daa06257c80f8fa2c3f1490724e394c26b18c"
|
||||
integrity sha512-IanQGI9RRPAN87VGTF7zs2uxkSyQSrSPsju0COgbsKQOOXr5LtcVPeyXWgwVa0ywG3d8dg6kSYKGBuYK021qeA==
|
||||
|
||||
yup@^0.32.8:
|
||||
version "0.32.8"
|
||||
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.8.tgz#16e4a949a86a69505abf99fd0941305ac9adfc39"
|
||||
integrity sha512-SZulv5FIZ9d5H99EN5tRCRPXL0eyoYxWIP1AacCrjC9d4DfP13J1dROdKGfpfRHT3eQB6/ikBl5jG21smAfCkA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.5"
|
||||
"@types/lodash" "^4.14.165"
|
||||
lodash "^4.17.20"
|
||||
lodash-es "^4.17.11"
|
||||
nanoclone "^0.2.1"
|
||||
property-expr "^2.0.4"
|
||||
toposort "^2.0.2"
|
||||
|
Loading…
Reference in New Issue
Block a user