@wger-project/react-components 25.12.5 → 26.2.26
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/assets/ajax-loader.gif +0 -0
- package/build/assets/index.css +1 -1
- package/build/assets/slick.svg +14 -0
- package/build/locales/ar/translation.json +347 -236
- package/build/locales/cs/translation.json +257 -253
- package/build/locales/de/translation.json +13 -3
- package/build/locales/en/translation.json +4 -1
- package/build/locales/es/translation.json +26 -4
- package/build/locales/fr/translation.json +10 -1
- package/build/locales/hi/translation.json +4 -1
- package/build/locales/hr/translation.json +15 -3
- package/build/locales/mk/translation.json +138 -0
- package/build/locales/nl/translation.json +118 -11
- package/build/locales/pt/translation.json +338 -338
- package/build/locales/pt_BR/translation.json +15 -3
- package/build/locales/ru/translation.json +43 -3
- package/build/locales/ta/translation.json +1 -1
- package/build/locales/uk/translation.json +13 -1
- package/build/locales/zh_Hans/translation.json +25 -1
- package/build/locales/zh_Hant/translation.json +255 -243
- package/build/main.js +170 -170
- package/build/main.js.map +1 -1
- package/package.json +15 -13
- package/src/components/BodyWeight/TableDashboard/TableDashboard.tsx +4 -6
- package/src/components/Calendar/Components/CalendarComponent.test.tsx +18 -22
- package/src/components/Calendar/Components/CalendarComponent.tsx +11 -8
- package/src/components/Calendar/Components/CalendarHeader.tsx +3 -3
- package/src/components/Calendar/Components/Entries.tsx +8 -3
- package/src/components/Dashboard/CalendarCard.tsx +16 -0
- package/src/components/Dashboard/ConfigurableDashboard.test.ts +129 -0
- package/src/components/Dashboard/ConfigurableDashboard.tsx +352 -89
- package/src/components/Dashboard/DashboardCard.tsx +3 -2
- package/src/components/Dashboard/MeasurementCard.test.tsx +75 -0
- package/src/components/Dashboard/MeasurementCard.tsx +101 -0
- package/src/components/Dashboard/RoutineCard.tsx +1 -1
- package/src/components/Dashboard/TrophiesCard.test.tsx +63 -0
- package/src/components/Dashboard/TrophiesCard.tsx +84 -0
- package/src/components/Dashboard/WeightCard.test.tsx +0 -10
- package/src/components/Measurements/Screens/MeasurementCategoryOverview.tsx +1 -1
- package/src/components/Measurements/models/Category.ts +13 -2
- package/src/components/Measurements/models/Entry.ts +13 -2
- package/src/components/Trophies/components/TrophiesDetail.test.tsx +34 -0
- package/src/components/Trophies/components/TrophiesDetail.tsx +88 -0
- package/src/components/Trophies/models/trophy.test.ts +33 -0
- package/src/components/Trophies/models/trophy.ts +75 -0
- package/src/components/Trophies/models/userTrophy.test.ts +38 -0
- package/src/components/Trophies/models/userTrophy.ts +67 -0
- package/src/components/Trophies/models/userTrophyProgression.test.ts +43 -0
- package/src/components/Trophies/models/userTrophyProgression.ts +68 -0
- package/src/components/Trophies/queries/trophies.ts +31 -0
- package/src/components/Trophies/services/trophies.ts +22 -0
- package/src/components/Trophies/services/userTrophies.ts +33 -0
- package/src/components/Trophies/services/userTrophyProgression.ts +16 -0
- package/src/components/WorkoutRoutines/Detail/RoutineDetail.tsx +1 -1
- package/src/components/WorkoutRoutines/Detail/TemplateDetail.tsx +1 -1
- package/src/components/WorkoutRoutines/models/Routine.test.ts +17 -0
- package/src/components/WorkoutRoutines/models/Routine.ts +20 -3
- package/src/components/WorkoutRoutines/widgets/RoutineDetailsCard.tsx +1 -1
- package/src/components/index.ts +0 -2
- package/src/pages/Calendar/index.tsx +2 -2
- package/src/pages/WeightOverview/index.tsx +1 -1
- package/src/routes.tsx +6 -1
- package/src/services/measurements.ts +10 -17
- package/src/tests/trophies/trophiesTestData.ts +80 -0
- package/src/utils/consts.ts +18 -3
- package/src/utils/url.test.ts +32 -1
- package/src/utils/url.ts +24 -3
- package/src/components/Carousel/carousel.module.css +0 -43
- package/src/components/Carousel/carousel.module.css.map +0 -1
- package/src/components/Carousel/carousel.module.scss +0 -46
- package/src/components/Carousel/index.tsx +0 -66
- package/src/components/Dashboard/GoalCard.tsx +0 -71
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import Button from "@mui/material/Button";
|
|
2
|
+
import Table from "@mui/material/Table";
|
|
3
|
+
import TableBody from "@mui/material/TableBody";
|
|
4
|
+
import TableCell from "@mui/material/TableCell";
|
|
5
|
+
import TableHead from "@mui/material/TableHead";
|
|
6
|
+
import TableRow from "@mui/material/TableRow";
|
|
7
|
+
import Typography from "@mui/material/Typography";
|
|
8
|
+
import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget";
|
|
9
|
+
import { DashboardCard } from "components/Dashboard/DashboardCard";
|
|
10
|
+
import { EmptyCard } from "components/Dashboard/EmptyCard";
|
|
11
|
+
import { MeasurementCategory } from "components/Measurements/models/Category";
|
|
12
|
+
import { useMeasurementsCategoryQuery } from "components/Measurements/queries";
|
|
13
|
+
import { CategoryForm } from "components/Measurements/widgets/CategoryForm";
|
|
14
|
+
import { MeasurementChart } from "components/Measurements/widgets/MeasurementChart";
|
|
15
|
+
import i18n from "i18n";
|
|
16
|
+
import React from "react";
|
|
17
|
+
import { useTranslation } from "react-i18next";
|
|
18
|
+
import Slider, { Settings } from "react-slick";
|
|
19
|
+
import { makeLink, WgerLink } from "utils/url";
|
|
20
|
+
import "slick-carousel/slick/slick.css";
|
|
21
|
+
import "slick-carousel/slick/slick-theme.css";
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export const MeasurementCard = () => {
|
|
25
|
+
const { t } = useTranslation();
|
|
26
|
+
const categoryQuery = useMeasurementsCategoryQuery();
|
|
27
|
+
|
|
28
|
+
if (categoryQuery.isLoading) {
|
|
29
|
+
return <LoadingPlaceholder />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return categoryQuery.data === null
|
|
33
|
+
? <EmptyCard
|
|
34
|
+
title={t("measurements.measurements")}
|
|
35
|
+
modalContent={<CategoryForm />}
|
|
36
|
+
modalTitle={t("add")} />
|
|
37
|
+
: <MeasurementCardContent categories={categoryQuery.data!} />;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const MeasurementCardContent = (props: { categories: MeasurementCategory[] }) => {
|
|
41
|
+
const { t } = useTranslation();
|
|
42
|
+
|
|
43
|
+
const settings: Settings = {
|
|
44
|
+
dots: true,
|
|
45
|
+
infinite: true,
|
|
46
|
+
speed: 500,
|
|
47
|
+
slidesToShow: 1,
|
|
48
|
+
slidesToScroll: 1,
|
|
49
|
+
arrows: false,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (<>
|
|
53
|
+
<DashboardCard
|
|
54
|
+
title={t("measurements.measurements")}
|
|
55
|
+
actions={
|
|
56
|
+
<>
|
|
57
|
+
<Button
|
|
58
|
+
size="small"
|
|
59
|
+
href={makeLink(WgerLink.MEASUREMENT_OVERVIEW, i18n.language)}
|
|
60
|
+
>
|
|
61
|
+
{t("seeDetails")}
|
|
62
|
+
</Button>
|
|
63
|
+
</>
|
|
64
|
+
}
|
|
65
|
+
>
|
|
66
|
+
<div className="slider-container">
|
|
67
|
+
<Slider {...settings}>
|
|
68
|
+
{props.categories.map(c => <MeasurementCardTableContent category={c} />)}
|
|
69
|
+
</Slider>
|
|
70
|
+
</div>
|
|
71
|
+
</DashboardCard>
|
|
72
|
+
</>);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
const MeasurementCardTableContent = (props: { category: MeasurementCategory }) => {
|
|
77
|
+
const { t } = useTranslation();
|
|
78
|
+
|
|
79
|
+
return (<>
|
|
80
|
+
<Typography variant="h6" gutterBottom>
|
|
81
|
+
{props.category.name}
|
|
82
|
+
</Typography>
|
|
83
|
+
<MeasurementChart category={props.category} />
|
|
84
|
+
<Table size="small">
|
|
85
|
+
<TableHead>
|
|
86
|
+
<TableRow>
|
|
87
|
+
<TableCell>{t('date')}</TableCell>
|
|
88
|
+
<TableCell>{t('value')}</TableCell>
|
|
89
|
+
</TableRow>
|
|
90
|
+
</TableHead>
|
|
91
|
+
<TableBody>
|
|
92
|
+
{[...props.category.entries].slice(0, 5).map(entry => (
|
|
93
|
+
<TableRow key={`measurement-entry-${entry.id}`}>
|
|
94
|
+
<TableCell>{entry.date.toLocaleDateString()}</TableCell>
|
|
95
|
+
<TableCell>{entry.value} {props.category.unit}</TableCell>
|
|
96
|
+
</TableRow>
|
|
97
|
+
))}
|
|
98
|
+
</TableBody>
|
|
99
|
+
</Table>
|
|
100
|
+
</>);
|
|
101
|
+
};
|
|
@@ -44,7 +44,7 @@ const RoutineCardContent = (props: { routine: Routine }) => {
|
|
|
44
44
|
}
|
|
45
45
|
>
|
|
46
46
|
<List>
|
|
47
|
-
{props.routine.
|
|
47
|
+
{props.routine.dayDataCurrentIterationFiltered.map((dayData) => (
|
|
48
48
|
<DayListItem dayData={dayData} key={`dayDetails-${dayData.date.toISOString()}`} />
|
|
49
49
|
))}
|
|
50
50
|
</List>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { TrophiesCard } from "components/Dashboard/TrophiesCard";
|
|
4
|
+
import { useUserTrophiesQuery } from "components/Trophies/queries/trophies";
|
|
5
|
+
import { testQueryClient } from "tests/queryClient";
|
|
6
|
+
import { testUserTrophies } from "tests/trophies/trophiesTestData";
|
|
7
|
+
|
|
8
|
+
jest.mock("components/Trophies/queries/trophies");
|
|
9
|
+
|
|
10
|
+
describe("test the TrophiesCard component", () => {
|
|
11
|
+
|
|
12
|
+
describe("Trophies available", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
(useUserTrophiesQuery as jest.Mock).mockImplementation(() => ({
|
|
15
|
+
isSuccess: true,
|
|
16
|
+
isLoading: false,
|
|
17
|
+
data: testUserTrophies()
|
|
18
|
+
}));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('renders the trophies correctly', async () => {
|
|
22
|
+
// Act
|
|
23
|
+
render(
|
|
24
|
+
<QueryClientProvider client={testQueryClient}>
|
|
25
|
+
<TrophiesCard />
|
|
26
|
+
</QueryClientProvider>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Assert
|
|
30
|
+
expect(useUserTrophiesQuery).toHaveBeenCalled();
|
|
31
|
+
expect(screen.getByText('Beginner')).toBeInTheDocument();
|
|
32
|
+
expect(screen.getByText('Unstoppable')).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
describe("No trophies available", () => {
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
(useUserTrophiesQuery as jest.Mock).mockImplementation(() => ({
|
|
41
|
+
isSuccess: true,
|
|
42
|
+
isLoading: false,
|
|
43
|
+
data: null
|
|
44
|
+
}));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('correctly shows custom empty card, without call to action button', async () => {
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
render(
|
|
51
|
+
<QueryClientProvider client={testQueryClient}>
|
|
52
|
+
<TrophiesCard />
|
|
53
|
+
</QueryClientProvider>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(useUserTrophiesQuery).toHaveBeenCalled();
|
|
58
|
+
expect(screen.getByText('nothingHereYet')).toBeInTheDocument();
|
|
59
|
+
expect(screen.queryByText('nothingHereYetAction')).not.toBeInTheDocument();
|
|
60
|
+
expect(screen.queryByText('add')).not.toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Button, Card, CardContent, CardHeader, CardMedia, Tooltip, Typography, } from "@mui/material";
|
|
2
|
+
import Box from "@mui/system/Box";
|
|
3
|
+
import Stack from "@mui/system/Stack";
|
|
4
|
+
import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget";
|
|
5
|
+
import { UserTrophy } from "components/Trophies/models/userTrophy";
|
|
6
|
+
import { useUserTrophiesQuery } from "components/Trophies/queries/trophies";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { useTranslation } from "react-i18next";
|
|
9
|
+
import { makeLink, WgerLink } from "utils/url";
|
|
10
|
+
import { DashboardCard } from "./DashboardCard";
|
|
11
|
+
|
|
12
|
+
export const TrophiesCard = () => {
|
|
13
|
+
const trophiesQuery = useUserTrophiesQuery();
|
|
14
|
+
|
|
15
|
+
if (trophiesQuery.isLoading) {
|
|
16
|
+
return <LoadingPlaceholder />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return trophiesQuery.data !== null
|
|
20
|
+
? <TrophiesCardContent trophies={trophiesQuery.data!} />
|
|
21
|
+
: <EmptyTrophiesCardContent />;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function TrophiesCardContent(props: { trophies: UserTrophy[] }) {
|
|
25
|
+
const { t, i18n } = useTranslation();
|
|
26
|
+
|
|
27
|
+
const tooltipWidget = (tooltip: string) => <Typography variant="body2" textAlign={'center'}>
|
|
28
|
+
{tooltip}
|
|
29
|
+
</Typography>;
|
|
30
|
+
|
|
31
|
+
return (<DashboardCard
|
|
32
|
+
title={''}
|
|
33
|
+
scrollable={false}
|
|
34
|
+
actions={
|
|
35
|
+
<>
|
|
36
|
+
<Button
|
|
37
|
+
size="small"
|
|
38
|
+
href={makeLink(WgerLink.TROPHIES, i18n.language)}
|
|
39
|
+
>
|
|
40
|
+
{t("seeDetails")}
|
|
41
|
+
</Button>
|
|
42
|
+
</>
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
<Box sx={{ overflowX: 'auto', width: '100%' }}>
|
|
46
|
+
<Stack direction="row" spacing={3} sx={{ display: 'flex' }}>
|
|
47
|
+
{props.trophies.map((userTrophy) => (
|
|
48
|
+
<Tooltip title={tooltipWidget(userTrophy.trophy.description)} arrow key={userTrophy.trophy.uuid}>
|
|
49
|
+
<Card sx={{ width: 80, flex: '0 0 auto', boxShadow: 'none' }}>
|
|
50
|
+
<CardMedia
|
|
51
|
+
component="img"
|
|
52
|
+
image={userTrophy.trophy.image}
|
|
53
|
+
title={userTrophy.trophy.name}
|
|
54
|
+
/>
|
|
55
|
+
<CardContent>
|
|
56
|
+
<Typography gutterBottom variant="body2" component="div" textAlign="center">
|
|
57
|
+
{userTrophy.trophy.name}
|
|
58
|
+
</Typography>
|
|
59
|
+
</CardContent>
|
|
60
|
+
</Card>
|
|
61
|
+
</Tooltip>
|
|
62
|
+
))}
|
|
63
|
+
</Stack>
|
|
64
|
+
</Box>
|
|
65
|
+
</DashboardCard>);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const EmptyTrophiesCardContent = () => {
|
|
69
|
+
const [t] = useTranslation();
|
|
70
|
+
|
|
71
|
+
return (<>
|
|
72
|
+
<Card sx={{ paddingTop: 0, height: "100%", }}>
|
|
73
|
+
<CardHeader
|
|
74
|
+
title={t("trophies.trophies")}
|
|
75
|
+
sx={{ paddingBottom: 0 }}
|
|
76
|
+
/>
|
|
77
|
+
<CardContent>
|
|
78
|
+
<Typography variant="h6" mr={3}>
|
|
79
|
+
{t('nothingHereYet')}
|
|
80
|
+
</Typography>
|
|
81
|
+
</CardContent>
|
|
82
|
+
</Card>
|
|
83
|
+
</>);
|
|
84
|
+
};
|
|
@@ -6,7 +6,6 @@ import { testQueryClient } from "tests/queryClient";
|
|
|
6
6
|
import { testWeightEntries } from "tests/weight/testData";
|
|
7
7
|
|
|
8
8
|
jest.mock("components/BodyWeight/queries");
|
|
9
|
-
const { ResizeObserver } = window;
|
|
10
9
|
|
|
11
10
|
describe("test the WeightCard component", () => {
|
|
12
11
|
|
|
@@ -17,18 +16,9 @@ describe("test the WeightCard component", () => {
|
|
|
17
16
|
isLoading: false,
|
|
18
17
|
data: testWeightEntries
|
|
19
18
|
}));
|
|
20
|
-
|
|
21
|
-
// @ts-ignore
|
|
22
|
-
delete window.ResizeObserver;
|
|
23
|
-
window.ResizeObserver = jest.fn().mockImplementation(() => ({
|
|
24
|
-
observe: jest.fn(),
|
|
25
|
-
unobserve: jest.fn(),
|
|
26
|
-
disconnect: jest.fn()
|
|
27
|
-
}));
|
|
28
19
|
});
|
|
29
20
|
|
|
30
21
|
afterEach(() => {
|
|
31
|
-
window.ResizeObserver = ResizeObserver;
|
|
32
22
|
jest.restoreAllMocks();
|
|
33
23
|
jest.useRealTimers();
|
|
34
24
|
});
|
|
@@ -15,7 +15,7 @@ import { EntryForm } from "components/Measurements/widgets/EntryForm";
|
|
|
15
15
|
import { WgerModal } from "components/Core/Modals/WgerModal";
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
const CategoryList = (props: { category: MeasurementCategory }) => {
|
|
18
|
+
export const CategoryList = (props: { category: MeasurementCategory }) => {
|
|
19
19
|
|
|
20
20
|
const [t, i18n] = useTranslation();
|
|
21
21
|
const [openModal, setOpenModal] = React.useState(false);
|
|
@@ -15,10 +15,19 @@ export class MeasurementCategory {
|
|
|
15
15
|
this.entries = entries;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
static fromJson(json: any): MeasurementCategory {
|
|
21
|
+
return adapter.fromJson(json);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
toJson() {
|
|
25
|
+
return adapter.toJson(this);
|
|
26
|
+
}
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
class MeasurementCategoryAdapter implements Adapter<MeasurementCategory> {
|
|
22
31
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
32
|
fromJson(item: any) {
|
|
24
33
|
return new MeasurementCategory(
|
|
@@ -35,4 +44,6 @@ export class MeasurementCategoryAdapter implements Adapter<MeasurementCategory>
|
|
|
35
44
|
unit: item.unit,
|
|
36
45
|
};
|
|
37
46
|
}
|
|
38
|
-
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const adapter = new MeasurementCategoryAdapter();
|
|
@@ -10,10 +10,19 @@ export class MeasurementEntry {
|
|
|
10
10
|
public notes: string
|
|
11
11
|
) {
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
static fromJson(json: any): MeasurementEntry {
|
|
16
|
+
return adapter.fromJson(json);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
toJson() {
|
|
20
|
+
return adapter.toJson(this);
|
|
21
|
+
}
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
class MeasurementEntryAdapter implements Adapter<MeasurementEntry> {
|
|
17
26
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
27
|
fromJson(item: any) {
|
|
19
28
|
return new MeasurementEntry(
|
|
@@ -34,4 +43,6 @@ export class MeasurementEntryAdapter implements Adapter<MeasurementEntry> {
|
|
|
34
43
|
notes: item.notes
|
|
35
44
|
};
|
|
36
45
|
}
|
|
37
|
-
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const adapter = new MeasurementEntryAdapter();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { testUserProgressionTrophies } from "tests/trophies/trophiesTestData";
|
|
5
|
+
import { TrophiesDetail } from './TrophiesDetail';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// Mock the trophies query hook
|
|
9
|
+
jest.mock('components/Trophies/queries/trophies', () => ({
|
|
10
|
+
useUserTrophyProgressionQuery: () => ({
|
|
11
|
+
isLoading: false,
|
|
12
|
+
data: testUserProgressionTrophies(),
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('TrophiesDetail', () => {
|
|
17
|
+
test('renders trophy names and progression values', () => {
|
|
18
|
+
|
|
19
|
+
// Act
|
|
20
|
+
render(<TrophiesDetail />);
|
|
21
|
+
|
|
22
|
+
// Assert
|
|
23
|
+
expect(screen.getByText('Beginner')).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText('Unstoppable')).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByText('Complete your first workout')).toBeInTheDocument();
|
|
26
|
+
expect(screen.getByText('Maintain a 30-day workout streak')).toBeInTheDocument();
|
|
27
|
+
|
|
28
|
+
// Progression value for the progressive trophy should be shown
|
|
29
|
+
expect(screen.getByText('4/30')).toBeInTheDocument();
|
|
30
|
+
|
|
31
|
+
// There should be at least one progressbar in the document
|
|
32
|
+
expect(screen.getAllByRole('progressbar').length).toBeGreaterThanOrEqual(1);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Card, CardContent, CardMedia, LinearProgress, LinearProgressProps, Typography } from "@mui/material";
|
|
2
|
+
import Box from "@mui/system/Box";
|
|
3
|
+
import Grid from "@mui/system/Grid";
|
|
4
|
+
import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget";
|
|
5
|
+
import { WgerContainerFullWidth } from "components/Core/Widgets/Container";
|
|
6
|
+
import { UserTrophyProgression } from "components/Trophies/models/userTrophyProgression";
|
|
7
|
+
import { useUserTrophyProgressionQuery } from "components/Trophies/queries/trophies";
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { useTranslation } from "react-i18next";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export const TrophiesDetail = () => {
|
|
13
|
+
const [t] = useTranslation();
|
|
14
|
+
|
|
15
|
+
const planQuery = useUserTrophyProgressionQuery();
|
|
16
|
+
|
|
17
|
+
if (planQuery.isLoading) {
|
|
18
|
+
return <LoadingPlaceholder />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return <WgerContainerFullWidth title={t('trophies.trophies')}>
|
|
22
|
+
<Grid container spacing={2}>
|
|
23
|
+
{planQuery.data!.map((trophyProgression) => (
|
|
24
|
+
<Grid size={{ xs: 12, sm: 4, md: 3, lg: 3, xl: 2 }} key={trophyProgression.trophy.uuid}>
|
|
25
|
+
<TrophyProgressCard key={trophyProgression.trophy.uuid} trophyProgression={trophyProgression} />
|
|
26
|
+
</Grid>
|
|
27
|
+
))}
|
|
28
|
+
</Grid>
|
|
29
|
+
</WgerContainerFullWidth>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
function TrophyProgressCard(props: { trophyProgression: UserTrophyProgression }) {
|
|
34
|
+
return <Card sx={{ height: "100%" }}>
|
|
35
|
+
<CardMedia
|
|
36
|
+
sx={{
|
|
37
|
+
opacity: props.trophyProgression.isEarned ? 1 : 0.3,
|
|
38
|
+
p: 1,
|
|
39
|
+
width: 'auto',
|
|
40
|
+
mx: 'auto',
|
|
41
|
+
maxHeight: 130,
|
|
42
|
+
}}
|
|
43
|
+
component="img"
|
|
44
|
+
image={props.trophyProgression.trophy.image}
|
|
45
|
+
title={props.trophyProgression.trophy.name}
|
|
46
|
+
/>
|
|
47
|
+
<CardContent sx={{ opacity: props.trophyProgression.isEarned ? 1 : 0.6 }}>
|
|
48
|
+
|
|
49
|
+
<Typography gutterBottom variant="h6" component="div" textAlign="center">
|
|
50
|
+
{props.trophyProgression.trophy.name}
|
|
51
|
+
</Typography>
|
|
52
|
+
|
|
53
|
+
<Box sx={{ mb: 2, mt: 0 }}>
|
|
54
|
+
{(props.trophyProgression.trophy.isProgressive && !props.trophyProgression.isEarned) &&
|
|
55
|
+
<LinearProgressWithLabel value={props.trophyProgression.progress} />
|
|
56
|
+
}
|
|
57
|
+
<Typography variant="body2" sx={{ color: "text.secondary", textAlign: 'center' }}>
|
|
58
|
+
{props.trophyProgression.progressDisplay}
|
|
59
|
+
</Typography>
|
|
60
|
+
|
|
61
|
+
</Box>
|
|
62
|
+
|
|
63
|
+
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
|
64
|
+
{props.trophyProgression.trophy.description}
|
|
65
|
+
</Typography>
|
|
66
|
+
|
|
67
|
+
</CardContent>
|
|
68
|
+
</Card>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
const LinearProgressWithLabel = (props: LinearProgressProps & { value: number, progressLabel?: string | null }) => {
|
|
73
|
+
// Extract custom prop so it doesn't get passed to the progress bar via the spread operator
|
|
74
|
+
const { progressLabel, ...linearProps } = props;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
|
78
|
+
<Box sx={{ width: '100%', mr: 1 }}>
|
|
79
|
+
<LinearProgress variant="determinate" {...linearProps} />
|
|
80
|
+
</Box>
|
|
81
|
+
<Box sx={{ minWidth: 35 }}>
|
|
82
|
+
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
83
|
+
{progressLabel ?? `${Math.round(props.value)}%`}
|
|
84
|
+
</Typography>
|
|
85
|
+
</Box>
|
|
86
|
+
</Box>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Trophy } from "components/Trophies/models/trophy";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
describe('Test the trophy model', () => {
|
|
5
|
+
|
|
6
|
+
test('correctly creates an object from the API response', () => {
|
|
7
|
+
// Arrange
|
|
8
|
+
const apiResponse = {
|
|
9
|
+
id: 1,
|
|
10
|
+
uuid: "5362e55b-eaf1-4e34-9ef8-661538a3bdd9",
|
|
11
|
+
name: "Beginner",
|
|
12
|
+
description: "Complete your first workout",
|
|
13
|
+
image: "http://localhost:8000/static/trophies/count/5362e55b-eaf1-4e34-9ef8-661538a3bdd9.png",
|
|
14
|
+
"trophy_type": "count",
|
|
15
|
+
"is_hidden": false,
|
|
16
|
+
"is_progressive": false,
|
|
17
|
+
order: 1
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Act
|
|
21
|
+
const trophy = Trophy.fromJson(apiResponse);
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
24
|
+
expect(trophy.id).toBe(1);
|
|
25
|
+
expect(trophy.uuid).toBe("5362e55b-eaf1-4e34-9ef8-661538a3bdd9");
|
|
26
|
+
expect(trophy.name).toBe("Beginner");
|
|
27
|
+
expect(trophy.description).toBe("Complete your first workout");
|
|
28
|
+
expect(trophy.image).toBe("http://localhost:8000/static/trophies/count/5362e55b-eaf1-4e34-9ef8-661538a3bdd9.png");
|
|
29
|
+
expect(trophy.type).toBe("count");
|
|
30
|
+
expect(trophy.isHidden).toBe(false);
|
|
31
|
+
expect(trophy.isProgressive).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Adapter } from "utils/Adapter";
|
|
2
|
+
|
|
3
|
+
export type trophyType = 'time' | 'volume' | 'count' | 'sequence' | 'date' | 'pr' | 'other';
|
|
4
|
+
|
|
5
|
+
export interface ApiTrophyType {
|
|
6
|
+
id: number,
|
|
7
|
+
uuid: string,
|
|
8
|
+
name: string,
|
|
9
|
+
description: string,
|
|
10
|
+
image: string,
|
|
11
|
+
"trophy_type": trophyType,
|
|
12
|
+
"is_hidden": boolean,
|
|
13
|
+
"is_progressive": boolean,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TrophyConstructorParams = {
|
|
17
|
+
id: number;
|
|
18
|
+
uuid: string;
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
image: string;
|
|
22
|
+
type: trophyType;
|
|
23
|
+
isHidden: boolean;
|
|
24
|
+
isProgressive: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class Trophy {
|
|
28
|
+
|
|
29
|
+
public id: number;
|
|
30
|
+
public uuid: string;
|
|
31
|
+
public name: string;
|
|
32
|
+
public description: string;
|
|
33
|
+
public image: string;
|
|
34
|
+
public type: string;
|
|
35
|
+
public isHidden: boolean;
|
|
36
|
+
public isProgressive: boolean;
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
constructor(params: TrophyConstructorParams) {
|
|
40
|
+
this.id = params.id;
|
|
41
|
+
this.uuid = params.uuid;
|
|
42
|
+
this.name = params.name;
|
|
43
|
+
this.description = params.description;
|
|
44
|
+
this.image = params.image;
|
|
45
|
+
this.type = params.type;
|
|
46
|
+
this.isHidden = params.isHidden;
|
|
47
|
+
this.isProgressive = params.isProgressive;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static fromJson(json: ApiTrophyType): Trophy {
|
|
51
|
+
return adapter.fromJson(json);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class TrophyAdapter implements Adapter<Trophy> {
|
|
56
|
+
fromJson(item: ApiTrophyType) {
|
|
57
|
+
return new Trophy({
|
|
58
|
+
id: item.id,
|
|
59
|
+
uuid: item.uuid,
|
|
60
|
+
name: item.name,
|
|
61
|
+
description: item.description,
|
|
62
|
+
image: item.image,
|
|
63
|
+
type: item.trophy_type,
|
|
64
|
+
isHidden: item.is_hidden,
|
|
65
|
+
isProgressive: item.is_progressive,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
toJson(_: Trophy) {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
const adapter = new TrophyAdapter();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { UserTrophy } from "components/Trophies/models/userTrophy";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
describe('Test the user UserTrophy model', () => {
|
|
5
|
+
|
|
6
|
+
test('correctly creates an object from the API response', () => {
|
|
7
|
+
// Arrange
|
|
8
|
+
const apiResponse = {
|
|
9
|
+
id: 4,
|
|
10
|
+
trophy: {
|
|
11
|
+
id: 9,
|
|
12
|
+
uuid: "32bb12da-b25f-4e18-81e4-b695eb65283e",
|
|
13
|
+
name: "Phoenix",
|
|
14
|
+
description: "Return to training after being inactive for 30 days",
|
|
15
|
+
image: "http://localhost:8000/static/trophies/other/32bb12da-b25f-4e18-81e4-b695eb65283e.png",
|
|
16
|
+
"trophy_type": "other",
|
|
17
|
+
"is_hidden": true,
|
|
18
|
+
"is_progressive": false,
|
|
19
|
+
order: 9
|
|
20
|
+
},
|
|
21
|
+
"earned_at": "2025-12-19T13:48:07.519497+01:00",
|
|
22
|
+
progress: 100.0,
|
|
23
|
+
"is_notified": false
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Act
|
|
27
|
+
const userTrophy = UserTrophy.fromJson(apiResponse);
|
|
28
|
+
|
|
29
|
+
// Assert
|
|
30
|
+
expect(userTrophy.id).toBe(4);
|
|
31
|
+
expect(userTrophy.earnedAt).toStrictEqual(new Date("2025-12-19T13:48:07.519497+01:00"));
|
|
32
|
+
expect(userTrophy.progress).toBe(100.0);
|
|
33
|
+
expect(userTrophy.isNotified).toBe(false);
|
|
34
|
+
|
|
35
|
+
expect(userTrophy.trophy.uuid).toBe("32bb12da-b25f-4e18-81e4-b695eb65283e");
|
|
36
|
+
|
|
37
|
+
});
|
|
38
|
+
});
|