• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wger-project / react / 24782100753

22 Apr 2026 01:50PM UTC coverage: 74.625% (-0.1%) from 74.74%
24782100753

push

github

web-flow
Merge pull request #1235 from Sahasra0108/fix/exercise-ui-match-ingredients

Fix/exercise UI match ingredients

2042 of 3109 branches covered (65.68%)

Branch coverage included in aggregate %.

31 of 43 new or added lines in 5 files covered. (72.09%)

3 existing lines in 3 files now uncovered.

6110 of 7815 relevant lines covered (78.18%)

28.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.11
/src/components/Exercises/ExerciseOverview.tsx
1
import AddIcon from '@mui/icons-material/Add';
1✔
2
import FilterListIcon from '@mui/icons-material/FilterList';
1✔
3
import { Box, Button, Container, Pagination, Stack, Typography, useMediaQuery, IconButton } from "@mui/material";
1✔
4
import Grid from '@mui/material/Grid';
1✔
5
import { CategoryFilter, CategoryFilterDropdown } from "components/Exercises/Filter/CategoryFilter";
1✔
6
import { EquipmentFilter, EquipmentFilterDropdown } from "components/Exercises/Filter/EquipmentFilter";
1✔
7
import { MuscleFilter, MuscleFilterDropdown } from "components/Exercises/Filter/MuscleFilter";
1✔
8
import { NameAutocompleter } from "components/Exercises/Filter/NameAutcompleter";
1✔
9
import { Category } from "components/Exercises/models/category";
10
import { Equipment } from "components/Exercises/models/equipment";
11
import { Exercise } from "components/Exercises/models/exercise";
12
import { Muscle } from "components/Exercises/models/muscle";
13
import { ExerciseGrid } from "components/Exercises/Overview/ExerciseGrid";
1✔
14
import { ExerciseGridSkeleton } from "components/Exercises/Overview/ExerciseGridLoadingSkeleton";
1✔
15

16
import { useExercisesQuery } from "components/Exercises/queries";
1✔
17
import React, { useContext, useMemo, useState } from "react";
1✔
18
import { useTranslation } from "react-i18next";
1✔
19
import { Link, useNavigate } from "react-router-dom";
1✔
20
import { makeLink, WgerLink } from "utils/url";
1✔
21
import { ExerciseFiltersContext } from './Filter/ExerciseFiltersContext';
1✔
22
import { FilterDrawer } from './Filter/FilterDrawer';
1✔
23

24
const ContributeExerciseBanner = () => {
1✔
25
    const [t, i18n] = useTranslation();
14✔
26

27
    return (
14✔
28
        <Box
29
            sx={{
30
                marginTop: 4,
31
                padding: 4,
32
                width: "100%",
33
                backgroundColor: "#ebebeb",
34
                textAlign: "center",
35
            }}
36
        >
37
            <Typography gutterBottom variant="h4" component="div">
38
                {t("exercises.missingExercise")}
39
            </Typography>
40

41
            <Typography gutterBottom variant="body1" component="div">
42
                {t("exercises.missingExerciseDescription")}
43
            </Typography>
44

45
            <Link to={makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language)}>
46
                {t("exercises.contributeExercise")}
47
            </Link>
48
        </Box>
49
    );
50
};
51

52
const NoResultsBanner = () => {
1✔
53
    const [t] = useTranslation();
×
54

55
    return (
×
56
        <Box
57
            sx={{
58
                marginTop: 4,
59
                padding: 4,
60
                width: "100%",
61
                backgroundColor: "#ebebeb",
62
                textAlign: "center",
63
            }}
64
        >
65
            <Typography gutterBottom variant="h4" component="div">
66
                {t("noResults")}
67
            </Typography>
68

69
            <Typography gutterBottom variant="body1" component="div">
70
                {t("noResultsDescription")}
71
            </Typography>
72
        </Box>
73
    );
74
};
75

76
export const ExerciseOverviewList = () => {
1✔
77
    const exerciseQuery = useExercisesQuery();
14✔
78
    const [t, i18n] = useTranslation();
14✔
79
    const navigate = useNavigate();
14✔
80
    const { selectedCategories, selectedEquipment, selectedMuscles } = useContext(ExerciseFiltersContext);
14✔
81
    const isMobile = useMediaQuery('(max-width:600px)');
14✔
82

83
    const [page, setPage] = React.useState(1);
14✔
84
    const [showFilters, setShowFilters] = useState<boolean>(() => {
14✔
85
        return localStorage.getItem("wger.exerciseSearch.showFilters") !== "false";
6✔
86
    });
87
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
    const handlePageChange = (event: any, value: number) => {
14✔
89
        setPage(value);
×
90

91
        window.scrollTo({
×
92
            top: 0,
93
            behavior: 'smooth'
94
        });
95
    };
96

97
    const filteredExercises = useMemo(() => {
14✔
98
        let filteredExercises = exerciseQuery.data || [];
14✔
99

100
        // Filter exercise bases by categories
101
        if (selectedCategories.length > 0) {
14✔
102
            filteredExercises = filteredExercises!.filter(exercise => {
5✔
103
                return selectedCategories.some(
25✔
104
                    category => exercise.category.id === category.id
28✔
105
                );
106
            });
107
        }
108

109
        // Filter exercises that have one of the selected equipment
110
        if (selectedEquipment.length > 0) {
14✔
111
            filteredExercises = filteredExercises!.filter(exercise => {
2✔
112
                return exercise.equipment.some(equipment =>
7✔
113
                    selectedEquipment.some(
9✔
114
                        selectedEquipment => selectedEquipment.id === equipment.id
9✔
115
                    )
116
                );
117
            });
118
        }
119

120
        // Filter exercises that have one of the selected muscles
121
        if (selectedMuscles.length > 0) {
14✔
122
            filteredExercises = filteredExercises!.filter(exercise => {
1✔
123
                return exercise.muscles.some(muscle =>
5✔
124
                    selectedMuscles.some(selectedMuscle => selectedMuscle.id === muscle.id)
6✔
125
                );
126
            });
127
        }
128

129
        return filteredExercises;
14✔
130
    }, [exerciseQuery.data, selectedCategories, selectedEquipment, selectedMuscles]);
131

132
    // Should be a multiple of three, since there are three columns in the grid
133
    const ITEMS_PER_PAGE = 21;
14✔
134

135
    // Pagination calculations
136
    const pageCount = Math.ceil(filteredExercises!.length / ITEMS_PER_PAGE);
14✔
137
    const paginatedExercises = filteredExercises!.slice(
14✔
138
        (page - 1) * ITEMS_PER_PAGE,
139
        page * ITEMS_PER_PAGE
140
    );
141

142
    const exerciseAdded = (exercise: Exercise | null) => {
14✔
143
        if (!exercise) {
×
144
            return;
×
145
        }
146

147
        navigate(makeLink(WgerLink.EXERCISE_DETAIL, i18n.language, { id: exercise.id! }));
×
148
    };
149

150
    return (
14✔
151
        (<Container maxWidth="lg">
152
            <Grid container spacing={2} sx={{ mt: 2 }}>
153
                <Grid
154
                    size={{
155
                        xs: 10,
156
                        sm: 6
157
                    }}>
158
                    <Typography gutterBottom variant="h3" component="div">
159
                        {t("exercises.exercises")}
160
                    </Typography>
161
                </Grid>
162
                {isMobile ? (
14!
163
                    <>
164
                        <Grid
165
                            size={{
166
                                xs: 2,
167
                                sm: 6
168
                            }}>
169
                            <Button
170
                                variant="contained"
171
                                onClick={() => navigate(makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language))}
×
172
                            >
173
                                <AddIcon />
174
                            </Button>
175
                        </Grid>
176
                        <Grid
177
                            sx={{ flexGrow: 1 }}
178
                            size={{
179
                                sm: 6
180
                            }}>
181
                            <NameAutocompleter callback={exerciseAdded} />
182
                        </Grid>
183
                        <Grid
184
                            sx={{ display: "flex", justifyContent: "center", alignItems: "center" }}
185
                            size={{
186
                                xs: 2,
187
                                sm: 6
188
                            }}>
189
                            <FilterDrawer>
190
                                <CategoryFilterDropdown />
191
                                <EquipmentFilterDropdown />
192
                                <MuscleFilterDropdown />
193
                            </FilterDrawer>
194
                        </Grid>
195
                    </>
196
                ) : (
197
                    <>
198
                        <Grid
199
                            size={{
200
                                xs: 12,
201
                                sm: showFilters ? 3 : 6
14!
202
                            }}>
203
                            <Stack direction="row" alignItems="center" spacing={1} sx={{ width: '100%' }}>
204
                                <Box sx={{ flexGrow: 1 }}>
205
                                    <NameAutocompleter callback={exerciseAdded} />
206
                                </Box>
207
                                <IconButton
208
                                    onClick={() => {
NEW
209
                                        const newValue = !showFilters;
×
NEW
210
                                        setShowFilters(newValue);
×
NEW
211
                                        localStorage.setItem("wger.exerciseSearch.showFilters", String(newValue));
×
212
                                    }}
213
                                    title="Toggle filters"
214
                                >
215
                                    <FilterListIcon />
216
                                </IconButton>
217
                            </Stack>
218
                        </Grid>
219
                        <Grid
220
                            size={{
221
                                xs: 12,
222
                                sm: 3
223
                            }}>
224
                            <Button
225
                                variant="contained"
226
                                startIcon={<AddIcon />}
227
                                onClick={() => navigate(makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language))}
×
228
                            >
229
                                {t('exercises.contributeExercise')}
230
                            </Button>
231
                        </Grid>
232
                    </>
233
                )}
234

235
                {!isMobile && showFilters && (
42✔
236
                    <Grid
237
                        size={{
238
                            xs: 12,
239
                            sm: 3
240
                        }}>
241
                        <Grid container spacing={1}>
242
                            <Grid
243
                                size={{
244
                                    xs: 6,
245
                                    sm: 12
246
                                }}>
247
                                <CategoryFilter />
248
                            </Grid>
249

250
                            <Grid
251
                                size={{
252
                                    xs: 6,
253
                                    sm: 12
254
                                }}>
255
                                <EquipmentFilter />
256
                            </Grid>
257

258
                            <Grid size={12}>
259
                                <MuscleFilter />
260
                            </Grid>
261
                        </Grid>
262
                    </Grid>
263
                )}
264

265
                <Grid
266
                    size={{
267
                        xs: 12,
268
                        sm: showFilters ? 9 : 12
14!
269
                    }}>
270
                    {exerciseQuery.isLoading
14✔
271
                        ? <ExerciseGridSkeleton />
272
                        : <>
273
                            <ExerciseGrid exercises={paginatedExercises} />
274
                            <Stack spacing={2} sx={{ alignItems: "center", mt: 2 }}>
275
                                <Pagination
276
                                    count={pageCount}
277
                                    color="primary"
278
                                    page={page}
279
                                    onChange={handlePageChange}
280
                                />
281
                            </Stack>
282
                        </>
283
                    }
284

285
                    <ContributeExerciseBanner />
286
                </Grid>
287
            </Grid>
288
        </Container>)
289
    );
290
};
291

292
export const ExerciseOverview = () => {
1✔
293
    const [selectedEquipment, setSelectedEquipment] = useState<Equipment[]>([]);
13✔
294
    const [selectedMuscles, setSelectedMuscles] = useState<Muscle[]>([]);
13✔
295
    const [selectedCategories, setSelectedCategories] = React.useState<Category[]>([]);
13✔
296

297
    return (
13✔
298
        <ExerciseFiltersContext.Provider value={{
299
            selectedEquipment,
300
            setSelectedEquipment,
301
            selectedMuscles,
302
            setSelectedMuscles,
303
            selectedCategories,
304
            setSelectedCategories
305
        }}>
306
            <ExerciseOverviewList />
307
        </ExerciseFiltersContext.Provider>
308
    );
309
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc