Skip to content

Commit

Permalink
Add a drawer with filters for mobile use
Browse files Browse the repository at this point in the history
On the exercise overview page, the filters are now placed
in a drawer.
  • Loading branch information
StijnKing committed Apr 1, 2024
1 parent 96f5c0c commit 7a20d2e
Show file tree
Hide file tree
Showing 6 changed files with 400 additions and 217 deletions.
202 changes: 113 additions & 89 deletions src/components/Exercises/ExerciseOverview.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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<Equipment[]>([]);
const [selectedMuscles, setSelectedMuscles] = React.useState<Muscle[]>([]);
const [selectedCategories, setSelectedCategories] = React.useState<Category[]>([]);
const { selectedCategories, selectedEquipment, selectedMuscles} = useContext(ExerciseFiltersContext);
const isMobile = useMediaQuery('(max-width:600px)');

const [page, setPage] = React.useState(1);
const handlePageChange = (event: any, value: number) => {
Expand All @@ -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(
Expand All @@ -144,57 +139,67 @@ export const ExerciseOverview = () => {
return (
<Container maxWidth="lg">
<Grid container spacing={2} mt={2}>
<Grid item xs={12} sm={6}>
<Grid item xs={10} sm={6}>
<Typography gutterBottom variant="h3" component="div">
{t("exercises.exercises")}
</Typography>
</Grid>
<Grid item xs={12} sm={3}>
<NameAutocompleter callback={exerciseAdded} />
</Grid>
<Grid item xs={12} sm={3}>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => navigate(makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language))}
>
{t('exercises.contributeExercise')}
</Button>
</Grid>

<Grid item xs={12} sm={3}>
<Grid container spacing={1}>
{categoryQuery.isLoading ? <LoadingPlaceholder /> : (
{isMobile ? (
<>
<Grid item xs={2} sm={6}>
<Button
variant="contained"
onClick={() => navigate(makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language))}
>
<AddIcon />
</Button>
</Grid>
<Grid item sm={6} flexGrow={1}>
<NameAutocompleter callback={exerciseAdded} />
</Grid>
<Grid item xs={2} sm={6} display="flex" justifyContent="center" alignItems="center">
<FilterDrawer>
<CategoryFilterDropdown />
<EquipmentFilterDropdown />
<MuscleFilterDropdown />
</FilterDrawer>
</Grid>
</>
) : (
<>
<Grid item xs={12} sm={3}>
<NameAutocompleter callback={exerciseAdded} />
</Grid>
<Grid item xs={12} sm={3}>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => navigate(makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language))}
>
{t('exercises.contributeExercise')}
</Button>
</Grid>
</>
)}

{!isMobile && (
<Grid item xs={12} sm={3}>
<Grid container spacing={1}>
<Grid item xs={6} sm={12}>
<CategoryFilter
categories={categoryQuery.data!}
selectedCategories={selectedCategories}
setSelectedCategories={setSelectedCategories}
/>
<CategoryFilter />
</Grid>
)}

{equipmentQuery.isLoading ? <LoadingPlaceholder /> : (
<Grid item xs={6} sm={12}>
<EquipmentFilter
equipment={equipmentQuery.data!}
selectedEquipment={selectedEquipment}
setSelectedEquipment={setSelectedEquipment}
/>
<EquipmentFilter />
</Grid>
)}

{musclesQuery.isLoading ? <LoadingPlaceholder /> : (
<Grid item xs={12}>
<MuscleFilter
muscles={musclesQuery.data!}
selectedMuscles={selectedMuscles}
setSelectedMuscles={setSelectedMuscles}
/>
<MuscleFilter />
</Grid>
)}
</Grid>
</Grid>
</Grid>
)}

<Grid item xs={12} sm={9}>
{basesQuery.isLoading
? <ExerciseGridSkeleton />
Expand All @@ -217,3 +222,22 @@ export const ExerciseOverview = () => {
</Container>
);
};

export const ExerciseOverview = () => {
const [selectedEquipment, setSelectedEquipment] = useState<Equipment[]>([]);
const [selectedMuscles, setSelectedMuscles] = useState<Muscle[]>([]);
const [selectedCategories, setSelectedCategories] = React.useState<Category[]>([]);

return (
<ExerciseFiltersContext.Provider value={{
selectedEquipment,
setSelectedEquipment,
selectedMuscles,
setSelectedMuscles,
selectedCategories,
setSelectedCategories
}}>
<ExerciseOverviewList />
</ExerciseFiltersContext.Provider>
);
};
111 changes: 74 additions & 37 deletions src/components/Exercises/Filter/CategoryFilter.tsx
Original file line number Diff line number Diff line change
@@ -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) => () => {
Expand All @@ -28,39 +39,65 @@ export const CategoryFilter = ({ categories, selectedCategories, setSelectedCate
setSelectedCategories(newChecked);
};

if (isLoading) {
return <LoadingPlaceholder />;
}

return (
<List>
{categories!.map((category) => {
const labelId = `checkbox-list-label-${category.id}`;

return (
<ListItem
key={category.id}
disablePadding
>
<ListItemButton role={undefined} onClick={handleToggle(category)} dense>
<ListItemIcon>
<Switch
key={`category-${category.id}`}
edge="start"
checked={selectedCategories.indexOf(category) !== -1}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': labelId }}
/>
</ListItemIcon>
<ListItemText id={labelId} primary={t(getTranslationKey(category.name))} />
</ListItemButton>
</ListItem>
);
})}
</List>
);
};

export const CategoryFilterDropdown = () => {
const [t] = useTranslation();

return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
{t('category')}
</AccordionSummary>
<AccordionDetails>
<CategoryFilterList />
</AccordionDetails>
</Accordion>
);
};

export const CategoryFilter = () => {
const [t] = useTranslation();

return (
<div data-testid={"categories"}>
<Paper>
<Typography gutterBottom variant="h6" m={2}>
{t('category')}
</Typography>

<List>
{categories.map((category) => {
const labelId = `checkbox-list-label-${category.id}`;

return (
<ListItem
key={category.id}
disablePadding
>
<ListItemButton role={undefined} onClick={handleToggle(category)} dense>
<ListItemIcon>
<Switch
key={`category-${category.id}`}
edge="start"
checked={selectedCategories.indexOf(category) !== -1}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': labelId }}
/>
</ListItemIcon>
<ListItemText id={labelId} primary={t(getTranslationKey(category.name))} />
</ListItemButton>
</ListItem>
);
})}
</List>
<CategoryFilterList />
</Paper>
</div>
);
Expand Down
Loading

0 comments on commit 7a20d2e

Please sign in to comment.