Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Nutrition Game #70 #114

Draft
wants to merge 33 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c7043b9
bug: shortCut issue solved
Shmigolk Jul 27, 2022
9c92737
UI: add settings icon and moved links in NavBar to the right
Shmigolk Jul 27, 2022
d0149e2
UI: align pictures and add gap
Shmigolk Jul 27, 2022
4529d8b
UI: add settings icon and moved links in NavBar to the right
Shmigolk Jul 27, 2022
0df05c0
UI: align pictures and add gap
Shmigolk Jul 28, 2022
c89e1f2
UI: create table component
Shmigolk Jul 29, 2022
3a5bd0d
UI: add media queueueueueueury
Shmigolk Jul 29, 2022
9412b2c
fix: add controlled input
Shmigolk Jul 31, 2022
d36a119
fix: add controlled input
Shmigolk Jul 31, 2022
ce50230
Merge remote-tracking branch 'upstream/master' into nutrition
Shmigolk Aug 1, 2022
93d4b4e
feature: change the code of additional nutr
Shmigolk Aug 1, 2022
fe21b02
feature: implement adding nutriments to the table
Shmigolk Aug 1, 2022
a674dfd
feature: implement adding nutriments to the table and removing them f…
Shmigolk Aug 1, 2022
630f0ef
feature: add delete icons and implement deleting nutriments from the …
Shmigolk Aug 1, 2022
257bd35
feature: add Title and the description of the product (hardcoded)
Shmigolk Aug 1, 2022
cb1e486
feature: get data from API. Display picture
Shmigolk Aug 2, 2022
ecd4f07
feature: make clickHandler shorter
Shmigolk Aug 2, 2022
fb90329
feature: move nutriments arraus out of the component. Refresh the tab…
Shmigolk Aug 3, 2022
b952bc4
feature: replace useEffect with React.useEffect
Shmigolk Aug 3, 2022
7d36892
feature: restructure Table component. Make TableRowComp
Shmigolk Aug 9, 2022
b179515
feature: now delete icon toggle display property
Shmigolk Aug 9, 2022
cb6e2c2
feature: now delete icon toggle display property
Shmigolk Aug 9, 2022
089d9df
Merge remote-tracking branch 'origin/master' into nutrition
alexfauquette Aug 9, 2022
79d4643
remove unused import
alexfauquette Aug 9, 2022
9b873e3
feature: now i18n works into the table
Shmigolk Aug 11, 2022
10f5d56
Merge branch 'master' into nutrition
Shmigolk Aug 11, 2022
0add0f0
feature: add keys to common json and en json
Shmigolk Aug 11, 2022
93e7719
Merge remote-tracking branch 'origin/nutrition' into nutrition
Shmigolk Aug 11, 2022
79764f1
feature: display correct properties in select elements
Shmigolk Aug 12, 2022
ae0bffe
feature: on change is working
Shmigolk Aug 12, 2022
30be061
UI: fixed width problem
Shmigolk Aug 13, 2022
7214c83
UI: change the order of elements of productCard and add temporary bor…
Shmigolk Aug 13, 2022
d19a758
bug: fix onChange handler bug
Shmigolk Aug 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
"dependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@material-ui/system": "^4.12.2",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it useful to have this package? I see that it is kind of used in pages/nutrition/index.jsx but seems to be importing a non used flexbox (what would be the advantage of using that flexbox compared to css defined flexbox?)

"@mui/icons-material": "^5.8.4",
"@mui/lab": "^5.0.0-alpha.89",
"@mui/material": "^5.8.7",
"@mui/x-data-grid": "^5.13.0",
"axios": "^0.27.2",
"clsx": "^1.2.1",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this package used?

"i18next": "^21.8.13",
"lodash.isequal": "^4.5.0",
"react": "^18.2.0",
Expand All @@ -19,6 +21,7 @@
"react-medium-image-zoom": "^4.4.3",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"react-virtualized": "^9.22.3",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this being used somewhere?

"web-vitals": "^2.1.4"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
NotFoundPage,
NutriscoreValidator,
Home,
Nutrition,
} from "./pages";
import ResponsiveAppBar from "./components/ResponsiveAppBar";
import DevModeContext from "./contexts/devMode";
Expand Down Expand Up @@ -62,6 +63,7 @@ export default function App() {
<Route path="/settings" element={<SettingsPage />} />
<Route path="/questions" element={<QuestionsPage />} />
<Route path="/insights" element={<InsightsPage />} />
<Route path="/nutrition" element={<Nutrition />} />
<Route path="*" element={<NotFoundPage />} />

<Route path="/nutriscore" element={<NutriscoreValidator />} />
Expand Down
11 changes: 4 additions & 7 deletions src/components/ResponsiveAppBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const pages = [
{ url: "eco-score", translationKey: "menu.eco-score" },
{ translationKey: "menu.manage" },
{ url: "insights", translationKey: "menu.insights" },
// { url: "settings", translationKey: "menu.settings" },
alexfauquette marked this conversation as resolved.
Show resolved Hide resolved
/*{ url: "nutrition", translationKey: "menu.nutrition" },*/
];

const ResponsiveAppBar = () => {
Expand Down Expand Up @@ -156,12 +156,9 @@ const ResponsiveAppBar = () => {
component="a"
href="/"
sx={{
mr: 2,
fontFamily: "monospace",
fontWeight: 700,
letterSpacing: ".3rem",
color: "inherit",
textDecoration: "none",
display: { xs: "none", md: "flex" },
flexDirection: "row",
alignItems: "baseline",
}}
Shmigolk marked this conversation as resolved.
Show resolved Hide resolved
>
Hunger Games
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"insights": "Insights",
"nutritions": "Nutrition facts",
"settings": "Settings",
"eco-score": "Eco-score"
"eco-score": "Eco-score",
"nutrition": "nutrition"
Shmigolk marked this conversation as resolved.
Show resolved Hide resolved
},
"insights": {
"insights": "Insights",
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"insights": "Insights",
"nutritions": "Nutrition facts",
"settings": "Settings",
"eco-score": "Eco-score"
"eco-score": "Eco-score",
"nutrition": "nutrition"
Shmigolk marked this conversation as resolved.
Show resolved Hide resolved
},
"insights": {
"insights": "Insights",
Expand Down
1 change: 1 addition & 0 deletions src/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { default as LogoUpdatePage } from "./logos/LogoUpdate";

// experimental
export { default as NutriscoreValidator } from "./nutriscoreValidator";
export { default as Nutrition } from "./nutrition"
32 changes: 32 additions & 0 deletions src/pages/nutrition/additionalNutritions.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';

export default function AdditionalNutriments({options, setNutriments, setAdditionalNutriments}) {

const [inputValue, setInputValue] = React.useState('')

return (
<Autocomplete
value={null}
disablePortal
id="nutrition-input"
options={options}
sx={{ width: 245, marginLeft: 2, marginTop: 2 }}
onChange={(event) => {
const nutrIndex = event.target.dataset.optionIndex
if (nutrIndex) {
setNutriments(prev => [...prev, options[nutrIndex]])
setAdditionalNutriments(prev => prev.filter(elem => elem !== options[nutrIndex]))
setInputValue("")
}
}}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
renderInput={(params) => <TextField {...params} label="Add nutriment" />}
/>
);
}

24 changes: 24 additions & 0 deletions src/pages/nutrition/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react";
import NutritionTable from "./table";
import ProductNutriments from "./productCard";
import { Box } from "@mui/material";
import { flexbox } from "@material-ui/system";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { flexbox } from "@material-ui/system";

imported but not used. Normally when you do yarn start in the console, you should see some warnings about lines where you import or define variables you do not use. You should remove them to get a cleaner code.


Here is what I get when I run yarn start from your branch

image


export default function Nutrition() {

return (
<Box display={"flex"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Box display={"flex"}
<Box display="flex"

Some points which will be useful for tech interview about React (JSX syntax to be precise):

  • By default props are provided as follow propName={propValue}
  • You do not need the {} for sting. FOr example propName={"some text"} is same as propName="some text"
  • A prop alone is equivalent to true. For example <Component isOpen /> is the same as <Component isOpen={true} />.

flexDirection={{ xs: "column", md: "row" }}
gap={2}
sx={{
width: 1,
height: 1,
alignItems: { xs: 'center', md:"flex-start" },
justifyContent: "center",
padding: 4
}}>
<ProductNutriments />
<NutritionTable />
</Box>
);
}
76 changes: 76 additions & 0 deletions src/pages/nutrition/productCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Zoom from "react-medium-image-zoom";
import Button from "@mui/material/Button";
import Typography from '@mui/material/Typography'
import offService from '../../off'
import { useEffect } from "react";
Shmigolk marked this conversation as resolved.
Show resolved Hide resolved

export default function ProductNutriments({}) {

const { getNutritionToFillUrl } = offService
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works, but writing offService.getNutritionToFillUrl() as an advantage. When reading the line of code we know this data is fetched from openfoodfact server. Otherwise it could be fetched from robotoff, or an other file

Suggested change
const { getNutritionToFillUrl } = offService


const [ page, setPage ] = React.useState(1)
const [ index, setIndex ] = React.useState(0)
const [ products, setProducts ] = React.useState([])

useEffect(() => {
console.log('fetched')
const productListUrl = getNutritionToFillUrl({page})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const productListUrl = getNutritionToFillUrl({page})
const productListUrl = offService.getNutritionToFillUrl({page})

fetch(productListUrl)
.then(res => res.json())
.then(data => setProducts(data.products))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe set the index to 0, or keep the previous products with setPoducts(prevProducts => [...prevProducts, ...data.products])

}, [page])

const pictureURL = products[index] ? products[index].image_nutrition_url : "https://static.openfoodfacts.org/images/image-placeholder.png"

function clickHandler(){
setIndex(prev => prev < 22 ? ++prev : 0)
}

return (

<Box flexGrow={1}
flexShrink={1}
display={'flex'}
flexDirection={'column'}
sx={{
maxWidth: '380px'
}}>
<Typography variant="h5" component="h5" sx={{alignSelf: 'center'}}>
PRODUCT.BRAND
</Typography>
<Typography variant="p" component="p" sx={{alignSelf: 'center'}}>
PRODUCT DESCRIPTION IF NEEDED
</Typography>
<Zoom wrapStyle={{ height: "100%" }}>
<img
// TODO: use getFullSizeImage when the zoom is activated
// src={getFullSizeImage(question.source_image_url)}
src={pictureURL}
alt=""
style={{ maxWidth: "100%", maxHeight: "100%" }}
/>
</Zoom>
<Box display={"flex"} flexDirection={"row"} Gap={'.5em'}>
<Button
onClick={clickHandler}
color="secondary"
variant="contained"
size="large"
sx={{ display: "flex", flexDirection: "column", flexGrow: 1, width: '47%' }}
>
SKIP
</Button>
<Button
onClick={clickHandler}
color="success"
variant="contained"
size="large"
sx={{ display: "flex", flexDirection: "column", flexGrow: 1, width: '47%' }}
>
VALIDATE
</Button>
</Box>
</Box>)
}
149 changes: 149 additions & 0 deletions src/pages/nutrition/table.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import * as React from "react";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import TextField from "@mui/material/TextField";
import SelectAutoWidth from "./unitSelect";
import { Box } from "@mui/material";
import Checkbox from "@mui/material/Checkbox";
import AdditionalNutriments from "./additionalNutritions";
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';

function createData(
label, property, unit
) {
return { label, property, unit };
}

export default function NutritionTable() {

const [nutriments, setNutriments] = React.useState([
{
off_nutriment_id: "energy_kj",
label: "Energie (kJ)",
value: "",
unit: "null",
quantification: "=",
robotoffPrediction: null
},
{
off_nutriment_id: "energy_kcal",
label: "Protein",
value: "",
unit: "null",
quantification: "<",
robotoffPrediction: null
},
{
off_nutriment_id: "energy_kcal",
label: "Shugar",
value: "",
unit: "null",
quantification: "<",
robotoffPrediction: null
},
{
off_nutriment_id: "energy_kcal",
label: "Fat",
value: "",
unit: null,
quantification: "<",
robotoffPrediction: null
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this values could live outside of the component .... const NUTRIMENTS = [ ... ] and just used in the useState const [nutriments, setNutriments] = React.useState(NUTRIMENTS). That way the component is a bit more clean

])

const [additionalNutriments, setAdditionalNutriments] = React.useState([
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing here

off_nutriment_id: "energy_kj",
label: "Energie (kJ)",
value: "",
unit: "null",
quantification: "=",
robotoffPrediction: null
},
{
off_nutriment_id: "energy_kcal",
label: "Protein",
value: "",
unit: "null",
quantification: "<",
robotoffPrediction: null
}
])

function onchangeHandler(e) {
const {value, name} = e.target
setNutriments(prevState => prevState.map(
nutr => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I prefer not to use abbreviations as they quickly become difficult to read and understand, specially for other developers. Plase use a whole word, more typing but better readability. In this cases I tend to use the suffix ..item, something like nutrimentItem that way I understand that it is the thing inside the loop

return name === nutr.label? {...nutr, value} : nutr
}
))
}

function deleteItem(nutrition) {
setNutriments(prev => prev.filter(elem => elem !== nutrition))
setAdditionalNutriments(prev => [...prev, nutrition])
}

const rows = nutriments.map(nutrition => {
return (
createData(<Box sx={{
display: "flex",
alignItems: "center",
flexDirection: "row"
}}><TextField id="outlined-basic"
type={'number'}
label={nutrition.label}
variant="outlined"
sx={{ width: "10rem" }}
value={nutrition.value}
name={nutrition.label}
onChange={onchangeHandler}
/> , <SelectAutoWidth /></Box>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd advice against this kind of implicit closing elements (</Box>) as the chance of forgetting or using it wrongly is relatively high

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plase see the coment below

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume it's a mistake. The </Box> is planned to be before the coma But that's not important.

@Shmigolk I think you get confused by the MUI demo, because it's cells only contain strings/numbers, so they use

<TableCell>{row.thValueToDisplay}</TabCell>

In our case, we want to render inputs which is a bit more complex.

I propose you the following strategy:
create a component NutritionTableRow as follow:

const NutritionTableRow = ({ nutrimentData, updateValue, updateUnit, updateQuantificator }) => {
	const onNutrimentValueChange = (event) => {updateValue(event.target.value)}
	const onUnitChange = () => {...}
	const onQuantificatorChange = () => {...}
	
	const {label, value, quantification, unit} = nutrimentData
	
	return <TableRow>
		<TableCell>
			<TextField select value={quantification} onChange={onQuantificatorChange}>
				<MenuItem value="=">=</MenuItem>
				<MenuItem value=">">></MenuItem>
				<MenuItem value="<"><</MenuItem>
			</TextField>
		</TableCell>
		<TableCell>
			<TextField label={label} value={value} onChange={onNutrimentValueChange} />
		</TableCell>
		//  Same for the unit
	<TableRow>
}

Then you will be able to simplify your table content as follow:

	nutriments.map(nutriment => <NutritionTableRow nutrimentData={nutriment}  updateValue={updateUnit(nutriment.off_nutriment_id)}/>)

The trickiest part being updateValue which is a function returning a function.

const updateValue = (nutrimentId) => (newValue) => {
	// We get the index of the nutriment we want to update
	const indexToModify = nutriments.findIndex(nutriment => nutriment.off_nutriment_id === nutrimentId)
	
	// early return if not element correspond to this id
	if(indexToModify < 0){ return }
	
	// We update the value to the nutriment
	const newNutriment = {...nutriments[indexToModify], value: newValue }
	setNutriments([...nutriments.slice(0, indexToModify), newNutriment, ...nutriments.slice(indexToModify+1)])
}

<DeleteOutlineIcon sx={{cursor: 'pointer', color: 'red'}} onClick={() => deleteItem(nutrition)}/>));
});

return (
<Box>
<TableContainer sx={{ margin: 0, maxWidth: "1000px", width: "340px" }}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell sx={{
maxWidth: "8em",
fontSize: "large",
fontWeight: "bold"
}}>nutrition.table.value</TableCell>

</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about using the map directly?, I mean, instead of first map to createData() and using that result

<TableRow
key={row.label}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell component="th"
scope="row"
sx={{ width: 10 }}
>
{row.label}
</TableCell>
<TableCell align="left"
sx={{ width: "1rem"}}>{row.property}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<AdditionalNutriments
options = {additionalNutriments}
setNutriments={setNutriments}
setAdditionalNutriments={setAdditionalNutriments}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are trying to keep up to date 2 different arrays which is error pron. As soon as you forget an edge case, the two arrays will not be sync and it's hard to debug

I propose to keep the setNutriments that could be renamed addNutrimentToTheTable
Replace the setAdditionalNutriments by missingNutriments which would be computed as follow:

const missingNutriments = offNutriments.filter(nutrimentKey => 
	data.every(element) => element.off_nutriment_id !== nutrimentKey) 
)

with offNutriments the list of all the nutrients available on openfoodfact

This would replace options and setAdditionalNutriments

/>
</Box>
);
}