@wger-project/react-components 25.11.22 → 26.1.18
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/de/translation.json +25 -6
- package/build/locales/en/translation.json +12 -0
- package/build/locales/es/translation.json +22 -3
- package/build/locales/fr/translation.json +10 -1
- package/build/locales/hi/translation.json +8 -0
- package/build/locales/nl/translation.json +350 -239
- package/build/locales/pt_BR/translation.json +349 -255
- package/build/locales/ru/translation.json +39 -3
- package/build/locales/uk/translation.json +10 -1
- package/build/main.js +170 -166
- package/build/main.js.map +1 -1
- package/package.json +8 -2
- 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 +128 -0
- package/src/components/Dashboard/ConfigurableDashboard.tsx +479 -0
- package/src/components/Dashboard/DashboardCard.tsx +122 -0
- package/src/components/Dashboard/EmptyCard.tsx +3 -3
- package/src/components/Dashboard/MeasurementCard.test.tsx +75 -0
- package/src/components/Dashboard/MeasurementCard.tsx +101 -0
- package/src/components/Dashboard/NutritionCard.tsx +88 -96
- package/src/components/Dashboard/RoutineCard.tsx +54 -69
- 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/Dashboard/WeightCard.tsx +36 -42
- package/src/components/Exercises/Detail/Head/ExerciseDeleteDialog.tsx +1 -1
- 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/WorkoutStats.tsx +1 -1
- package/src/components/WorkoutRoutines/widgets/forms/DayTypeSelect.tsx +1 -2
- package/src/components/WorkoutRoutines/widgets/forms/SlotForm.tsx +0 -4
- package/src/components/index.ts +0 -2
- package/src/index.tsx +0 -46
- package/src/pages/Calendar/index.tsx +2 -2
- package/src/pages/WeightOverview/index.tsx +1 -1
- package/src/routes.tsx +87 -79
- package/src/services/exerciseTranslation.ts +5 -6
- package/src/services/measurements.ts +10 -17
- package/src/services/video.test.ts +4 -4
- 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/Dashboard.tsx +0 -22
|
@@ -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
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import AddIcon from "@mui/icons-material/Add";
|
|
2
|
-
import { Box, Button,
|
|
2
|
+
import { Box, Button, IconButton } from "@mui/material";
|
|
3
3
|
import Tooltip from "@mui/material/Tooltip";
|
|
4
4
|
import { WeightForm } from "components/BodyWeight/Form/WeightForm";
|
|
5
5
|
import { WeightEntry } from "components/BodyWeight/model";
|
|
@@ -9,64 +9,58 @@ import { WeightChart } from "components/BodyWeight/WeightChart";
|
|
|
9
9
|
import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget";
|
|
10
10
|
import { WgerModal } from "components/Core/Modals/WgerModal";
|
|
11
11
|
import { EmptyCard } from "components/Dashboard/EmptyCard";
|
|
12
|
-
import React from
|
|
12
|
+
import React from "react";
|
|
13
13
|
import { useTranslation } from "react-i18next";
|
|
14
14
|
import { makeLink, WgerLink } from "utils/url";
|
|
15
|
+
import { DashboardCard } from "./DashboardCard";
|
|
15
16
|
|
|
16
17
|
export const WeightCard = () => {
|
|
17
|
-
|
|
18
18
|
const [t] = useTranslation();
|
|
19
|
-
const weightyQuery = useBodyWeightQuery(
|
|
19
|
+
const weightyQuery = useBodyWeightQuery("lastYear");
|
|
20
20
|
|
|
21
21
|
if (weightyQuery.isLoading) {
|
|
22
22
|
return <LoadingPlaceholder />;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
return weightyQuery.data?.length !== undefined && weightyQuery.data?.length > 0
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/>;
|
|
25
|
+
return weightyQuery.data?.length !== undefined && weightyQuery.data?.length > 0 ? (
|
|
26
|
+
<WeightCardContent entries={weightyQuery.data} />
|
|
27
|
+
) : (
|
|
28
|
+
<EmptyCard title={t("weight")} modalContent={<WeightForm />} />
|
|
29
|
+
);
|
|
31
30
|
};
|
|
32
31
|
export const WeightCardContent = (props: { entries: WeightEntry[] }) => {
|
|
33
|
-
|
|
34
32
|
const [openModal, setOpenModal] = React.useState(false);
|
|
35
33
|
const handleOpenModal = () => setOpenModal(true);
|
|
36
34
|
const handleCloseModal = () => setOpenModal(false);
|
|
37
35
|
const [t, i18n] = useTranslation();
|
|
38
36
|
|
|
39
|
-
return (
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
title={t(
|
|
43
|
-
subheader={
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<DashboardCard
|
|
40
|
+
title={t("weight")}
|
|
41
|
+
subheader={"."}
|
|
42
|
+
actions={
|
|
43
|
+
<>
|
|
44
|
+
<Button size="small" href={makeLink(WgerLink.WEIGHT_OVERVIEW, i18n.language)}>
|
|
45
|
+
{t("seeDetails")}
|
|
46
|
+
</Button>
|
|
47
|
+
<Tooltip title={t("addEntry")}>
|
|
48
|
+
<IconButton onClick={handleOpenModal}>
|
|
49
|
+
<AddIcon />
|
|
50
|
+
</IconButton>
|
|
51
|
+
</Tooltip>
|
|
52
|
+
</>
|
|
53
|
+
}
|
|
54
|
+
>
|
|
46
55
|
<WeightChart weights={props.entries} height={200} />
|
|
47
|
-
<Box sx={{ mt: 2
|
|
56
|
+
<Box sx={{ mt: 2 }}>
|
|
48
57
|
<WeightTableDashboard weights={props.entries} />
|
|
49
58
|
</Box>
|
|
50
|
-
</
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
>
|
|
59
|
-
{t('seeDetails')}
|
|
60
|
-
</Button>
|
|
61
|
-
<Tooltip title={t('addEntry')}>
|
|
62
|
-
<IconButton onClick={handleOpenModal}>
|
|
63
|
-
<AddIcon />
|
|
64
|
-
</IconButton>
|
|
65
|
-
</Tooltip>
|
|
66
|
-
</CardActions>
|
|
67
|
-
</Card>
|
|
68
|
-
<WgerModal title={t('add')} isOpen={openModal} closeFn={handleCloseModal}>
|
|
69
|
-
<WeightForm closeFn={handleCloseModal} />
|
|
70
|
-
</WgerModal>
|
|
71
|
-
</>);
|
|
72
|
-
};
|
|
59
|
+
</DashboardCard>
|
|
60
|
+
|
|
61
|
+
<WgerModal title={t("add")} isOpen={openModal} closeFn={handleCloseModal}>
|
|
62
|
+
<WeightForm closeFn={handleCloseModal} />
|
|
63
|
+
</WgerModal>
|
|
64
|
+
</>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ApiTrophyType, Trophy } from "components/Trophies/models/trophy";
|
|
2
|
+
import { Adapter } from "utils/Adapter";
|
|
3
|
+
|
|
4
|
+
export interface ApiUserTrophyType {
|
|
5
|
+
id: number,
|
|
6
|
+
trophy: ApiTrophyType,
|
|
7
|
+
earned_at: string,
|
|
8
|
+
progress: number,
|
|
9
|
+
"is_notified": boolean,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type UserTrophyConstructorParams = {
|
|
13
|
+
id: number;
|
|
14
|
+
trophy: Trophy;
|
|
15
|
+
earnedAt: Date;
|
|
16
|
+
progress: number;
|
|
17
|
+
isNotified: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* A list of trophies earned by a user, along with their progress.
|
|
22
|
+
*/
|
|
23
|
+
export class UserTrophy {
|
|
24
|
+
|
|
25
|
+
public id: number;
|
|
26
|
+
public trophy: Trophy;
|
|
27
|
+
public earnedAt: Date;
|
|
28
|
+
public progress: number;
|
|
29
|
+
public isNotified: boolean;
|
|
30
|
+
|
|
31
|
+
constructor(params: UserTrophyConstructorParams) {
|
|
32
|
+
this.id = params.id;
|
|
33
|
+
this.trophy = params.trophy;
|
|
34
|
+
this.earnedAt = params.earnedAt;
|
|
35
|
+
this.progress = params.progress;
|
|
36
|
+
this.isNotified = params.isNotified;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static fromJson(json: ApiUserTrophyType): UserTrophy {
|
|
40
|
+
return adapter.fromJson(json);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
toJson() {
|
|
44
|
+
return adapter.toJson(this);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class UserTrophyAdapter implements Adapter<UserTrophy> {
|
|
49
|
+
fromJson(item: ApiUserTrophyType) {
|
|
50
|
+
return new UserTrophy({
|
|
51
|
+
id: item.id,
|
|
52
|
+
trophy: Trophy.fromJson(item.trophy),
|
|
53
|
+
earnedAt: new Date(item.earned_at),
|
|
54
|
+
progress: item.progress,
|
|
55
|
+
isNotified: item.is_notified,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
toJson(trophy: UserTrophy) {
|
|
60
|
+
return {
|
|
61
|
+
id: trophy.id,
|
|
62
|
+
"is_notified": trophy.isNotified,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const adapter = new UserTrophyAdapter();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { trophyType } from "components/Trophies/models/trophy";
|
|
2
|
+
import { UserTrophyProgression } from "components/Trophies/models/userTrophyProgression";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('Test the UserTrophyProgression model', () => {
|
|
6
|
+
|
|
7
|
+
test('correctly creates an object from the API response', () => {
|
|
8
|
+
// Arrange
|
|
9
|
+
const apiResponse = {
|
|
10
|
+
trophy: {
|
|
11
|
+
id: 1,
|
|
12
|
+
uuid: "5362e55b-eaf1-4e34-9ef8-661538a3bdd9",
|
|
13
|
+
name: "Beginner",
|
|
14
|
+
description: "Complete your first workout",
|
|
15
|
+
image: "http://localhost:8000/static/trophies/count/5362e55b-eaf1-4e34-9ef8-661538a3bdd9.png",
|
|
16
|
+
"trophy_type": "count" as trophyType,
|
|
17
|
+
"is_hidden": false,
|
|
18
|
+
"is_progressive": false,
|
|
19
|
+
order: 1
|
|
20
|
+
},
|
|
21
|
+
"is_earned": true,
|
|
22
|
+
"earned_at": "2025-12-19T13:48:07.513138+01:00",
|
|
23
|
+
progress: 100.0,
|
|
24
|
+
"current_value": null,
|
|
25
|
+
"target_value": null,
|
|
26
|
+
"progress_display": null
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Act
|
|
30
|
+
const userTrophyProgression = UserTrophyProgression.fromJson(apiResponse);
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect(userTrophyProgression.isEarned).toBe(true);
|
|
34
|
+
expect(userTrophyProgression.earnedAt).toStrictEqual(new Date("2025-12-19T13:48:07.513138+01:00"));
|
|
35
|
+
expect(userTrophyProgression.progress).toBe(100.0);
|
|
36
|
+
expect(userTrophyProgression.currentValue).toBe(null);
|
|
37
|
+
expect(userTrophyProgression.targetValue).toBe(null);
|
|
38
|
+
expect(userTrophyProgression.progressDisplay).toBe(null);
|
|
39
|
+
|
|
40
|
+
expect(userTrophyProgression.trophy.uuid).toBe("5362e55b-eaf1-4e34-9ef8-661538a3bdd9");
|
|
41
|
+
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ApiTrophyType, Trophy } from "components/Trophies/models/trophy";
|
|
2
|
+
import { Adapter } from "utils/Adapter";
|
|
3
|
+
|
|
4
|
+
export interface ApiUserTrophyType {
|
|
5
|
+
trophy: ApiTrophyType,
|
|
6
|
+
is_earned: boolean
|
|
7
|
+
earned_at: string | null,
|
|
8
|
+
progress: number,
|
|
9
|
+
current_value: number | null,
|
|
10
|
+
target_value: number | null,
|
|
11
|
+
progress_display: string | null,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type UserTrophyConstructorParams = {
|
|
15
|
+
trophy: Trophy;
|
|
16
|
+
isEarned: boolean;
|
|
17
|
+
earnedAt: Date | null;
|
|
18
|
+
progress: number;
|
|
19
|
+
currentValue: number | null;
|
|
20
|
+
targetValue: number | null;
|
|
21
|
+
progressDisplay: string | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class UserTrophyProgression {
|
|
25
|
+
|
|
26
|
+
public trophy: Trophy;
|
|
27
|
+
public earnedAt: Date | null;
|
|
28
|
+
public isEarned: boolean;
|
|
29
|
+
public progress: number;
|
|
30
|
+
public currentValue: number | null;
|
|
31
|
+
public targetValue: number | null;
|
|
32
|
+
public progressDisplay: string | null;
|
|
33
|
+
|
|
34
|
+
constructor(params: UserTrophyConstructorParams) {
|
|
35
|
+
this.trophy = params.trophy;
|
|
36
|
+
this.earnedAt = params.earnedAt;
|
|
37
|
+
this.isEarned = params.isEarned;
|
|
38
|
+
this.progress = params.progress;
|
|
39
|
+
this.currentValue = params.currentValue;
|
|
40
|
+
this.targetValue = params.targetValue;
|
|
41
|
+
this.progressDisplay = params.progressDisplay;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static fromJson(json: ApiUserTrophyType): UserTrophyProgression {
|
|
45
|
+
return adapter.fromJson(json);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class UserTrophyAdapter implements Adapter<UserTrophyProgression> {
|
|
50
|
+
fromJson(item: ApiUserTrophyType) {
|
|
51
|
+
return new UserTrophyProgression({
|
|
52
|
+
trophy: Trophy.fromJson(item.trophy),
|
|
53
|
+
earnedAt: item.earned_at !== null ? new Date(item.earned_at) : null,
|
|
54
|
+
isEarned: item.is_earned,
|
|
55
|
+
progress: item.progress,
|
|
56
|
+
currentValue: item.current_value,
|
|
57
|
+
targetValue: item.target_value,
|
|
58
|
+
progressDisplay: item.progress_display,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
toJson(item: UserTrophyProgression) {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
const adapter = new UserTrophyAdapter();
|