diff --git a/src/assets/logo.svg b/src/assets/logo.svg index 4b9393f..15ef44c 100644 --- a/src/assets/logo.svg +++ b/src/assets/logo.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/src/components/AddProductToMealDialog/ProductItem.js b/src/components/AddProductToMealDialog/ProductItem.js new file mode 100644 index 0000000..fac8ac1 --- /dev/null +++ b/src/components/AddProductToMealDialog/ProductItem.js @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types' +import {Checkbox, ListItem, ListItemSecondaryAction, ListItemText} from "@material-ui/core"; +import ProductLabel from 'components/ProductLabel' + +const ProductItem = ({ id, verified, eco, label, unit, servingCapacity, macronutrients }) => { + return ( + + } + disableTypography + /> + + + + + ); +}; + +ProductItem.propTypes = { + 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; diff --git a/src/components/AddProductToMealDialog/index.js b/src/components/AddProductToMealDialog/index.js new file mode 100644 index 0000000..91d75c9 --- /dev/null +++ b/src/components/AddProductToMealDialog/index.js @@ -0,0 +1,96 @@ +import React, { useState } from 'react' +import PropTypes from 'prop-types' +import { + DialogTitle, + DialogContent, + Grid, + Button, + DialogActions, + IconButton, + Dialog, + List, +} from '@material-ui/core'; +import {Add as AddIcon, Delete as DeleteIcon} from "@material-ui/icons"; +import SearchInput from 'components/SearchInput' +import BarcodeScanner from 'components/BarcodeScanner' +import { createStructuredSelector } from 'reselect' +import { makeSelectProducts, makeSelectProduct } from 'pages/Home/selectors' +import { useSelector } from 'react-redux' +import ProductItem from './ProductItem' + +const stateSelector = createStructuredSelector({ + products: makeSelectProducts(), + product: makeSelectProduct(), +}) + +const AddProductToMealDialog = ({ mealLabel }) => { + const [isOpen, setIsOpen] = useState(false); + const { products, product } = useSelector(stateSelector) + + const handleClose = (event) => { + event.stopPropagation(); + setIsOpen(false) + } + + const handleOpen = (event) => { + event.stopPropagation(); + setIsOpen(true) + } + + return ( + + event.stopPropagation()}> + + + + Add products to {mealLabel} + + + + + + + {products.map(({ id, verified, eco, salt, brand, servingCapacity, barcode, label, unit, calories, fat, carbohydrates, protein }) => ( + + ))} + + + + + + + + ) +}; + +AddProductToMealDialog.propTypes = { + mealLabel: PropTypes.string.isRequired, +} + +export default AddProductToMealDialog; diff --git a/src/components/BarcodeScanner/index.js b/src/components/BarcodeScanner/index.js index 7259b75..00d7a72 100644 --- a/src/components/BarcodeScanner/index.js +++ b/src/components/BarcodeScanner/index.js @@ -1,55 +1,16 @@ -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) - }, []) +import React from 'react'; +import {IconButton, Grid} from '@material-ui/core' +import {CropFree as CropFreeIcon} from '@material-ui/icons' +const BarcodeScanner = () => { return ( - - - + + + + + + + ); } diff --git a/src/components/MacronutrientsChart/index.js b/src/components/MacronutrientsChart/index.js index f601f4d..bd008f6 100644 --- a/src/components/MacronutrientsChart/index.js +++ b/src/components/MacronutrientsChart/index.js @@ -24,7 +24,7 @@ const MacronutrientsChart = ({ max, current, unit, label }) => { height={40} className={classes.macronutrientsChart} /> - + {current}{unit} diff --git a/src/components/MacronutrientsDetails/index.js b/src/components/MacronutrientsDetails/index.js index 981c1e7..22242d0 100644 --- a/src/components/MacronutrientsDetails/index.js +++ b/src/components/MacronutrientsDetails/index.js @@ -6,8 +6,8 @@ import MacronutrientsChart from "components/MacronutrientsChart"; const MacronutrientsDetails = ({ macronutrients }) => { return ( - - 250g + + 250g { - - console.log(products[0]) - const calcMealMacronutrients = (dishes) => { const mealMacronutrients = dishes .flatMap(({ macronutrients }) => macronutrients) @@ -36,19 +32,11 @@ const MealCard = ({ label, products }) => { - {JSON.stringify(products)} {label} - event.stopPropagation()} - onFocus={(event) => event.stopPropagation()} - > - - + { diff --git a/src/components/Navbar/index.js b/src/components/Navbar/index.js index d08a343..5a2cb68 100644 --- a/src/components/Navbar/index.js +++ b/src/components/Navbar/index.js @@ -4,6 +4,8 @@ import {AppBar, IconButton, Toolbar, Typography} from "@material-ui/core"; import {Menu as MenuIcon, Waves as WavesIcon } from "@material-ui/icons"; import useStyles from './styles' +import {ReactComponent as LogoIcon} from 'assets/logo.svg' + const Navbar = ({ toggleDrawerNav }) => { const classes = useStyles() @@ -14,7 +16,7 @@ const Navbar = ({ toggleDrawerNav }) => { - + diff --git a/src/components/ProductCard/index.js b/src/components/ProductCard/index.js index b10bf35..b636939 100644 --- a/src/components/ProductCard/index.js +++ b/src/components/ProductCard/index.js @@ -11,6 +11,7 @@ const ProductCard = ({ label, macronutrients }) => { ( + + {text} {eco && } {verified && } + +); + +ProductLabel.propTypes = { + verified: PropTypes.bool.isRequired, + eco: PropTypes.bool.isRequired, + text: PropTypes.string.isRequired, +} + +export default ProductLabel; diff --git a/src/components/RadialChart/index.js b/src/components/RadialChart/index.js index 1c6e50a..20d576e 100644 --- a/src/components/RadialChart/index.js +++ b/src/components/RadialChart/index.js @@ -9,7 +9,7 @@ const RadialChart = ({ strokeWidth = 20, circleRadius = 50, progress, color, wi const strokeLength = circumference / 100 * progress; return ( -
+ -
+ ) } diff --git a/src/components/SearchInput/index.js b/src/components/SearchInput/index.js new file mode 100644 index 0000000..503bdb5 --- /dev/null +++ b/src/components/SearchInput/index.js @@ -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 ( + + + + ); +}; + +export default SearchInput; diff --git a/src/containers/App/reducer.js b/src/containers/App/reducer.js index 2bc100a..8252134 100644 --- a/src/containers/App/reducer.js +++ b/src/containers/App/reducer.js @@ -8,7 +8,7 @@ import { LOGIN_SUCCESS } from 'pages/Login/constants' import { REGISTER_SUCCESS } from 'pages/Register/constants' export const initialState = { - isLogged: true, + isLogged: false, notifications: [], tokens: { access: { diff --git a/src/containers/Routes/index.js b/src/containers/Routes/index.js index 28b5c38..bd7a9f3 100644 --- a/src/containers/Routes/index.js +++ b/src/containers/Routes/index.js @@ -9,27 +9,29 @@ const Routes = () => { const classes = useStyles() return ( - - - {Object.values(routes).map(({privateRoute, exact, path, component, }, index) => ( - - {privateRoute ? ( - - ) : ( - - )} - - ))} - - + + + + {Object.values(routes).map(({privateRoute, exact, path, component, }, index) => ( + + {privateRoute ? ( + + ) : ( + + )} + + ))} + + + ); }; diff --git a/src/containers/Routes/styles.js b/src/containers/Routes/styles.js index f7553bb..5b2f0c4 100644 --- a/src/containers/Routes/styles.js +++ b/src/containers/Routes/styles.js @@ -1,7 +1,7 @@ import {makeStyles} from "@material-ui/core/styles"; const useStyles = makeStyles((theme) => ({ - contentConainer: { + container: { marginTop: theme.spacing(10), display: 'flex', flexDirection: 'column', diff --git a/src/pages/Home/actions.js b/src/pages/Home/actions.js index e55b2d7..4cf4e90 100644 --- a/src/pages/Home/actions.js +++ b/src/pages/Home/actions.js @@ -11,8 +11,44 @@ import { 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 } from './constants'; +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 = ({ product }) => ({ + type: SEARCH_PRODUCT_BY_BARCODE_SUCCESS, + product, +}) + +export const searchProductByBarcodeErrorAction = ({ error }) => ({ + type: SEARCH_PRODUCT_BY_BARCODE_ERROR, + error, +}) + export const getMealsAction = ({ date }) => ({ type: GET_MEALS_REQUEST, date, diff --git a/src/pages/Home/constants.js b/src/pages/Home/constants.js index 8d370b8..99df3da 100644 --- a/src/pages/Home/constants.js +++ b/src/pages/Home/constants.js @@ -13,3 +13,11 @@ 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 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'; diff --git a/src/pages/Home/reducer.js b/src/pages/Home/reducer.js index 6275752..32e8525 100644 --- a/src/pages/Home/reducer.js +++ b/src/pages/Home/reducer.js @@ -13,51 +13,89 @@ import { 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, + SEARCH_PRODUCT_BY_LABEL_ERROR } from './constants'; +const defaultMeals = [ + { + id: 1, + label: 'Breakfast', + products: [], + }, + { + id: 2, + label: 'Snack I', + products: [], + }, + { + id: 3, + label: 'Lunch', + products: [], + }, + { + id: 4, + label: 'Snack II', + products: [], + }, + { + id: 5, + label: 'Dinner', + products: [], + }, + { + id: 6, + label: 'Snack III', + products: [], + }, + { + id: 7, + label: 'Supper', + products: [], + }, +] + + export const initialState = { isLoading: false, error: {}, label: '', products: [], + barcode: '', + product: {}, date: '', mealId: '', - meals: [ - { - label: 'Breakfast', - products: [], - }, - { - label: 'Snack I', - products: [], - }, - { - label: 'Lunch', - products: [], - }, - { - label: 'Snack II', - products: [], - }, - { - label: 'Dinner', - products: [], - }, - { - label: 'Snack III', - products: [], - }, - { - label: 'Supper', - products: [], - }, - ] + meals: defaultMeals }; const homePageReducer = produce((draft, action) => { switch(action.type) { case GET_MEALS_SUCCESS: - draft.meals = action.meals; + draft.meals = action.meals.length || defaultMeals; + 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.product = action.product; + 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; @@ -95,6 +133,8 @@ const homePageReducer = produce((draft, action) => { 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: draft.isLoading = false; draft.error = action.error; break; diff --git a/src/pages/Home/saga.js b/src/pages/Home/saga.js index bddf80e..d9d5102 100644 --- a/src/pages/Home/saga.js +++ b/src/pages/Home/saga.js @@ -1,11 +1,8 @@ import { takeLatest, call, put, select } from 'redux-saga/effects'; import {api, request, routes} from 'utils'; -import { GET_MEALS_REQUEST, UPDATE_MEAL_REQUEST, CREATE_MEAL_REQUEST, ADD_PRODUCTS_TO_MEAL_REQUEST } from './constants'; -import { - -} from './selectors'; -import { createMealSuccessAction, createMealErrorAction, updateMealErrorAction, updateMealSuccessAction, getMealsSuccessAction, getMealsErrorAction } from './actions'; -import { makeSelectLabel, makeSelectMealId, makeSelectProducts, makeSelectDate, } from './selectors' +import { GET_MEALS_REQUEST, UPDATE_MEAL_REQUEST, SEARCH_PRODUCT_BY_BARCODE_REQUEST, SEARCH_PRODUCT_BY_LABEL_REQUEST, CREATE_MEAL_REQUEST, ADD_PRODUCTS_TO_MEAL_REQUEST } from './constants'; +import { searchProductByLabelErrorAction, searchProductByLabelSuccessAction, createMealSuccessAction, createMealErrorAction, searchProductByBarcodeSuccessAction, searchProductByBarcodeErrorAction, updateMealErrorAction, updateMealSuccessAction, getMealsSuccessAction, getMealsErrorAction } from './actions'; +import { makeSelectLabel, makeSelectMealId, makeSelectBarcode, makeSelectProducts, makeSelectDate, } from './selectors' import { makeSelectTokens } from 'containers/App/selectors' import {push} from "connected-react-router"; @@ -98,7 +95,52 @@ export function* addProductsToMeal() { } } +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 { product } = yield call(request, requestURL, requestParameters); + yield put(searchProductByBarcodeSuccessAction({product})); + } 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(UPDATE_MEAL_REQUEST, updateMeal); yield takeLatest(CREATE_MEAL_REQUEST, createMeal); diff --git a/src/pages/Home/selectors.js b/src/pages/Home/selectors.js index c313b4f..b659b27 100644 --- a/src/pages/Home/selectors.js +++ b/src/pages/Home/selectors.js @@ -24,6 +24,12 @@ const makeSelectLabel = () => const makeSelectProducts = () => createSelector(selectHomePageDomain, (substate) => substate.products); +const makeSelectProduct = () => + createSelector(selectHomePageDomain, (substate) => substate.product); + +const makeSelectBarcode = () => + createSelector(selectHomePageDomain, (substate) => substate.barocde); + export { selectHomePageDomain, makeSelectMealId, @@ -33,4 +39,7 @@ export { makeSelectError, makeSelectIsLoading, makeSelectMeals, + makeSelectProduct, + makeSelectBarcode, + }; diff --git a/src/utils/api.js b/src/utils/api.js index dcb653b..b4624bf 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -2,6 +2,7 @@ const API_BASE_URL = 'http://localhost:3001/v1' const AUTH = 'auth'; const PROFILE = 'profiles'; const MEALS = 'meals'; +const PRODUCTS = 'products' const urls = { auth: { @@ -11,6 +12,7 @@ const urls = { }, profile: `${API_BASE_URL}/${PROFILE}`, meals: `${API_BASE_URL}/${MEALS}`, + products: `${API_BASE_URL}/${PRODUCTS}`, } export default urls