123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- import React, { useEffect, useState } from 'react';
- import { makeStyles } from "@material-ui/styles";
- import { alpha, Box, Button, Collapse, Divider, List, ListItem, ListItemAvatar, ListItemIcon, ListItemText, Typography } from "@material-ui/core";
- import { ExpandLess, ExpandMore, UnfoldLess, UnfoldMore } from "@material-ui/icons";
- import { useTranslation } from "react-i18next";
- import { fetchAndUpdateMealsFromUser } from "./meals.util";
- import useCategoryIcons from "./useCategoryIcons";
- import { bool, string } from "prop-types";
- import MealAvatar from "./MealAvatar";
- import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
- import SelectMealTags from "./SelectMealTags";
- import { useLocation, useNavigate } from "react-router-dom";
- const useStyles = makeStyles(theme => ({
- infoText: {
- textAlign: "center",
- margin: "3rem 2rem",
- fontFamily: "Neucha",
- fontSize: "1.3rem",
- lineHeight: "1.4rem",
- },
- category: props => ({
- backgroundColor: alpha(props.own ? theme.palette.primary.main : theme.palette.secondary.main, 0.1),
- '&:hover': {
- backgroundColor: alpha(props.own ? theme.palette.primary.main : theme.palette.secondary.main, 0.2),
- }
- }),
- listItemIcon: {
- fontSize: "1rem",
- color: theme.palette.text.primary,
- minWidth: '2rem',
- },
- nestedListItem: {
- paddingLeft: theme.spacing(4),
- },
- controlBox: {
- height: '50px',
- padding: '5px',
- },
- filterBox: {
- padding: '10px',
- marginTop: '-15px',
- },
- filterTags: {
- borderRadius: 0,
- marginTop: 0,
- },
- optionRowButton: {
- margin: '5px 0.5rem 5px',
- padding: '0 5px',
- }
- }));
- /** Content of page that displays all meals of a given use and opens their detail views on click.
- * If meals belong to logged in user, editing will be allowed.
- * todo: keep filters if meal is visualized
- * todo: add search in addition to filters
- * todo: restructure to have logic in a wrapper component, so that refresh can be triggered
- * todo: Snackbar visualization */
- const Meals = (props) => {
- const classes = useStyles(props);
- const { t } = useTranslation();
- let navigate = useNavigate();
- const { pathname } = useLocation();
- const { own, userId } = props;
- const [, updateState] = useState();
- const forceUpdate = React.useCallback(() => updateState({}), []);
- const [isFilterOpen, setIsFilterOpen] = useState(false);
- const [filterTags, setFilterTags] = useState([]);
- const [meals, setMeals] = useState([]);
- const [filteredMeals, setFilteredMeals] = useState([]);
- const [mealsByCategory,] = useState(new Map());
- const [isCategoryOpen, setIsCategoryOpen] = useState({});
- const [allCategoriesClosed, setAllCategoriesClosed] = useState(false);
- const [categoryIcons, fireIconReload] = useCategoryIcons(userId);
- const [emptyListFound, setEmptyListFound] = useState(false);
- const updateMealsCallback = (mealsFound) => {
- setMeals(mealsFound);
- setFilteredMeals(mealsFound);
- if (mealsFound.length === 0) {
- setEmptyListFound(true);
- }
- }
- const sortMealsIntoCategories = () => {
- mealsByCategory.clear();
- const mealsWithoutCategory = [];
- const categoriesInitiallyExpanded = true;
- filteredMeals.sort(function (a, b) {
- if (!b.category) return 1;
- if (!a.category) return -1;
- return a.category > b.category ? 1 : -1;
- });
- filteredMeals.forEach(meal => {
- if (meal.category) {
- const key = meal.category;
- let mappedMeals = mealsByCategory.get(key);
- if (!mappedMeals) {
- mealsByCategory.set(key, [meal]);
- updateIsCategoryOpen(key, categoriesInitiallyExpanded);
- } else {
- mappedMeals.push(meal);
- }
- } else {
- mealsWithoutCategory.push(meal);
- }
- });
- if (mealsWithoutCategory.length > 0) {
- const key = t('Meals without category');
- mealsByCategory.set(key, mealsWithoutCategory);
- updateIsCategoryOpen(key, categoriesInitiallyExpanded);
- }
- // I don't remember why this was necessary but now I don't dare to remove it
- forceUpdate();
- }
- const updateIsCategoryOpen = (key, value) => {
- setIsCategoryOpen(prevState => {return { ...prevState, [key]: value }});
- }
- const fetchAndUpdateMeals = () => {
- fetchAndUpdateMealsFromUser(userId, updateMealsCallback);
- fireIconReload();
- }
- useEffect(() => {
- fetchAndUpdateMeals();
- // eslint-disable-next-line
- }, [userId, pathname]);
- useEffect(() => {
- sortMealsIntoCategories();
- // eslint-disable-next-line
- }, [filteredMeals]);
- useEffect(() => {
- if (filterTags.length > 0) {
- const newFilteredMeals = meals.filter(meal => {
- if (!meal.tags || meal.tags.length === 0) return false;
- return filterTags.every(tag => meal.tags.includes(tag));
- });
- setFilteredMeals(newFilteredMeals);
- } else {
- setFilteredMeals(meals);
- }
- // eslint-disable-next-line
- }, [filterTags]
- );
- const openMealDetailView = (meal) => {
- if(own) navigate('detail/' + meal._id, {state: {meal}});
- else navigate('/meals/detail/' + meal._id, {state: {meal}});
- };
- const getListItems = () => {
- const listItems = [];
- mealsByCategory.forEach((meals, categoryName) => {
- const listItemsForCategory = [];
- meals.forEach(meal => {
- listItemsForCategory.push(
- <ListItem key={meal._id} className={classes.nestedListItem} button onClick={() => {openMealDetailView(meal); }}>
- <ListItemAvatar>
- <MealAvatar meal={meal} />
- </ListItemAvatar>
- <ListItemText primary={meal.title} />
- </ListItem>,
- <Divider key={'Divider' + meal._id} />
- );
- });
- const open = isCategoryOpen[categoryName];
- const icon = categoryIcons[categoryName];
- listItems.push(
- <ListItem button key={categoryName} onClick={() => {
- updateIsCategoryOpen(categoryName, !isCategoryOpen[categoryName]);
- }} className={classes.category} >
- {icon && <ListItemIcon className={classes.listItemIcon}>
- <FontAwesomeIcon icon={icon} />
- </ListItemIcon>}
- <ListItemText primary={categoryName} />
- {open ? <ExpandLess /> : <ExpandMore />}
- </ListItem>,
- <Collapse key={categoryName + 'MealList'} in={open} timeout="auto" unmountOnExit>
- <List component="div" disablePadding>
- {listItemsForCategory}
- </List>
- </Collapse>);
- });
- return listItems;
- }
- const updateFilterTags = (newTags) => {
- setFilterTags(newTags);
- }
- const toggleAllCategories = () => {
- let allClosed = Object.values(isCategoryOpen).every(isOpen => !isOpen);
- Object.keys(isCategoryOpen).forEach(category => {
- updateIsCategoryOpen(category, allClosed);
- });
- }
- useEffect(() => {
- setAllCategoriesClosed(Object.values(isCategoryOpen).every(isOpen => !isOpen));
- }, [isCategoryOpen]);
- let infoText = t("Looks like there are no meals here yet.");
- if(own) infoText += ' ' + t('Add one by clicking in the top right corner.');
- return (
- <>
- {meals.length === 0 ?
- <Typography className={classes.infoText}>{emptyListFound ? infoText : t('Loading') + '...'} </Typography> :
- <>
- <Box className={classes.controlBox} style={{ display: 'flex', justifyContent: own ? 'space-between' : 'end' }}>
- <Button variant="text"
- className={classes.optionRowButton}
- color="secondary"
- onClick={() => {setIsFilterOpen(prevState => !prevState)}}
- endIcon={isFilterOpen ? <ExpandLess /> : <ExpandMore />}>
- {t('Filter')}
- </Button>
- <Button variant="text"
- className={classes.optionRowButton}
- style={{ marginLeft: 'auto' }}
- color="primary"
- endIcon={allCategoriesClosed ? <UnfoldMore /> : <UnfoldLess />}
- onClick={toggleAllCategories}>
- {allCategoriesClosed ? t('expand all') : t('collapse all')}
- </Button>
- </Box>
- {isFilterOpen && <Box className={classes.filterBox}>
- <SelectMealTags currentTags={filterTags}
- own={own}
- otherUserId={userId}
- updateTags={updateFilterTags}
- placeholderText={t('Filter by Tags')}
- className={classes.filterTags}
- customControlStyles={{ borderRadius: 0 }} />
- </Box>}
- {filteredMeals.length === 0 ?
- <Typography className={classes.infoText}>{t('No meals found for filter selection')}</Typography> :
- <List component="nav" className={classes.root} aria-label="meal list" disablePadding>
- {getListItems()}
- </List>}
- </>
- }
- </>
- );
- }
- Meals.propTypes = {
- /** userId of user whose meals are to be displayed */
- userId: string.isRequired,
- /** are these the user's own meals or is another user watching foreign meals? In the latter case editing will be prohibited. */
- own: bool.isRequired,
- }
- export default Meals;
|