Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"start": "Start",
"end": "End",
"comment": "Comment",
"trophies": {
"trophies": "Trophies"
},
"licenses": {
"authors": "Author(s)",
"authorProfile": "Link to author website or profile, if available",
Expand Down
10 changes: 9 additions & 1 deletion src/components/Dashboard/ConfigurableDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CalendarCard } from "components/Dashboard/CalendarCard";
import { MeasurementCard } from "components/Dashboard/MeasurementCard";
import { NutritionCard } from "components/Dashboard/NutritionCard";
import { RoutineCard } from "components/Dashboard/RoutineCard";
import { TrophiesCard } from "components/Dashboard/TrophiesCard";
import { WeightCard } from "components/Dashboard/WeightCard";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Layout, Layouts, Responsive, WidthProvider } from "react-grid-layout";
Expand All @@ -30,7 +31,7 @@ type DashboardState = {
const BREAKPOINTS = ['lg', 'md', 'sm', 'xs'] as const;

// Define widget types for extensibility
export type WidgetType = "routine" | "nutrition" | "weight" | "calendar" | "measurement";
export type WidgetType = "routine" | "nutrition" | "weight" | "calendar" | "measurement" | "trophies";

export interface WidgetConfig {
id: string;
Expand Down Expand Up @@ -84,6 +85,13 @@ export const AVAILABLE_WIDGETS: WidgetConfig[] = [
translationKey: 'measurements.measurements',
defaultLayout: { w: 4, h: 4, x: 8, y: 1, minW: 3, minH: 2 },
},
{
id: "trophies",
type: "trophies",
component: TrophiesCard,
translationKey: 'trophies.trophies',
defaultLayout: { w: 12, h: 2, x: 0, y: 2, minW: 3, minH: 2 },
},
];

/*
Expand Down
23 changes: 11 additions & 12 deletions src/components/Dashboard/DashboardCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,27 +74,26 @@ export interface DashboardCardProps {
*/
export const DashboardCard: React.FC<DashboardCardProps> = (
{
title,
subheader,
children,
actions,
headerAction,
contentHeight,
scrollable = true,
cardSx = {},
contentSx = {},
}) => {
title,
subheader,
children,
actions,
headerAction,
contentHeight,
scrollable = true,
cardSx = {},
contentSx = {},
}) => {
return (
<Card

sx={{
height: "100%",
display: "flex",
flexDirection: "column",
...cardSx,
}}
>
<CardHeader title={title} subheader={subheader} action={headerAction} />
{title !== '' && <CardHeader title={title} subheader={subheader} action={headerAction} />}

<CardContent
sx={{
Expand Down
1 change: 1 addition & 0 deletions src/components/Dashboard/MeasurementCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe("smoke test the MeasurementCard component", () => {

// Assert
expect(useMeasurementsCategoryQuery).toHaveBeenCalled();
expect(screen.getAllByText('Biceps').length).toBeGreaterThan(0);
expect(screen.getAllByText('11 %').length).toBeGreaterThan(0);
expect(screen.getAllByText('22 %').length).toBeGreaterThan(0);
expect(screen.getAllByText('33 %').length).toBeGreaterThan(0);
Expand Down
4 changes: 4 additions & 0 deletions src/components/Dashboard/MeasurementCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget";
import { DashboardCard } from "components/Dashboard/DashboardCard";
import { EmptyCard } from "components/Dashboard/EmptyCard";
Expand Down Expand Up @@ -76,6 +77,9 @@ const MeasurementCardTableContent = (props: { category: MeasurementCategory }) =
const { t } = useTranslation();

return (<>
<Typography variant="h6" gutterBottom>
{props.category.name}
</Typography>
<MeasurementChart category={props.category} />
<Table size="small">
<TableHead>
Expand Down
63 changes: 63 additions & 0 deletions src/components/Dashboard/TrophiesCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { render, screen } from '@testing-library/react';
import { TrophiesCard } from "components/Dashboard/TrophiesCard";
import { useUserTrophiesQuery } from "components/Trophies/queries/trophies";
import { testQueryClient } from "tests/queryClient";
import { testUserTrophies } from "tests/trophies/trophiesTestData";

jest.mock("components/Trophies/queries/trophies");

describe("test the TrophiesCard component", () => {

describe("Trophies available", () => {
beforeEach(() => {
(useUserTrophiesQuery as jest.Mock).mockImplementation(() => ({
isSuccess: true,
isLoading: false,
data: testUserTrophies()
}));
});

test('renders the trophies correctly', async () => {
// Act
render(
<QueryClientProvider client={testQueryClient}>
<TrophiesCard />
</QueryClientProvider>
);

// Assert
expect(useUserTrophiesQuery).toHaveBeenCalled();
expect(screen.getByText('Beginner')).toBeInTheDocument();
expect(screen.getByText('Unstoppable')).toBeInTheDocument();
});
});


describe("No trophies available", () => {

beforeEach(() => {
(useUserTrophiesQuery as jest.Mock).mockImplementation(() => ({
isSuccess: true,
isLoading: false,
data: null
}));
});

test('correctly shows custom empty card, without call to action button', async () => {

// Act
render(
<QueryClientProvider client={testQueryClient}>
<TrophiesCard />
</QueryClientProvider>
);

// Assert
expect(useUserTrophiesQuery).toHaveBeenCalled();
expect(screen.getByText('nothingHereYet')).toBeInTheDocument();
expect(screen.queryByText('nothingHereYetAction')).not.toBeInTheDocument();
expect(screen.queryByText('add')).not.toBeInTheDocument();
});
});
});
84 changes: 84 additions & 0 deletions src/components/Dashboard/TrophiesCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Button, Card, CardContent, CardHeader, CardMedia, Tooltip, Typography, } from "@mui/material";
import Box from "@mui/system/Box";
import Stack from "@mui/system/Stack";
import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget";
import { UserTrophy } from "components/Trophies/models/userTrophy";
import { useUserTrophiesQuery } from "components/Trophies/queries/trophies";
import React from "react";
import { useTranslation } from "react-i18next";
import { makeLink, WgerLink } from "utils/url";
import { DashboardCard } from "./DashboardCard";

export const TrophiesCard = () => {
const trophiesQuery = useUserTrophiesQuery();

if (trophiesQuery.isLoading) {
return <LoadingPlaceholder />;
}

return trophiesQuery.data !== null
? <TrophiesCardContent trophies={trophiesQuery.data!} />
: <EmptyTrophiesCardContent />;
};

function TrophiesCardContent(props: { trophies: UserTrophy[] }) {
const { t, i18n } = useTranslation();

const tooltipWidget = (tooltip: string) => <Typography variant="body2" textAlign={'center'}>
{tooltip}
</Typography>;

return (<DashboardCard
title={''}
scrollable={false}
actions={
<>
<Button
size="small"
href={makeLink(WgerLink.TROPHIES, i18n.language)}
>
{t("seeDetails")}
</Button>
</>
}
>
<Box sx={{ overflowX: 'auto', width: '100%' }}>
<Stack direction="row" spacing={3} sx={{ display: 'flex' }}>
{props.trophies.map((userTrophy) => (
<Tooltip title={tooltipWidget(userTrophy.trophy.description)} arrow key={userTrophy.trophy.uuid}>
<Card sx={{ width: 80, flex: '0 0 auto', boxShadow: 'none' }}>
<CardMedia
component="img"
image={userTrophy.trophy.image}
title={userTrophy.trophy.name}
/>
<CardContent>
<Typography gutterBottom variant="body2" component="div" textAlign="center">
{userTrophy.trophy.name}
</Typography>
</CardContent>
</Card>
</Tooltip>
))}
</Stack>
</Box>
</DashboardCard>);
}

export const EmptyTrophiesCardContent = () => {
const [t] = useTranslation();

return (<>
<Card sx={{ paddingTop: 0, height: "100%", }}>
<CardHeader
title={t("trophies.trophies")}
sx={{ paddingBottom: 0 }}
/>
<CardContent>
<Typography variant="h6" mr={3}>
{t('nothingHereYet')}
</Typography>
</CardContent>
</Card>
</>);
};
10 changes: 0 additions & 10 deletions src/components/Dashboard/WeightCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { testQueryClient } from "tests/queryClient";
import { testWeightEntries } from "tests/weight/testData";

jest.mock("components/BodyWeight/queries");
const { ResizeObserver } = window;

describe("test the WeightCard component", () => {

Expand All @@ -17,18 +16,9 @@ describe("test the WeightCard component", () => {
isLoading: false,
data: testWeightEntries
}));

// @ts-ignore
delete window.ResizeObserver;
window.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn()
}));
});

afterEach(() => {
window.ResizeObserver = ResizeObserver;
jest.restoreAllMocks();
jest.useRealTimers();
});
Expand Down
34 changes: 34 additions & 0 deletions src/components/Trophies/components/TrophiesDetail.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import React from 'react';
import { testUserProgressionTrophies } from "tests/trophies/trophiesTestData";
import { TrophiesDetail } from './TrophiesDetail';


// Mock the trophies query hook
jest.mock('components/Trophies/queries/trophies', () => ({
useUserTrophyProgressionQuery: () => ({
isLoading: false,
data: testUserProgressionTrophies(),
}),
}));

describe('TrophiesDetail', () => {
test('renders trophy names and progression values', () => {

// Act
render(<TrophiesDetail />);

// Assert
expect(screen.getByText('Beginner')).toBeInTheDocument();
expect(screen.getByText('Unstoppable')).toBeInTheDocument();
expect(screen.getByText('Complete your first workout')).toBeInTheDocument();
expect(screen.getByText('Maintain a 30-day workout streak')).toBeInTheDocument();

// Progression value for the progressive trophy should be shown
expect(screen.getByText('4/30')).toBeInTheDocument();

// There should be at least one progressbar in the document
expect(screen.getAllByRole('progressbar').length).toBeGreaterThanOrEqual(1);
});
});
Loading
Loading