@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.
Files changed (72) hide show
  1. package/build/assets/ajax-loader.gif +0 -0
  2. package/build/assets/index.css +1 -1
  3. package/build/assets/slick.svg +14 -0
  4. package/build/locales/ar/translation.json +347 -236
  5. package/build/locales/cs/translation.json +257 -253
  6. package/build/locales/de/translation.json +13 -3
  7. package/build/locales/en/translation.json +4 -1
  8. package/build/locales/es/translation.json +26 -4
  9. package/build/locales/fr/translation.json +10 -1
  10. package/build/locales/hi/translation.json +4 -1
  11. package/build/locales/hr/translation.json +15 -3
  12. package/build/locales/mk/translation.json +138 -0
  13. package/build/locales/nl/translation.json +118 -11
  14. package/build/locales/pt/translation.json +338 -338
  15. package/build/locales/pt_BR/translation.json +15 -3
  16. package/build/locales/ru/translation.json +43 -3
  17. package/build/locales/ta/translation.json +1 -1
  18. package/build/locales/uk/translation.json +13 -1
  19. package/build/locales/zh_Hans/translation.json +25 -1
  20. package/build/locales/zh_Hant/translation.json +255 -243
  21. package/build/main.js +170 -170
  22. package/build/main.js.map +1 -1
  23. package/package.json +15 -13
  24. package/src/components/BodyWeight/TableDashboard/TableDashboard.tsx +4 -6
  25. package/src/components/Calendar/Components/CalendarComponent.test.tsx +18 -22
  26. package/src/components/Calendar/Components/CalendarComponent.tsx +11 -8
  27. package/src/components/Calendar/Components/CalendarHeader.tsx +3 -3
  28. package/src/components/Calendar/Components/Entries.tsx +8 -3
  29. package/src/components/Dashboard/CalendarCard.tsx +16 -0
  30. package/src/components/Dashboard/ConfigurableDashboard.test.ts +129 -0
  31. package/src/components/Dashboard/ConfigurableDashboard.tsx +352 -89
  32. package/src/components/Dashboard/DashboardCard.tsx +3 -2
  33. package/src/components/Dashboard/MeasurementCard.test.tsx +75 -0
  34. package/src/components/Dashboard/MeasurementCard.tsx +101 -0
  35. package/src/components/Dashboard/RoutineCard.tsx +1 -1
  36. package/src/components/Dashboard/TrophiesCard.test.tsx +63 -0
  37. package/src/components/Dashboard/TrophiesCard.tsx +84 -0
  38. package/src/components/Dashboard/WeightCard.test.tsx +0 -10
  39. package/src/components/Measurements/Screens/MeasurementCategoryOverview.tsx +1 -1
  40. package/src/components/Measurements/models/Category.ts +13 -2
  41. package/src/components/Measurements/models/Entry.ts +13 -2
  42. package/src/components/Trophies/components/TrophiesDetail.test.tsx +34 -0
  43. package/src/components/Trophies/components/TrophiesDetail.tsx +88 -0
  44. package/src/components/Trophies/models/trophy.test.ts +33 -0
  45. package/src/components/Trophies/models/trophy.ts +75 -0
  46. package/src/components/Trophies/models/userTrophy.test.ts +38 -0
  47. package/src/components/Trophies/models/userTrophy.ts +67 -0
  48. package/src/components/Trophies/models/userTrophyProgression.test.ts +43 -0
  49. package/src/components/Trophies/models/userTrophyProgression.ts +68 -0
  50. package/src/components/Trophies/queries/trophies.ts +31 -0
  51. package/src/components/Trophies/services/trophies.ts +22 -0
  52. package/src/components/Trophies/services/userTrophies.ts +33 -0
  53. package/src/components/Trophies/services/userTrophyProgression.ts +16 -0
  54. package/src/components/WorkoutRoutines/Detail/RoutineDetail.tsx +1 -1
  55. package/src/components/WorkoutRoutines/Detail/TemplateDetail.tsx +1 -1
  56. package/src/components/WorkoutRoutines/models/Routine.test.ts +17 -0
  57. package/src/components/WorkoutRoutines/models/Routine.ts +20 -3
  58. package/src/components/WorkoutRoutines/widgets/RoutineDetailsCard.tsx +1 -1
  59. package/src/components/index.ts +0 -2
  60. package/src/pages/Calendar/index.tsx +2 -2
  61. package/src/pages/WeightOverview/index.tsx +1 -1
  62. package/src/routes.tsx +6 -1
  63. package/src/services/measurements.ts +10 -17
  64. package/src/tests/trophies/trophiesTestData.ts +80 -0
  65. package/src/utils/consts.ts +18 -3
  66. package/src/utils/url.test.ts +32 -1
  67. package/src/utils/url.ts +24 -3
  68. package/src/components/Carousel/carousel.module.css +0 -43
  69. package/src/components/Carousel/carousel.module.css.map +0 -1
  70. package/src/components/Carousel/carousel.module.scss +0 -46
  71. package/src/components/Carousel/index.tsx +0 -66
  72. 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.dayDataCurrentIterationNoNulls.map((dayData) => (
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
- export class MeasurementCategoryAdapter implements Adapter<MeasurementCategory> {
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
- export class MeasurementEntryAdapter implements Adapter<MeasurementEntry> {
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
+ });