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

wger-project / react / 24395780142

14 Apr 2026 11:11AM UTC coverage: 75.526% (+0.04%) from 75.488%
24395780142

push

github

rolandgeider
Bump dependencies

1952 of 2938 branches covered (66.44%)

Branch coverage included in aggregate %.

6019 of 7616 relevant lines covered (79.03%)

29.27 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 { Exercise } from "components/Exercises/models/exercise";
11
import { Muscle } from "components/Exercises/models/muscle";
12
import { ExerciseGrid } from "components/Exercises/Overview/ExerciseGrid";
1✔
13
import { ExerciseGridSkeleton } from "components/Exercises/Overview/ExerciseGridLoadingSkeleton";
1✔
14

15
import { useExercisesQuery } from "components/Exercises/queries";
1✔
16
import React, { useContext, useMemo, useState } from "react";
1✔
17
import { useTranslation } from "react-i18next";
1✔
18
import { Link, useNavigate } from "react-router-dom";
1✔
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();
14✔
25

26
    return (
14✔
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 exerciseQuery = useExercisesQuery();
14✔
77
    const [t, i18n] = useTranslation();
14✔
78
    const navigate = useNavigate();
14✔
79
    const { selectedCategories, selectedEquipment, selectedMuscles } = useContext(ExerciseFiltersContext);
14✔
80
    const isMobile = useMediaQuery('(max-width:600px)');
14✔
81

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

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

93
    const filteredExercises = useMemo(() => {
14✔
94
        let filteredExercises = exerciseQuery.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
    }, [exerciseQuery.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;
14✔
130

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

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

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

146
    return (
14✔
147
        (<Container maxWidth="lg">
148
            <Grid container spacing={2} sx={{ 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 ? (
14!
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
                            sx={{ flexGrow: 1 }}
174
                            size={{
175
                                sm: 6
176
                            }}>
177
                            <NameAutocompleter callback={exerciseAdded} />
178
                        </Grid>
179
                        <Grid
180
                            sx={{ display: "flex", justifyContent: "center", alignItems: "center" }}
181
                            size={{
182
                                xs: 2,
183
                                sm: 6
184
                            }}>
185
                            <FilterDrawer>
186
                                <CategoryFilterDropdown />
187
                                <EquipmentFilterDropdown />
188
                                <MuscleFilterDropdown />
189
                            </FilterDrawer>
190
                        </Grid>
191
                    </>
192
                ) : (
193
                    <>
194
                        <Grid
195
                            size={{
196
                                xs: 12,
197
                                sm: 3
198
                            }}>
199
                            <NameAutocompleter callback={exerciseAdded} />
200
                        </Grid>
201
                        <Grid
202
                            size={{
203
                                xs: 12,
204
                                sm: 3
205
                            }}>
206
                            <Button
207
                                variant="contained"
208
                                startIcon={<AddIcon />}
209
                                onClick={() => navigate(makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language))}
×
210
                            >
211
                                {t('exercises.contributeExercise')}
212
                            </Button>
213
                        </Grid>
214
                    </>
215
                )}
216

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

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

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

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

267
                    <ContributeExerciseBanner />
268
                </Grid>
269
            </Grid>
270
        </Container>)
271
    );
272
};
273

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

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