From 12c7a971476fa17d1ccffe176f8eee75abbb59bc Mon Sep 17 00:00:00 2001 From: StijnKing Date: Mon, 1 Apr 2024 21:06:37 +0200 Subject: [PATCH] Add a drawer with filters for mobile use On the exercise overview page, the filters are now placed in a drawer. --- public/locales/en/translation.json | 1 + src/components/Exercises/ExerciseOverview.tsx | 202 ++++++++++-------- .../Exercises/Filter/CategoryFilter.tsx | 111 ++++++---- .../Exercises/Filter/EquipmentFilter.tsx | 110 ++++++---- .../Filter/ExerciseFiltersContext.tsx | 16 ++ .../Exercises/Filter/FilterDrawer.tsx | 38 ++++ .../Exercises/Filter/MuscleFilter.tsx | 142 +++++++----- 7 files changed, 403 insertions(+), 217 deletions(-) create mode 100644 src/components/Exercises/Filter/ExerciseFiltersContext.tsx create mode 100644 src/components/Exercises/Filter/FilterDrawer.tsx diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index f83688f2..7fd15e17 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -32,6 +32,7 @@ "unit": "Unit", "alsoSearchEnglish": "Also search for names in English", "copyToClipboard": "Copy to clipboard", + "filter": "Filter", "exercises": { "replacements": "Replacements", "replacementsInfoText": "Optionally, you can also select an exercise that should replace this one (e.g. because it was submitted twice, or similar). This will replace the exercise in routines as well as training logs, instead of just deleting it. These changes will also propagate to any instance that syncs the exercises from this one.", diff --git a/src/components/Exercises/ExerciseOverview.tsx b/src/components/Exercises/ExerciseOverview.tsx index a68fbcac..0bfb2758 100644 --- a/src/components/Exercises/ExerciseOverview.tsx +++ b/src/components/Exercises/ExerciseOverview.tsx @@ -1,26 +1,22 @@ import AddIcon from '@mui/icons-material/Add'; -import { Box, Button, Container, Grid, Pagination, Stack, Typography, } from "@mui/material"; -import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget"; -import { CategoryFilter } from "components/Exercises/Filter/CategoryFilter"; -import { EquipmentFilter } from "components/Exercises/Filter/EquipmentFilter"; -import { MuscleFilter } from "components/Exercises/Filter/MuscleFilter"; +import { Box, Button, Container, Grid, Pagination, Stack, Typography, useMediaQuery } from "@mui/material"; +import { CategoryFilter, CategoryFilterDropdown } from "components/Exercises/Filter/CategoryFilter"; +import { EquipmentFilter, EquipmentFilterDropdown } from "components/Exercises/Filter/EquipmentFilter"; +import { MuscleFilter, MuscleFilterDropdown } from "components/Exercises/Filter/MuscleFilter"; import { NameAutocompleter } from "components/Exercises/Filter/NameAutcompleter"; import { Category } from "components/Exercises/models/category"; import { Equipment } from "components/Exercises/models/equipment"; import { Muscle } from "components/Exercises/models/muscle"; import { ExerciseGrid } from "components/Exercises/Overview/ExerciseGrid"; import { ExerciseGridSkeleton } from "components/Exercises/Overview/ExerciseGridLoadingSkeleton"; -import { - useCategoriesQuery, - useEquipmentQuery, - useExercisesQuery, - useMusclesQuery -} from "components/Exercises/queries"; -import React from "react"; +import { useExercisesQuery } from "components/Exercises/queries"; +import React, { useContext, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; import { ExerciseSearchResponse } from "services/responseType"; import { makeLink, WgerLink } from "utils/url"; +import { FilterDrawer } from './Filter/FilterDrawer'; +import { ExerciseFiltersContext } from './Filter/ExerciseFiltersContext'; const ContributeExerciseBanner = () => { const [t, i18n] = useTranslation(); @@ -74,17 +70,12 @@ const NoResultsBanner = () => { ); }; -export const ExerciseOverview = () => { +export const ExerciseOverviewList = () => { const basesQuery = useExercisesQuery(); - const categoryQuery = useCategoriesQuery(); - const musclesQuery = useMusclesQuery(); - const equipmentQuery = useEquipmentQuery(); const [t, i18n] = useTranslation(); const navigate = useNavigate(); - - const [selectedEquipment, setSelectedEquipment] = React.useState([]); - const [selectedMuscles, setSelectedMuscles] = React.useState([]); - const [selectedCategories, setSelectedCategories] = React.useState([]); + const { selectedCategories, selectedEquipment, selectedMuscles} = useContext(ExerciseFiltersContext); + const isMobile = useMediaQuery('(max-width:600px)'); const [page, setPage] = React.useState(1); const handlePageChange = (event: any, value: number) => { @@ -96,40 +87,44 @@ export const ExerciseOverview = () => { }); }; + let filteredExercises = useMemo(() => { + let filteredExercises = basesQuery.data || []; + + // Filter exercise bases by categories + if (selectedCategories.length > 0) { + filteredExercises = filteredExercises!.filter(exercise => { + return selectedCategories.some( + category => exercise.category.id === category.id + ); + }); + } + + // Filter exercises that have one of the selected equipment + if (selectedEquipment.length > 0) { + filteredExercises = filteredExercises!.filter(exercise => { + return exercise.equipment.some(equipment => + selectedEquipment.some( + selectedEquipment => selectedEquipment.id === equipment.id + ) + ); + }); + } + + // Filter exercises that have one of the selected muscles + if (selectedMuscles.length > 0) { + filteredExercises = filteredExercises!.filter(exercise => { + return exercise.muscles.some(muscle => + selectedMuscles.some(selectedMuscle => selectedMuscle.id === muscle.id) + ); + }); + } + + return filteredExercises; + }, [basesQuery.data, selectedCategories, selectedEquipment, selectedMuscles]); + // Should be a multiple of three, since there are three columns in the grid const ITEMS_PER_PAGE = 21; - let filteredExercises = basesQuery.data || []; - - // Filter exercise bases by categories - if (selectedCategories.length > 0) { - filteredExercises = filteredExercises!.filter(exercise => { - return selectedCategories.some( - category => exercise.category.id === category.id - ); - }); - } - - // Filter exercises that have one of the selected equipment - if (selectedEquipment.length > 0) { - filteredExercises = filteredExercises!.filter(exercise => { - return exercise.equipment.some(equipment => - selectedEquipment.some( - selectedEquipment => selectedEquipment.id === equipment.id - ) - ); - }); - } - - // Filter exercises that have one of the selected muscles - if (selectedMuscles.length > 0) { - filteredExercises = filteredExercises!.filter(exercise => { - return exercise.muscles.some(muscle => - selectedMuscles.some(selectedMuscle => selectedMuscle.id === muscle.id) - ); - }); - } - // Pagination calculations const pageCount = Math.ceil(filteredExercises!.length / ITEMS_PER_PAGE); const paginatedExercises = filteredExercises!.slice( @@ -144,57 +139,67 @@ export const ExerciseOverview = () => { return ( - + {t("exercises.exercises")} - - - - - - - - - - {categoryQuery.isLoading ? : ( + {isMobile ? ( + <> + + + + + + + + + + + + + + + ) : ( + <> + + + + + + + + )} + + {!isMobile && ( + + - + - )} - {equipmentQuery.isLoading ? : ( - + - )} - {musclesQuery.isLoading ? : ( - + - )} + - + )} + {basesQuery.isLoading ? @@ -217,3 +222,22 @@ export const ExerciseOverview = () => { ); }; + +export const ExerciseOverview = () => { + const [selectedEquipment, setSelectedEquipment] = useState([]); + const [selectedMuscles, setSelectedMuscles] = useState([]); + const [selectedCategories, setSelectedCategories] = React.useState([]); + + return ( + + + + ); +}; diff --git a/src/components/Exercises/Filter/CategoryFilter.tsx b/src/components/Exercises/Filter/CategoryFilter.tsx index fc4dc6ab..d7d865a1 100644 --- a/src/components/Exercises/Filter/CategoryFilter.tsx +++ b/src/components/Exercises/Filter/CategoryFilter.tsx @@ -1,18 +1,29 @@ -import React from 'react'; -import { List, ListItem, ListItemButton, ListItemIcon, ListItemText, Paper, Switch, Typography } from "@mui/material"; +import React, { useContext } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Paper, + Switch, + Typography +} from "@mui/material"; import { useTranslation } from "react-i18next"; import { Category } from "components/Exercises/models/category"; import { getTranslationKey } from "utils/strings"; +import { useCategoriesQuery } from '../queries'; +import { ExerciseFiltersContext } from './ExerciseFiltersContext'; +import { LoadingPlaceholder } from '../../Core/LoadingWidget/LoadingWidget'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +const CategoryFilterList = () => { -type CategoryFilterProps = { - categories: Category[]; - selectedCategories: Category[]; - setSelectedCategories: (categories: Category[]) => void; -} - -export const CategoryFilter = ({ categories, selectedCategories, setSelectedCategories }: CategoryFilterProps) => { - + const { data: categories, isLoading } = useCategoriesQuery(); + const { selectedCategories, setSelectedCategories } = useContext(ExerciseFiltersContext); const [t] = useTranslation(); const handleToggle = (value: Category) => () => { @@ -28,39 +39,65 @@ export const CategoryFilter = ({ categories, selectedCategories, setSelectedCate setSelectedCategories(newChecked); }; + if (isLoading) { + return ; + } + + return ( + + {categories!.map((category) => { + const labelId = `checkbox-list-label-${category.id}`; + + return ( + + + + + + + + + ); + })} + + ); +}; + +export const CategoryFilterDropdown = () => { + const [t] = useTranslation(); + + return ( + + }> + {t('category')} + + + + + + ); +}; + +export const CategoryFilter = () => { + const [t] = useTranslation(); + return (
{t('category')} - - - {categories.map((category) => { - const labelId = `checkbox-list-label-${category.id}`; - - return ( - - - - - - - - - ); - })} - +
); diff --git a/src/components/Exercises/Filter/EquipmentFilter.tsx b/src/components/Exercises/Filter/EquipmentFilter.tsx index feff9bbb..073f871f 100644 --- a/src/components/Exercises/Filter/EquipmentFilter.tsx +++ b/src/components/Exercises/Filter/EquipmentFilter.tsx @@ -1,18 +1,29 @@ -import React from 'react'; -import { List, ListItem, ListItemButton, ListItemIcon, ListItemText, Paper, Switch, Typography } from "@mui/material"; +import React, { useContext } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Paper, + Switch, + Typography +} from "@mui/material"; import { useTranslation } from "react-i18next"; import { Equipment } from "components/Exercises/models/equipment"; import { getTranslationKey } from "utils/strings"; +import { useEquipmentQuery } from '../queries'; +import { ExerciseFiltersContext } from './ExerciseFiltersContext'; +import { LoadingPlaceholder } from '../../Core/LoadingWidget/LoadingWidget'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +const EquipmentFilterList = () => { -type EquipmentFilterProps = { - equipment: Equipment[]; - selectedEquipment: Equipment[]; - setSelectedEquipment: (equipment: Equipment[]) => void; -} - -export const EquipmentFilter = ({ equipment, selectedEquipment, setSelectedEquipment }: EquipmentFilterProps) => { - + const { data: equipment, isLoading } = useEquipmentQuery(); + const { selectedEquipment, setSelectedEquipment } = useContext(ExerciseFiltersContext); const [t] = useTranslation(); const handleToggle = (value: Equipment) => () => { @@ -28,38 +39,65 @@ export const EquipmentFilter = ({ equipment, selectedEquipment, setSelectedEquip setSelectedEquipment(newChecked); }; + if (isLoading) { + return ; + } + + return ( + + {equipment!.map((equipment) => { + const labelId = `checkbox-list-label-${equipment.id}`; + + return ( + + + + + + + + + ); + })} + + ); +}; + +export const EquipmentFilterDropdown = () => { + const [t] = useTranslation(); + + return ( + + }> + {t('exercises.equipment')} + + + + + + ); +}; + +export const EquipmentFilter = () => { + const [t] = useTranslation(); + return (
{t('exercises.equipment')} - - {equipment.map((equipment) => { - const labelId = `checkbox-list-label-${equipment.id}`; - - return ( - - - - - - - - - ); - })} - +
); diff --git a/src/components/Exercises/Filter/ExerciseFiltersContext.tsx b/src/components/Exercises/Filter/ExerciseFiltersContext.tsx new file mode 100644 index 00000000..47d8ba51 --- /dev/null +++ b/src/components/Exercises/Filter/ExerciseFiltersContext.tsx @@ -0,0 +1,16 @@ +import { createContext } from 'react'; +import { Equipment } from '../models/equipment'; +import { Muscle } from '../models/muscle'; +import { Category } from '../models/category'; + + +type ExerciseContext = { + selectedEquipment: Equipment[]; + setSelectedEquipment: (equipment: Equipment[]) => void; + selectedMuscles: Muscle[]; + setSelectedMuscles: (muscles: Muscle[]) => void; + selectedCategories: Category[]; + setSelectedCategories: (exercises: Category[]) => void; +} + +export const ExerciseFiltersContext = createContext({} as unknown as ExerciseContext); diff --git a/src/components/Exercises/Filter/FilterDrawer.tsx b/src/components/Exercises/Filter/FilterDrawer.tsx new file mode 100644 index 00000000..70faa24e --- /dev/null +++ b/src/components/Exercises/Filter/FilterDrawer.tsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; +import { Button, Divider, Drawer, Stack, Typography } from '@mui/material'; +import FilterAltIcon from '@mui/icons-material/FilterAlt'; +import CloseIcon from '@mui/icons-material/Close'; +import { useTranslation } from 'react-i18next'; + +type FilterDrawerProps = { + children: React.ReactNode; +} + +export const FilterDrawer = ({ children }: FilterDrawerProps) => { + const [t] = useTranslation(); + const [open, setOpen] = useState(false); + + const toggleDrawer = (newOpen: boolean) => () => { + setOpen(newOpen); + }; + + return ( + <> + + + + + {t('filter')} + + + + + {children} + + + ); +}; diff --git a/src/components/Exercises/Filter/MuscleFilter.tsx b/src/components/Exercises/Filter/MuscleFilter.tsx index 7c565e8f..75a16d8c 100644 --- a/src/components/Exercises/Filter/MuscleFilter.tsx +++ b/src/components/Exercises/Filter/MuscleFilter.tsx @@ -1,5 +1,8 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { + Accordion, + AccordionDetails, + AccordionSummary, IconButton, List, ListItem, @@ -16,15 +19,15 @@ import { Muscle } from "components/Exercises/models/muscle"; import { getTranslationKey } from "utils/strings"; import { MuscleOverview } from "components/Muscles/MuscleOverview"; import { LightTooltip } from "components/Core/Tooltips/LightToolTip"; +import { useMusclesQuery } from '../queries'; +import { ExerciseFiltersContext } from './ExerciseFiltersContext'; +import { LoadingPlaceholder } from '../../Core/LoadingWidget/LoadingWidget'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -type MuscleFilterProps = { - muscles: Muscle[]; - selectedMuscles: Muscle[]; - setSelectedMuscles: (muscles: Muscle[]) => void; -} - -export const MuscleFilter = ({ muscles, selectedMuscles, setSelectedMuscles }: MuscleFilterProps) => { +const MuscleFilterList = () => { + const { data: muscles, isLoading } = useMusclesQuery(); + const { selectedMuscles, setSelectedMuscles } = useContext(ExerciseFiltersContext); const [t] = useTranslation(); const handleToggle = (value: Muscle) => () => { @@ -40,59 +43,88 @@ export const MuscleFilter = ({ muscles, selectedMuscles, setSelectedMuscles }: M setSelectedMuscles(newChecked); }; + if (isLoading) { + return ; + } + + return ( + + {muscles!.map((m) => { + const labelId = `checkbox-list-label-${m.id}`; + + return ( + + } + placement="right" + arrow> + + + + + } + > + + + + + + + + + + ); + })} + + ); +}; + +export const MuscleFilterDropdown = () => { + + const [t] = useTranslation(); + + return ( + + }> + {t('exercises.muscles')} + + + + + + ); +}; + +export const MuscleFilter = () => { + + const [t] = useTranslation(); + return (
{t('exercises.muscles')} - - {muscles.map((m) => { - const labelId = `checkbox-list-label-${m.id}`; - - return ( - - } - placement="right" - arrow> - - - - - } - > - - - - - - - - - - ); - })} - +
);