Compare commits

...

17 Commits

Author SHA1 Message Date
=
a7628f00a3 Add logout component 2021-01-30 12:57:35 +01:00
=
9856768d11 Fix barcode scanner 2021-01-30 12:35:53 +01:00
=
049e2fb57e Add removing products from meal lis 2021-01-29 21:15:17 +01:00
=
6c8bbc6e3f Display card with marco 2021-01-26 19:53:37 +01:00
=
8f72bfdb23 Add marco display 2021-01-24 23:32:34 +01:00
=
d3b1fa03fc Fixes 2021-01-24 21:23:52 +01:00
=
8191799760 Add modal for barcode scanner 2021-01-19 18:39:12 +01:00
=
52dfb0522f Little refactor of barcode scanner 2021-01-17 20:50:48 +01:00
=
26c4406401 Add basic barcode scanner 2021-01-17 20:41:51 +01:00
=
1a611fbea1 Fix creating meal list and adding products 2021-01-16 22:53:26 +01:00
=
bf835d2a89 Add searching products by name 2021-01-12 22:44:00 +01:00
=
c11dd4a8af Add redux for meals page 2021-01-02 22:41:09 +01:00
=
8160437379 Add redux saga for home apge 2020-12-29 16:39:17 +01:00
=
64ab7aef58 Add the calculation of macronutrients from meals 2020-12-22 19:44:38 +01:00
=
693c89c666 Add profile page and basic meal page 2020-12-22 16:43:41 +01:00
=
81277d0d75 Add create profile form validation 2020-12-18 16:27:54 +01:00
0a4461bbe4 Merge pull request 'Add formik' (#2) from formik into master
Reviewed-on: #2
2020-12-17 17:12:26 +01:00
96 changed files with 3068 additions and 1512 deletions

View File

@ -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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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

View File

@ -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",

View File

@ -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

View File

@ -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;

View File

@ -0,0 +1,14 @@
import productFormModel from './productFormModel';
const {
formField: {
quantity,
unit,
}
} = productFormModel;
const formInitialValues = {
[quantity.name]: 1,
[unit.name]: 'portion',
};
export default formInitialValues

View File

@ -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

View File

@ -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

View File

@ -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

View 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;

View 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;

View File

@ -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;

View File

@ -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

View 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

View 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

View File

@ -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}
/>
);
}

View 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;

View File

@ -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

View File

@ -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

View 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;

View File

@ -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

View 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;

View File

@ -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

View File

@ -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;

View 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

View File

@ -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

View 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

View 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;

View File

@ -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,

View 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

View File

@ -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

View File

@ -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

View 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;

View File

@ -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
};

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -0,0 +1,5 @@
import { LOGOUT } from './constants'
export const logoutAction = () => ({
type: LOGOUT
})

View File

@ -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';

View File

@ -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>
);

View File

@ -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);

View 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;

View 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;

View 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

View File

@ -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;

View File

@ -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>
);
};

View 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

View 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

View 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

View 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

View File

@ -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>

View File

@ -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

View 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,
})

View 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';

View 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

View 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;

View 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);
}

View 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
};

View 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
View 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,
})

View 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'

View File

@ -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
View 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
View 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);
}

View 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
View 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

View 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

View 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

View 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

View File

@ -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}) => ({

View File

@ -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>
);

View File

@ -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
View 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
View 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;

View File

@ -1,11 +0,0 @@
import React from 'react';
const ReviewProfileForm = () => {
return (
<div>
ReviewProfileForm
</div>
);
};
export default ReviewProfileForm;

View File

@ -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,
})

View File

@ -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';

View File

@ -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;

View File

@ -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);

View File

@ -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);
}

View File

@ -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
};

View 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

View 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

View 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

View File

@ -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}) => ({

View File

@ -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';

View File

@ -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

View File

@ -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);

View File

@ -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 }));
}

View 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

View File

@ -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
View 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)

View File

@ -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],
},
]

View File

@ -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
View 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))

View File

@ -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: {

View File

@ -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"