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

wger-project / react / 13974728903

20 Mar 2025 04:44PM UTC coverage: 76.613% (+0.7%) from 75.927%
13974728903

push

github

web-flow
Merge pull request #975 from wger-project/feature/flexible-routines

UI for flexible routines

1273 of 1859 branches covered (68.48%)

Branch coverage included in aggregate %.

2063 of 2541 new or added lines in 104 files covered. (81.19%)

3 existing lines in 3 files now uncovered.

5161 of 6539 relevant lines covered (78.93%)

25.57 hits per line

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

85.33
/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/Grid2';
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
import { useExercisesQuery } from "components/Exercises/queries";
1✔
14
import React, { useContext, useMemo, useState } from "react";
1✔
15
import { useTranslation } from "react-i18next";
1✔
16
import { Link, useNavigate } from "react-router-dom";
1✔
17
import { ExerciseSearchResponse } from "services/responseType";
18
import { makeLink, WgerLink } from "utils/url";
1✔
19
import { ExerciseFiltersContext } from './Filter/ExerciseFiltersContext';
1✔
20
import { FilterDrawer } from './Filter/FilterDrawer';
1✔
21

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

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

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

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

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

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

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

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

81
    const [page, setPage] = React.useState(1);
20✔
82
    const handlePageChange = (event: any, value: number) => {
20✔
83
        setPage(value);
×
84

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

91
    let filteredExercises = useMemo(() => {
20✔
92
        let filteredExercises = basesQuery.data || [];
14✔
93

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

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

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

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

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

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

136
    const exerciseAdded = (exerciseResponse: ExerciseSearchResponse | null) => {
20✔
NEW
137
        if (!exerciseResponse) {
×
NEW
138
            return;
×
139
        }
140

UNCOV
141
        navigate(makeLink(WgerLink.EXERCISE_DETAIL, i18n.language, { id: exerciseResponse.data.base_id }));
×
142
    };
143

144
    return (
20✔
145
        (<Container maxWidth="lg">
146
            <Grid container spacing={2} mt={2}>
147
                <Grid
148
                    size={{
149
                        xs: 10,
150
                        sm: 6
151
                    }}>
152
                    <Typography gutterBottom variant="h3" component="div">
153
                        {t("exercises.exercises")}
154
                    </Typography>
155
                </Grid>
156
                {isMobile ? (
20!
157
                    <>
158
                        <Grid
159
                            size={{
160
                                xs: 2,
161
                                sm: 6
162
                            }}>
163
                            <Button
164
                                variant="contained"
165
                                onClick={() => navigate(makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language))}
×
166
                            >
167
                                <AddIcon />
168
                            </Button>
169
                        </Grid>
170
                        <Grid
171
                            flexGrow={1}
172
                            size={{
173
                                sm: 6
174
                            }}>
175
                            <NameAutocompleter callback={exerciseAdded} />
176
                        </Grid>
177
                        <Grid
178
                            display="flex"
179
                            justifyContent="center"
180
                            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 && (
40✔
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
                    {basesQuery.isLoading
20✔
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