-
-
Notifications
You must be signed in to change notification settings - Fork 39
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
base: master
Are you sure you want to change the base?
Changes from 17 commits
c7043b9
9c92737
d0149e2
4529d8b
0df05c0
c89e1f2
3a5bd0d
9412b2c
d36a119
ce50230
93d4b4e
fe21b02
a674dfd
630f0ef
257bd35
cb1e486
ecd4f07
fb90329
b952bc4
7d36892
b179515
cb6e2c2
089d9df
79d4643
9b873e3
10f5d56
0add0f0
93e7719
79764f1
ae0bffe
30be061
7214c83
d19a758
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,11 +6,13 @@ | |
"dependencies": { | ||
"@emotion/react": "^11.9.0", | ||
"@emotion/styled": "^11.8.1", | ||
"@material-ui/system": "^4.12.2", | ||
"@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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
|
@@ -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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this being used somewhere? |
||
"web-vitals": "^2.1.4" | ||
}, | ||
"devDependencies": { | ||
|
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" />} | ||
/> | ||
); | ||
} | ||
|
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"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
imported but not used. Normally when you do Here is what I get when I run |
||||||
|
||||||
export default function Nutrition() { | ||||||
|
||||||
return ( | ||||||
<Box display={"flex"} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Some points which will be useful for tech interview about React (JSX syntax to be precise):
|
||||||
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> | ||||||
); | ||||||
} |
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It works, but writing
Suggested change
|
||||||
|
||||||
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}) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
fetch(productListUrl) | ||||||
.then(res => res.json()) | ||||||
.then(data => setProducts(data.products)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe set the |
||||||
}, [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>) | ||||||
} |
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 | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this values could live outside of the component .... |
||
]) | ||
|
||
const [additionalNutriments, setAdditionalNutriments] = React.useState([ | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd advice against this kind of implicit closing elements ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. plase see the coment below There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume it's a mistake. The @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: 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 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) => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 const missingNutriments = offNutriments.filter(nutrimentKey =>
data.every(element) => element.off_nutriment_id !== nutrimentKey)
) with This would replace |
||
/> | ||
</Box> | ||
); | ||
} |
There was a problem hiding this comment.
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 usedflexbox
(what would be the advantage of using that flexbox compared to css defined flexbox?)