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

wger-project / react / 19445192225

17 Nov 2025 09:30PM UTC coverage: 75.147% (-0.03%) from 75.176%
19445192225

push

github

web-flow
Merge pull request #1143 from FilipCuper/fix/exercise-search-endpoint-1127

Fix #1127: Move exercise search to new endpoint

1737 of 2616 branches covered (66.4%)

Branch coverage included in aggregate %.

18 of 33 new or added lines in 7 files covered. (54.55%)

2 existing lines in 2 files now uncovered.

5535 of 7061 relevant lines covered (78.39%)

28.2 hits per line

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

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

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

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

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

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

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

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

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

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

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

82
    const [page, setPage] = React.useState(1);
20✔
83
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
    const handlePageChange = (event: any, value: number) => {
20✔
85
        setPage(value);
×
86

87
        window.scrollTo({
×
88
            top: 0,
89
            behavior: 'smooth'
90
        });
91
    };
92

93
    const filteredExercises = useMemo(() => {
20✔
94
        let filteredExercises = basesQuery.data || [];
14✔
95

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

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

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

125
        return filteredExercises;
14✔
126
    }, [basesQuery.data, selectedCategories, selectedEquipment, selectedMuscles]);
127

128
    // Should be a multiple of three, since there are three columns in the grid
129
    const ITEMS_PER_PAGE = 21;
20✔
130

131
    // Pagination calculations
132
    const pageCount = Math.ceil(filteredExercises!.length / ITEMS_PER_PAGE);
20✔
133
    const paginatedExercises = filteredExercises!.slice(
20✔
134
        (page - 1) * ITEMS_PER_PAGE,
135
        page * ITEMS_PER_PAGE
136
    );
137

138
    const exerciseAdded = (exercise: Exercise | null) => {
20✔
NEW
139
        if (!exercise) {
×
UNCOV
140
            return;
×
141
        }
142

NEW
143
        navigate(makeLink(WgerLink.EXERCISE_DETAIL, i18n.language, { id: exercise.id }));
×
144
    };
145

146
    return (
20✔
147
        (<Container maxWidth="lg">
148
            <Grid container spacing={2} mt={2}>
149
                <Grid
150
                    size={{
151
                        xs: 10,
152
                        sm: 6
153
                    }}>
154
                    <Typography gutterBottom variant="h3" component="div">
155
                        {t("exercises.exercises")}
156
                    </Typography>
157
                </Grid>
158
                {isMobile ? (
20!
159
                    <>
160
                        <Grid
161
                            size={{
162
                                xs: 2,
163
                                sm: 6
164
                            }}>
165
                            <Button
166
                                variant="contained"
167
                                onClick={() => navigate(makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language))}
×
168
                            >
169
                                <AddIcon />
170
                            </Button>
171
                        </Grid>
172
                        <Grid
173
                            flexGrow={1}
174
                            size={{
175
                                sm: 6
176
                            }}>
177
                            <NameAutocompleter callback={exerciseAdded} />
178
                        </Grid>
179
                        <Grid
180
                            display="flex"
181
                            justifyContent="center"
182
                            alignItems="center"
183
                            size={{
184
                                xs: 2,
185
                                sm: 6
186
                            }}>
187
                            <FilterDrawer>
188
                                <CategoryFilterDropdown />
189
                                <EquipmentFilterDropdown />
190
                                <MuscleFilterDropdown />
191
                            </FilterDrawer>
192
                        </Grid>
193
                    </>
194
                ) : (
195
                    <>
196
                        <Grid
197
                            size={{
198
                                xs: 12,
199
                                sm: 3
200
                            }}>
201
                            <NameAutocompleter callback={exerciseAdded} />
202
                        </Grid>
203
                        <Grid
204
                            size={{
205
                                xs: 12,
206
                                sm: 3
207
                            }}>
208
                            <Button
209
                                variant="contained"
210
                                startIcon={<AddIcon />}
211
                                onClick={() => navigate(makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language))}
×
212
                            >
213
                                {t('exercises.contributeExercise')}
214
                            </Button>
215
                        </Grid>
216
                    </>
217
                )}
218

219
                {!isMobile && (
40✔
220
                    <Grid
221
                        size={{
222
                            xs: 12,
223
                            sm: 3
224
                        }}>
225
                        <Grid container spacing={1}>
226
                            <Grid
227
                                size={{
228
                                    xs: 6,
229
                                    sm: 12
230
                                }}>
231
                                <CategoryFilter />
232
                            </Grid>
233

234
                            <Grid
235
                                size={{
236
                                    xs: 6,
237
                                    sm: 12
238
                                }}>
239
                                <EquipmentFilter />
240
                            </Grid>
241

242
                            <Grid size={12}>
243
                                <MuscleFilter />
244
                            </Grid>
245
                        </Grid>
246
                    </Grid>
247
                )}
248

249
                <Grid
250
                    size={{
251
                        xs: 12,
252
                        sm: 9
253
                    }}>
254
                    {basesQuery.isLoading
20✔
255
                        ? <ExerciseGridSkeleton />
256
                        : <>
257
                            <ExerciseGrid exercises={paginatedExercises} />
258
                            <Stack spacing={2} alignItems="center" sx={{ mt: 2 }}>
259
                                <Pagination
260
                                    count={pageCount}
261
                                    color="primary"
262
                                    page={page}
263
                                    onChange={handlePageChange}
264
                                />
265
                            </Stack>
266
                        </>
267
                    }
268

269
                    <ContributeExerciseBanner />
270
                </Grid>
271
            </Grid>
272
        </Container>)
273
    );
274
};
275

276
export const ExerciseOverview = () => {
1✔
277
    const [selectedEquipment, setSelectedEquipment] = useState<Equipment[]>([]);
13✔
278
    const [selectedMuscles, setSelectedMuscles] = useState<Muscle[]>([]);
13✔
279
    const [selectedCategories, setSelectedCategories] = React.useState<Category[]>([]);
13✔
280

281
    return (
13✔
282
        <ExerciseFiltersContext.Provider value={{
283
            selectedEquipment,
284
            setSelectedEquipment,
285
            selectedMuscles,
286
            setSelectedMuscles,
287
            selectedCategories,
288
            setSelectedCategories
289
        }}>
290
            <ExerciseOverviewList />
291
        </ExerciseFiltersContext.Provider>
292
    );
293
};
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