@wger-project/react-components 25.10.24 → 25.11.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/chunks/{browser-ponyfill-DL_vVusK.js → browser-ponyfill-CyEkMYuR.js} +3 -3
- package/build/chunks/{browser-ponyfill-DL_vVusK.js.map → browser-ponyfill-CyEkMYuR.js.map} +1 -1
- package/build/locales/fil/translation.json +3 -0
- package/build/locales/sk/translation.json +26 -1
- package/build/main.js +137 -235
- package/build/main.js.map +1 -1
- package/package.json +14 -14
- package/src/components/BodyWeight/Form/WeightForm.tsx +3 -4
- package/src/components/BodyWeight/WeightChart/index.tsx +26 -29
- package/src/components/Exercises/Add/Step1Basics.test.tsx +4 -1
- package/src/components/Exercises/Detail/ExerciseDetailEdit.tsx +43 -27
- package/src/components/Exercises/Detail/ExerciseDetailView.tsx +57 -17
- package/src/components/Exercises/Detail/Head/ExerciseDeleteDialog.tsx +3 -4
- package/src/components/Exercises/Detail/OverviewCard.test.tsx +7 -11
- package/src/components/Exercises/ExerciseOverview.tsx +8 -8
- package/src/components/Exercises/Filter/NameAutcompleter.tsx +41 -39
- package/src/components/Exercises/Filter/NameAutocompleter.test.tsx +2 -1
- package/src/components/Exercises/Overview/ExerciseGrid.tsx +1 -2
- package/src/components/Exercises/forms/ExerciseAliases.tsx +78 -21
- package/src/components/Exercises/forms/yupValidators.ts +7 -4
- package/src/components/Exercises/models/exercise.ts +55 -43
- package/src/components/Measurements/Screens/MeasurementCategoryDetail.test.tsx +4 -2
- package/src/components/Measurements/widgets/MeasurementChart.tsx +27 -30
- package/src/components/Nutrition/components/BmiCalculator.tsx +40 -42
- package/src/components/Nutrition/widgets/charts/MacrosPieChart.tsx +17 -19
- package/src/components/Nutrition/widgets/charts/NutritionDiaryChart.tsx +39 -38
- package/src/components/Nutrition/widgets/charts/NutritionalValuesDashboardChart.tsx +27 -29
- package/src/components/Nutrition/widgets/charts/NutritionalValuesPlannedLoggedChart.tsx +20 -19
- package/src/components/WorkoutRoutines/Detail/WorkoutStats.tsx +17 -19
- package/src/components/WorkoutRoutines/widgets/DayDetails.tsx +3 -3
- package/src/components/WorkoutRoutines/widgets/LogWidgets.tsx +35 -38
- package/src/components/WorkoutRoutines/widgets/SlotDetails.tsx +4 -4
- package/src/components/WorkoutRoutines/widgets/forms/SessionLogsForm.tsx +4 -4
- package/src/services/exerciseTranslation.ts +24 -9
- package/src/services/measurements.ts +15 -2
- package/src/services/responseType.ts +3 -29
- package/src/tests/exerciseTestdata.ts +61 -55
- package/src/tests/exercises/searchResponse.ts +49 -22
- package/src/tests/responseApi.ts +25 -14
|
@@ -12,25 +12,25 @@ import {
|
|
|
12
12
|
Switch,
|
|
13
13
|
TextField,
|
|
14
14
|
} from "@mui/material";
|
|
15
|
+
import { Exercise } from "components/Exercises/models/exercise";
|
|
15
16
|
import { SERVER_URL } from "config";
|
|
16
17
|
import debounce from "lodash/debounce";
|
|
17
18
|
import * as React from "react";
|
|
18
19
|
import { useState } from "react";
|
|
19
20
|
import { useTranslation } from "react-i18next";
|
|
20
|
-
import {
|
|
21
|
-
import { ExerciseSearchResponse } from "services/responseType";
|
|
21
|
+
import { searchExerciseTranslations } from "services";
|
|
22
22
|
import { LANGUAGE_SHORT_ENGLISH } from "utils/consts";
|
|
23
23
|
|
|
24
24
|
type NameAutocompleterProps = {
|
|
25
|
-
callback: (
|
|
25
|
+
callback: (exercise: Exercise | null) => void;
|
|
26
26
|
loadExercise?: boolean;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
export function NameAutocompleter({ callback, loadExercise }: NameAutocompleterProps) {
|
|
30
|
-
const [value, setValue] = React.useState<
|
|
30
|
+
const [value, setValue] = React.useState<Exercise | null>(null);
|
|
31
31
|
const [inputValue, setInputValue] = React.useState("");
|
|
32
32
|
const [searchEnglish, setSearchEnglish] = useState<boolean>(true);
|
|
33
|
-
const [options, setOptions] = React.useState<readonly
|
|
33
|
+
const [options, setOptions] = React.useState<readonly Exercise[]>([]);
|
|
34
34
|
const [t, i18n] = useTranslation();
|
|
35
35
|
|
|
36
36
|
loadExercise = loadExercise === undefined ? false : loadExercise;
|
|
@@ -61,7 +61,7 @@ export function NameAutocompleter({ callback, loadExercise }: NameAutocompleterP
|
|
|
61
61
|
<>
|
|
62
62
|
<Autocomplete
|
|
63
63
|
id="exercise-name-autocomplete"
|
|
64
|
-
getOptionLabel={(option) => option.
|
|
64
|
+
getOptionLabel={(option) => option.getTranslation().name}
|
|
65
65
|
data-testid="autocomplete"
|
|
66
66
|
filterOptions={(x) => x}
|
|
67
67
|
options={options}
|
|
@@ -70,13 +70,10 @@ export function NameAutocompleter({ callback, loadExercise }: NameAutocompleterP
|
|
|
70
70
|
filterSelectedOptions
|
|
71
71
|
value={value}
|
|
72
72
|
noOptionsText={t("noResults")}
|
|
73
|
-
isOptionEqualToValue={(option, value) => option.
|
|
74
|
-
onChange={async (event: React.SyntheticEvent, newValue:
|
|
73
|
+
isOptionEqualToValue={(option, value) => option.id === value.id}
|
|
74
|
+
onChange={async (event: React.SyntheticEvent, newValue: Exercise | null) => {
|
|
75
75
|
setOptions(newValue ? [newValue, ...options] : options);
|
|
76
76
|
setValue(newValue);
|
|
77
|
-
if (loadExercise && newValue !== null) {
|
|
78
|
-
newValue.exercise = await getExercise(newValue.data.base_id);
|
|
79
|
-
}
|
|
80
77
|
callback(newValue);
|
|
81
78
|
}}
|
|
82
79
|
onInputChange={(event, newInputValue) => {
|
|
@@ -102,34 +99,39 @@ export function NameAutocompleter({ callback, loadExercise }: NameAutocompleterP
|
|
|
102
99
|
}}
|
|
103
100
|
/>
|
|
104
101
|
)}
|
|
105
|
-
renderOption={(props, option, state) =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
102
|
+
renderOption={(props, option, state) => {
|
|
103
|
+
const translation = option.getTranslation();
|
|
104
|
+
const mainImage = option.mainImage;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<li
|
|
108
|
+
{...props}
|
|
109
|
+
key={`exercise-${state.index}-${option.id}`}
|
|
110
|
+
data-testid={`autocompleter-result-${option.id}`}
|
|
111
|
+
>
|
|
112
|
+
<ListItem disablePadding component="div">
|
|
113
|
+
<ListItemIcon>
|
|
114
|
+
{mainImage ? (
|
|
115
|
+
<Avatar alt="" src={`${SERVER_URL}${mainImage.url}`} variant="rounded" />
|
|
116
|
+
) : (
|
|
117
|
+
<PhotoIcon fontSize="large" />
|
|
118
|
+
)}
|
|
119
|
+
</ListItemIcon>
|
|
120
|
+
<ListItemText
|
|
121
|
+
primary={translation.name}
|
|
122
|
+
slotProps={{
|
|
123
|
+
primary: {
|
|
124
|
+
whiteSpace: "nowrap",
|
|
125
|
+
overflow: "hidden",
|
|
126
|
+
textOverflow: "ellipsis",
|
|
127
|
+
},
|
|
128
|
+
}}
|
|
129
|
+
secondary={option.category.name}
|
|
130
|
+
/>
|
|
131
|
+
</ListItem>
|
|
132
|
+
</li>
|
|
133
|
+
);
|
|
134
|
+
}}
|
|
133
135
|
/>
|
|
134
136
|
{i18n.language !== LANGUAGE_SHORT_ENGLISH && (
|
|
135
137
|
<FormGroup>
|
|
@@ -3,6 +3,7 @@ import { NameAutocompleter } from "components/Exercises/Filter/NameAutcompleter"
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { searchExerciseTranslations } from "services";
|
|
5
5
|
import { searchResponse } from "tests/exercises/searchResponse";
|
|
6
|
+
import { Exercise } from "components/Exercises/models/exercise";
|
|
6
7
|
|
|
7
8
|
jest.mock("services");
|
|
8
9
|
const mockCallback = jest.fn();
|
|
@@ -71,6 +72,6 @@ describe("Test the NameAutocompleter component", () => {
|
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
// Assert
|
|
74
|
-
expect(mockCallback).
|
|
75
|
+
expect(mockCallback).toHaveBeenCalledWith(expect.any(Exercise));
|
|
75
76
|
});
|
|
76
77
|
});
|
|
@@ -15,8 +15,7 @@ export const ExerciseGrid = ({ exercises }: ExerciseGridProps) => {
|
|
|
15
15
|
|
|
16
16
|
const languageQuery = useLanguageQuery();
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
const [t, i18n] = useTranslation();
|
|
18
|
+
const { i18n } = useTranslation();
|
|
20
19
|
|
|
21
20
|
let currentUserLanguage: Language | undefined;
|
|
22
21
|
if (languageQuery.isSuccess) {
|
|
@@ -1,37 +1,94 @@
|
|
|
1
|
-
import { Autocomplete, Chip, TextField } from "@mui/material";
|
|
1
|
+
import { Autocomplete, Chip, InputAdornment, TextField } from "@mui/material";
|
|
2
2
|
import { useField } from "formik";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useTranslation } from "react-i18next";
|
|
5
5
|
|
|
6
|
+
type AliasItem = { id?: number; alias: string };
|
|
7
|
+
|
|
6
8
|
export function ExerciseAliases(props: { fieldName: string }) {
|
|
7
9
|
const [t] = useTranslation();
|
|
8
|
-
const [field, meta, helpers] = useField(props.fieldName);
|
|
10
|
+
const [field, meta, helpers] = useField<AliasItem[]>(props.fieldName);
|
|
11
|
+
|
|
12
|
+
const normalize = (items: (AliasItem | string)[] | null | undefined): AliasItem[] =>
|
|
13
|
+
(items || []).map(item =>
|
|
14
|
+
typeof item === "string" ? { alias: item } : ("alias" in item ? (item as AliasItem) : { alias: String(item) })
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract a human-readable error string from the Yup alias validator, which
|
|
19
|
+
* returns a list of errors.
|
|
20
|
+
*/
|
|
21
|
+
const formatError = (err: unknown): string | undefined => {
|
|
22
|
+
if (!err) return undefined;
|
|
23
|
+
if (typeof err === "string") return err;
|
|
24
|
+
|
|
25
|
+
if (typeof err === "object") {
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
const o = err as any;
|
|
28
|
+
if (typeof o.alias === "string") return o.alias;
|
|
29
|
+
if (typeof o.message === "string") return o.message;
|
|
30
|
+
|
|
31
|
+
for (const k of Object.keys(o)) {
|
|
32
|
+
const v = o[k];
|
|
33
|
+
if (typeof v === "string") return v;
|
|
34
|
+
if (v && typeof v === "object") {
|
|
35
|
+
if (typeof v.alias === "string") return v.alias;
|
|
36
|
+
if (typeof v.message === "string") return v.message;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return String(err);
|
|
42
|
+
};
|
|
9
43
|
|
|
10
44
|
return <Autocomplete
|
|
11
45
|
multiple
|
|
12
46
|
freeSolo
|
|
13
47
|
id={props.fieldName}
|
|
14
|
-
value={field.value}
|
|
15
|
-
options={
|
|
16
|
-
|
|
17
|
-
|
|
48
|
+
value={field.value || []}
|
|
49
|
+
options={[]}
|
|
50
|
+
getOptionLabel={(opt) => (typeof opt === "string" ? opt : opt.alias)}
|
|
51
|
+
isOptionEqualToValue={(option, value) => option.alias === value.alias && (option.id === value.id || option.id === undefined || value.id === undefined)}
|
|
52
|
+
onChange={(_, newValue) => {
|
|
53
|
+
helpers.setValue(normalize(newValue));
|
|
18
54
|
}}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
55
|
+
onBlur={field.onBlur}
|
|
56
|
+
renderInput={(params) => {
|
|
57
|
+
const chips = (field.value || []).map((option, index) => (
|
|
58
|
+
<Chip
|
|
59
|
+
label={option.alias}
|
|
60
|
+
onDelete={() => {
|
|
61
|
+
const newVal = [...(field.value || [])];
|
|
62
|
+
newVal.splice(index, 1);
|
|
63
|
+
helpers.setValue(newVal);
|
|
64
|
+
}}
|
|
65
|
+
key={option.id ?? `${option.alias}-${index}`}
|
|
66
|
+
/>
|
|
22
67
|
));
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<TextField
|
|
71
|
+
{...params}
|
|
72
|
+
id="exerciseAliases"
|
|
73
|
+
variant="standard"
|
|
74
|
+
label={t("exercises.alternativeNames")}
|
|
75
|
+
error={meta.touched && Boolean(meta.error)}
|
|
76
|
+
helperText={meta.touched ? formatError(meta.error) : undefined}
|
|
77
|
+
slotProps={{
|
|
78
|
+
input: {
|
|
79
|
+
startAdornment: (
|
|
80
|
+
<InputAdornment
|
|
81
|
+
position="start"
|
|
82
|
+
sx={{ display: "flex", gap: 0.5, alignItems: "center" }}
|
|
83
|
+
>
|
|
84
|
+
{chips}
|
|
85
|
+
{/*{params.InputProps?.startAdornment}*/}
|
|
86
|
+
</InputAdornment>
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
23
92
|
}}
|
|
24
|
-
onBlur={field.onBlur}
|
|
25
|
-
renderInput={params => (
|
|
26
|
-
<TextField
|
|
27
|
-
{...params}
|
|
28
|
-
id="exerciseAliases"
|
|
29
|
-
variant="standard"
|
|
30
|
-
label={t("exercises.alternativeNames")}
|
|
31
|
-
error={meta.touched && Boolean(meta.error)}
|
|
32
|
-
helperText={meta.touched && meta.error}
|
|
33
|
-
value={field.value}
|
|
34
|
-
/>
|
|
35
|
-
)}
|
|
36
93
|
/>;
|
|
37
94
|
}
|
|
@@ -20,10 +20,13 @@ export const alternativeNameValidator = () => yup
|
|
|
20
20
|
.ensure()
|
|
21
21
|
.compact()
|
|
22
22
|
.of(
|
|
23
|
-
yup
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
yup.object({
|
|
24
|
+
id: yup.number().nullable(),
|
|
25
|
+
alias: yup.string()
|
|
26
|
+
.min(MIN_CHAR_NAME, i18n.t("forms.minLength", { 'chars': MIN_CHAR_NAME }))
|
|
27
|
+
.max(MAX_CHAR_NAME, i18n.t("forms.maxLength", { 'chars': MAX_CHAR_NAME }))
|
|
28
|
+
.required()
|
|
29
|
+
})
|
|
27
30
|
);
|
|
28
31
|
|
|
29
32
|
export const descriptionValidator = () => yup
|
|
@@ -8,42 +8,53 @@ import { ExerciseVideo, ExerciseVideoAdapter } from "components/Exercises/models
|
|
|
8
8
|
import { Adapter } from "utils/Adapter";
|
|
9
9
|
import { ENGLISH_LANGUAGE_ID } from "utils/consts";
|
|
10
10
|
|
|
11
|
+
export type ExerciseConstructorParams = {
|
|
12
|
+
id: number | null;
|
|
13
|
+
uuid: string | null;
|
|
14
|
+
category: Category;
|
|
15
|
+
equipment?: Equipment[];
|
|
16
|
+
muscles?: Muscle[];
|
|
17
|
+
musclesSecondary?: Muscle[];
|
|
18
|
+
images?: ExerciseImage[];
|
|
19
|
+
variationId?: number | null;
|
|
20
|
+
lastUpdateGlobal?: Date;
|
|
21
|
+
translations?: Translation[];
|
|
22
|
+
videos?: ExerciseVideo[];
|
|
23
|
+
authors?: string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
11
26
|
export class Exercise {
|
|
12
|
-
|
|
27
|
+
id: number | null;
|
|
28
|
+
uuid: string | null;
|
|
29
|
+
variationId: number | null;
|
|
30
|
+
category: Category;
|
|
31
|
+
lastUpdateGlobal: Date;
|
|
32
|
+
|
|
33
|
+
muscles: Muscle[] = [];
|
|
34
|
+
musclesSecondary: Muscle[] = [];
|
|
35
|
+
images: ExerciseImage[] = [];
|
|
13
36
|
videos: ExerciseVideo[] = [];
|
|
37
|
+
equipment: Equipment[] = [];
|
|
14
38
|
authors: string[] = [];
|
|
39
|
+
translations: Translation[] = [];
|
|
15
40
|
|
|
16
|
-
constructor(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
videos
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
licenseAuthorS: string[],
|
|
31
|
-
*/
|
|
32
|
-
) {
|
|
33
|
-
if (translations) {
|
|
34
|
-
this.translations = translations;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (videos) {
|
|
38
|
-
this.videos = videos;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (authors) {
|
|
42
|
-
this.authors = authors;
|
|
43
|
-
}
|
|
41
|
+
constructor(init: ExerciseConstructorParams) {
|
|
42
|
+
this.id = init.id;
|
|
43
|
+
this.uuid = init.uuid;
|
|
44
|
+
this.category = init.category;
|
|
45
|
+
this.variationId = init.variationId ?? null;
|
|
46
|
+
this.lastUpdateGlobal = init.lastUpdateGlobal ?? new Date();
|
|
47
|
+
|
|
48
|
+
this.muscles = init.muscles ?? [];
|
|
49
|
+
this.musclesSecondary = init.musclesSecondary ?? [];
|
|
50
|
+
this.images = init.images ?? [];
|
|
51
|
+
this.videos = init.videos ?? [];
|
|
52
|
+
this.equipment = init.equipment ?? [];
|
|
53
|
+
this.authors = init.authors ?? [];
|
|
54
|
+
this.translations = init.translations ?? [];
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
// Returns the users translation or
|
|
57
|
+
// Returns the users' translation or English as a fallback
|
|
47
58
|
//
|
|
48
59
|
// Note that we still check for the case that no english translation can be
|
|
49
60
|
// found. While this can't happen for the "regular" wger server, other local
|
|
@@ -97,25 +108,26 @@ export class ExerciseAdapter implements Adapter<Exercise> {
|
|
|
97
108
|
const translationAdapter = new TranslationAdapter();
|
|
98
109
|
const videoAdapter = new ExerciseVideoAdapter();
|
|
99
110
|
|
|
100
|
-
const exercise = new Exercise(
|
|
101
|
-
item.id,
|
|
102
|
-
item.uuid,
|
|
103
|
-
categoryAdapter.fromJson(item.category),
|
|
111
|
+
const exercise = new Exercise({
|
|
112
|
+
id: item.id,
|
|
113
|
+
uuid: item.uuid,
|
|
114
|
+
category: categoryAdapter.fromJson(item.category),
|
|
104
115
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
-
item.equipment.map((e: any) =>
|
|
116
|
+
equipment: item.equipment.map((e: any) => equipmentAdapter.fromJson(e)),
|
|
106
117
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
107
|
-
item.muscles.map((m: any) =>
|
|
118
|
+
muscles: item.muscles.map((m: any) => muscleAdapter.fromJson(m)),
|
|
108
119
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
-
item.muscles_secondary.map((m: any) =>
|
|
120
|
+
musclesSecondary: item.muscles_secondary.map((m: any) => muscleAdapter.fromJson(m)),
|
|
110
121
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
-
item.images.map((i: any) =>
|
|
112
|
-
item.variations,
|
|
122
|
+
images: item.images.map((i: any) => imageAdapter.fromJson(i)),
|
|
123
|
+
variationId: item.variations,
|
|
113
124
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
-
item.translations.map((t: any) => translationAdapter.fromJson(t)),
|
|
125
|
+
translations: item.translations.map((t: any) => translationAdapter.fromJson(t)),
|
|
115
126
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
-
item.videos.map((t: any) => videoAdapter.fromJson(t)),
|
|
117
|
-
item.
|
|
118
|
-
|
|
127
|
+
videos: item.videos.map((t: any) => videoAdapter.fromJson(t)),
|
|
128
|
+
authors: item.total_authors_history,
|
|
129
|
+
lastUpdateGlobal: new Date(item.last_update_global),
|
|
130
|
+
});
|
|
119
131
|
|
|
120
132
|
if (!exercise.translations.some(t => t.language === ENGLISH_LANGUAGE_ID)) {
|
|
121
133
|
console.info(`No english translation found for exercise base ${exercise.uuid}!`);
|
|
@@ -52,15 +52,17 @@ describe("Test the MeasurementCategoryDetail component", () => {
|
|
|
52
52
|
</QueryClientProvider>
|
|
53
53
|
);
|
|
54
54
|
|
|
55
|
+
screen.logTestingPlaygroundURL();
|
|
56
|
+
|
|
55
57
|
// Assert
|
|
56
58
|
expect(useMeasurementsQuery).toHaveBeenCalled();
|
|
57
59
|
expect(screen.getByText('Biceps')).toBeInTheDocument();
|
|
58
60
|
|
|
59
|
-
expect(screen.
|
|
61
|
+
expect(screen.getByRole('gridcell', { name: /10cm/i })).toBeInTheDocument();
|
|
60
62
|
expect(screen.getByText(/Feb 1, 2023/i)).toBeInTheDocument();
|
|
61
63
|
expect(screen.getByText('test note')).toBeInTheDocument();
|
|
62
64
|
|
|
63
|
-
expect(screen.
|
|
65
|
+
expect(screen.getByRole('gridcell', { name: /20cm/i })).toBeInTheDocument();
|
|
64
66
|
expect(screen.getByText(/Feb 2, 2023/i)).toBeInTheDocument();
|
|
65
67
|
expect(screen.getByText('important note')).toBeInTheDocument();
|
|
66
68
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
2
|
import { MeasurementCategory } from "components/Measurements/models/Category";
|
|
3
3
|
import React from "react";
|
|
4
|
-
import { CartesianGrid, Line, LineChart,
|
|
4
|
+
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
|
5
5
|
import { theme } from "theme";
|
|
6
6
|
import { dateToLocale } from "utils/date";
|
|
7
7
|
|
|
@@ -19,34 +19,31 @@ export const MeasurementChart = (props: { category: MeasurementCategory }) => {
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
return <Box alignItems={'center'} display={'flex'} flexDirection={'column'}>
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{/*<Tooltip content={<CustomTooltip />} />*/}
|
|
49
|
-
</LineChart>
|
|
50
|
-
</ResponsiveContainer>
|
|
22
|
+
<LineChart data={entryData} responsive width="90%" height={200}>
|
|
23
|
+
<Line
|
|
24
|
+
type="monotone"
|
|
25
|
+
dataKey="value"
|
|
26
|
+
stroke={theme.palette.secondary.main}
|
|
27
|
+
strokeWidth={2}
|
|
28
|
+
dot={entryData.length > NR_OF_ENTRIES_CHART_DOT ? false : { strokeWidth: 1, r: 4 }}
|
|
29
|
+
activeDot={{
|
|
30
|
+
stroke: 'black',
|
|
31
|
+
strokeWidth: 1,
|
|
32
|
+
r: 6,
|
|
33
|
+
//onClick: handleClick
|
|
34
|
+
}} />
|
|
35
|
+
<CartesianGrid
|
|
36
|
+
stroke="#ccc"
|
|
37
|
+
strokeDasharray="5 5" />
|
|
38
|
+
<XAxis
|
|
39
|
+
dataKey="date"
|
|
40
|
+
type={'number'}
|
|
41
|
+
domain={['dataMin', 'dataMax']}
|
|
42
|
+
tickFormatter={timeStr => dateToLocale(new Date(timeStr))!}
|
|
43
|
+
tickCount={10}
|
|
44
|
+
/>
|
|
45
|
+
<YAxis domain={['auto', 'auto']} unit={props.category.unit} />
|
|
46
|
+
{/*<Tooltip content={<CustomTooltip />} />*/}
|
|
47
|
+
</LineChart>
|
|
51
48
|
</Box>;
|
|
52
49
|
};
|
|
@@ -6,7 +6,7 @@ import { WgerContainerRightSidebar } from "components/Core/Widgets/Container";
|
|
|
6
6
|
import { useProfileQuery } from "components/User/queries/profile";
|
|
7
7
|
import React, { useEffect, useState } from "react";
|
|
8
8
|
import { useTranslation } from "react-i18next";
|
|
9
|
-
import { Area, AreaChart, CartesianGrid, ReferenceDot,
|
|
9
|
+
import { Area, AreaChart, CartesianGrid, ReferenceDot, Tooltip, XAxis, YAxis, } from "recharts";
|
|
10
10
|
|
|
11
11
|
const bmiRanges = [
|
|
12
12
|
{ range: "obese", color: "#FF5733", min: 30, max: 100 },
|
|
@@ -128,48 +128,46 @@ export const BmiCalculator = () => {
|
|
|
128
128
|
{t('bmi.result', { value: bmi.toFixed(1) })}
|
|
129
129
|
</Typography>
|
|
130
130
|
}
|
|
131
|
-
<
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
131
|
+
<AreaChart data={chartData} responsive width="100%" height={400}>
|
|
132
|
+
<XAxis
|
|
133
|
+
dataKey="height"
|
|
134
|
+
type="number"
|
|
135
|
+
domain={[140, 220]}
|
|
136
|
+
unit="cm"
|
|
137
|
+
/>
|
|
138
|
+
<YAxis
|
|
139
|
+
domain={[40, 150]}
|
|
140
|
+
tickFormatter={(value) => Math.round(value).toString()} // Format as integers
|
|
141
|
+
unit="kg"
|
|
142
|
+
/>
|
|
143
|
+
<CartesianGrid strokeDasharray="3 3" />
|
|
144
|
+
<Tooltip
|
|
145
|
+
// @ts-ignore
|
|
146
|
+
formatter={(value, name) => [Math.round(value as number), t('bmi.' + (name as string))]}
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
{bmiRanges.map((range) => (
|
|
150
|
+
<Area
|
|
151
|
+
key={range.range}
|
|
152
|
+
type="monotone"
|
|
153
|
+
dataKey={range.range}
|
|
154
|
+
stroke={"black"}
|
|
155
|
+
// stroke={range.color}
|
|
156
|
+
fill={range.color}
|
|
157
|
+
fillOpacity={0.8}
|
|
143
158
|
/>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// stroke={range.color}
|
|
157
|
-
fill={range.color}
|
|
158
|
-
fillOpacity={0.8}
|
|
159
|
-
/>
|
|
160
|
-
))}
|
|
161
|
-
|
|
162
|
-
{bmi !== null &&
|
|
163
|
-
<ReferenceDot
|
|
164
|
-
x={height!}
|
|
165
|
-
y={weight!}
|
|
166
|
-
r={8}
|
|
167
|
-
fill="black"
|
|
168
|
-
stroke="none"
|
|
169
|
-
/>}
|
|
170
|
-
|
|
171
|
-
</AreaChart>
|
|
172
|
-
</ResponsiveContainer>
|
|
159
|
+
))}
|
|
160
|
+
|
|
161
|
+
{bmi !== null &&
|
|
162
|
+
<ReferenceDot
|
|
163
|
+
x={height!}
|
|
164
|
+
y={weight!}
|
|
165
|
+
r={8}
|
|
166
|
+
fill="black"
|
|
167
|
+
stroke="none"
|
|
168
|
+
/>}
|
|
169
|
+
|
|
170
|
+
</AreaChart>
|
|
173
171
|
<Stack direction={"row"} justifyContent="center">
|
|
174
172
|
<Box
|
|
175
173
|
height={20}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NutritionalValues } from "components/Nutrition/helpers/nutritionalValues";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { useTranslation } from "react-i18next";
|
|
4
|
-
import { Cell, Legend, Pie, PieChart
|
|
4
|
+
import { Cell, Legend, Pie, PieChart } from 'recharts';
|
|
5
5
|
import { generateChartColors } from "utils/colors";
|
|
6
6
|
import { numberGramLocale } from "utils/numbers";
|
|
7
7
|
|
|
@@ -45,22 +45,20 @@ export const MacrosPieChart = (props: { data: NutritionalValues }) => {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
return <
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
</PieChart>
|
|
65
|
-
</ResponsiveContainer>;
|
|
48
|
+
return <PieChart responsive width={"100%"} height={300}>
|
|
49
|
+
<Pie
|
|
50
|
+
data={data}
|
|
51
|
+
labelLine={false}
|
|
52
|
+
// outerRadius={80}
|
|
53
|
+
label={renderCustomizedLabel}
|
|
54
|
+
fill="#8884d8"
|
|
55
|
+
dataKey="value"
|
|
56
|
+
>
|
|
57
|
+
{data.map((entry, index) => (
|
|
58
|
+
<Cell key={`cell-${index}`} fill={colorGenerator.next().value!} />
|
|
59
|
+
))}
|
|
60
|
+
</Pie>
|
|
61
|
+
{/*<Tooltip />*/}
|
|
62
|
+
<Legend />
|
|
63
|
+
</PieChart>;
|
|
66
64
|
};
|