From 7a20d2e1e49043f65f7c850c6c7314ad9efe1697 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. --- src/components/Exercises/ExerciseOverview.tsx | 202 ++++++++++-------- .../Exercises/Filter/CategoryFilter.tsx | 111 ++++++---- .../Exercises/Filter/EquipmentFilter.tsx | 110 ++++++---- .../Filter/ExerciseFiltersContext.tsx | 16 ++ .../Exercises/Filter/FilterDrawer.tsx | 36 ++++ .../Exercises/Filter/MuscleFilter.tsx | 142 +++++++----- 6 files changed, 400 insertions(+), 217 deletions(-) create mode 100644 src/components/Exercises/Filter/ExerciseFiltersContext.tsx create mode 100644 src/components/Exercises/Filter/FilterDrawer.tsx 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..6a16326d --- /dev/null +++ b/src/components/Exercises/Filter/FilterDrawer.tsx @@ -0,0 +1,36 @@ +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'; + +type FilterDrawerProps = { + children: React.ReactNode; +} + +export const FilterDrawer = ({ children }: FilterDrawerProps) => { + const [open, setOpen] = useState(false); + + const toggleDrawer = (newOpen: boolean) => () => { + setOpen(newOpen); + }; + + return ( + <> + + + + + Filters + + + + + {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> - - - - - } - > - - - - - - - - - - ); - })} - +
);