Skip to content

Commit 5b88e9f

Browse files
authored
Merge pull request #988 from ChrisUlicny/feature/body-weight-chart-filter-1696
Feature/body weight chart filter 1696
2 parents 19f0be7 + e549286 commit 5b88e9f

File tree

11 files changed

+266
-17
lines changed

11 files changed

+266
-17
lines changed

public/locales/en/translation.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
"close": "Close",
1111
"difference": "Difference",
1212
"days": "Days",
13+
"all": "All",
14+
"lastYear": "Last Year",
15+
"lastHalfYear": "Last 6 Months",
16+
"lastMonth": "Last Month",
17+
"lastWeek": "Last Week",
1318
"licenses": {
1419
"authors": "Author(s)",
1520
"authorProfile": "Link to author website or profile, if available",

src/components/BodyWeight/WeightChart/index.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe("Test BodyWeight component", () => {
2828
test('renders without crashing', async () => {
2929

3030
// Arrange
31-
const weightData = [
31+
const weightData = [
3232
new WeightEntry(new Date('2021-12-10'), 80, 1),
3333
new WeightEntry(new Date('2021-12-20'), 90, 2),
3434
];

src/components/BodyWeight/index.test.tsx

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { QueryClientProvider } from "@tanstack/react-query";
2-
import { render, screen } from '@testing-library/react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
33
import { WeightEntry } from "components/BodyWeight/model";
44
import { getWeights } from "services";
55
import { testQueryClient } from "tests/queryClient";
66
import { BodyWeight } from "./index";
7+
import axios from "axios";
8+
import { FilterType } from "./widgets/FilterButtons";
79

810
const { ResizeObserver } = window;
911

@@ -29,13 +31,13 @@ describe("Test BodyWeight component", () => {
2931
jest.restoreAllMocks();
3032
});
3133

32-
test('renders without crashing', async () => {
34+
// Arrange
35+
const weightData = [
36+
new WeightEntry(new Date('2021-12-10'), 80, 1),
37+
new WeightEntry(new Date('2021-12-20'), 90, 2),
38+
];
3339

34-
// Arrange
35-
const weightData = [
36-
new WeightEntry(new Date('2021-12-10'), 80, 1),
37-
new WeightEntry(new Date('2021-12-20'), 90, 2),
38-
];
40+
test('renders without crashing', async () => {
3941

4042
// @ts-ignore
4143
getWeights.mockImplementation(() => Promise.resolve(weightData));
@@ -57,4 +59,42 @@ describe("Test BodyWeight component", () => {
5759
const textElement2 = await screen.findByText("90");
5860
expect(textElement2).toBeInTheDocument();
5961
});
62+
63+
64+
test('changes filter and updates displayed data', async () => {
65+
66+
// Mock the getWeights response based on the filter
67+
// @ts-ignore
68+
getWeights.mockImplementation((filter: FilterType) => {
69+
if (filter === 'lastYear') {
70+
return Promise.resolve(weightData);
71+
} else if (filter === 'lastMonth') {
72+
return Promise.resolve([]);
73+
}
74+
return Promise.resolve([]);
75+
});
76+
77+
render(
78+
<QueryClientProvider client={testQueryClient}>
79+
<BodyWeight />
80+
</QueryClientProvider>
81+
);
82+
83+
// Initially should display data for last year
84+
expect(await screen.findByText("80")).toBeInTheDocument();
85+
expect(await screen.findByText("90")).toBeInTheDocument();
86+
87+
// Change filter to 'lastMonth'
88+
const filterButton = screen.getByRole('button', { name: /lastMonth/i });
89+
fireEvent.click(filterButton);
90+
91+
// Expect getWeights to be called with 'lastMonth'
92+
expect(getWeights).toHaveBeenCalledWith('lastMonth');
93+
94+
// Check that entries for last year are no longer in the document
95+
expect(screen.queryByText("80")).not.toBeInTheDocument();
96+
expect(screen.queryByText("90")).not.toBeInTheDocument();
97+
});
98+
99+
60100
});

src/components/BodyWeight/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Box, Stack } from "@mui/material";
1+
import { Box, Stack, Button, ButtonGroup } from "@mui/material";
22
import { useBodyWeightQuery } from "components/BodyWeight/queries";
33
import { WeightTable } from "components/BodyWeight/Table";
44
import { WeightChart } from "components/BodyWeight/WeightChart";
@@ -7,16 +7,24 @@ import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget"
77
import { WgerContainerRightSidebar } from "components/Core/Widgets/Container";
88
import { OverviewEmpty } from "components/Core/Widgets/OverviewEmpty";
99
import { useTranslation } from "react-i18next";
10+
import { FilterButtons, FilterType } from "components/BodyWeight/widgets/FilterButtons";
11+
import { useState } from "react";
12+
1013

1114
export const BodyWeight = () => {
1215
const [t] = useTranslation();
13-
const weightyQuery = useBodyWeightQuery();
16+
const [filter, setFilter] = useState<FilterType>('lastYear');
17+
const weightyQuery = useBodyWeightQuery(filter);
18+
const handleFilterChange = (newFilter: FilterType) => {
19+
setFilter(newFilter);
20+
};
1421

1522
return weightyQuery.isLoading
1623
? <LoadingPlaceholder />
1724
: <WgerContainerRightSidebar
1825
title={t("weight")}
1926
mainContent={<Stack spacing={2}>
27+
<FilterButtons currentFilter={filter} onFilterChange={handleFilterChange} />
2028
{weightyQuery.data!.length === 0 && <OverviewEmpty />}
2129
<WeightChart weights={weightyQuery.data!} />
2230
<Box sx={{ mt: 4 }} />

src/components/BodyWeight/queries/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { WeightEntry } from "components/BodyWeight/model";
33
import { createWeight, deleteWeight, getWeights, updateWeight, } from "services";
44
import { QueryKey, } from "utils/consts";
55
import { number } from "yup";
6+
import { FilterType } from "../widgets/FilterButtons";
67

78

8-
export function useBodyWeightQuery() {
9+
export function useBodyWeightQuery(filter: FilterType = '') {
910
return useQuery({
10-
queryKey: [QueryKey.BODY_WEIGHT],
11-
queryFn: getWeights
11+
queryKey: [QueryKey.BODY_WEIGHT, filter],
12+
queryFn: () => getWeights(filter),
1213
});
1314
}
1415

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
import { FilterButtons, FilterButtonsProps, FilterType } from './FilterButtons';
5+
6+
describe('FilterButtons Component', () => {
7+
const onFilterChange = jest.fn();
8+
9+
const renderComponent = (currentFilter: FilterType) => {
10+
render(
11+
<FilterButtons
12+
currentFilter={currentFilter}
13+
onFilterChange={onFilterChange}
14+
/>
15+
);
16+
};
17+
18+
afterEach(() => {
19+
onFilterChange.mockClear();
20+
});
21+
22+
test('renders all filter buttons', () => {
23+
renderComponent('');
24+
const buttonLabels = ['all', 'lastYear', 'lastHalfYear', 'lastMonth', 'lastWeek'];
25+
buttonLabels.forEach(label => {
26+
expect(screen.getByText(label)).toBeInTheDocument();
27+
});
28+
});
29+
30+
test('applies primary color and contained variant to the active filter button', () => {
31+
renderComponent('lastMonth');
32+
const activeButton = screen.getByText('lastMonth');
33+
expect(activeButton).toHaveClass('MuiButton-containedPrimary');
34+
});
35+
36+
test('calls onFilterChange with correct value when a button is clicked', () => {
37+
renderComponent('');
38+
const lastYearButton = screen.getByText('lastYear');
39+
40+
fireEvent.click(lastYearButton);
41+
expect(onFilterChange).toHaveBeenCalledWith('lastYear');
42+
});
43+
44+
test('does not trigger onFilterChange when clicking the currently active filter button', () => {
45+
renderComponent('lastYear');
46+
const lastYearButton = screen.getByText('lastYear');
47+
48+
fireEvent.click(lastYearButton);
49+
expect(onFilterChange).not.toHaveBeenCalled();
50+
});
51+
52+
test('displays correct default style for inactive filter buttons', () => {
53+
renderComponent('');
54+
const inactiveButton = screen.getByText('lastYear');
55+
expect(inactiveButton).toHaveClass('MuiButton-outlined');
56+
});
57+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Button, ButtonGroup } from "@mui/material";
2+
import { useTheme } from '@mui/material/styles';
3+
import { useTranslation } from "react-i18next";
4+
5+
export type FilterType = 'lastYear' | 'lastHalfYear' | 'lastMonth' | 'lastWeek' | '';
6+
7+
export interface FilterButtonsProps {
8+
currentFilter: FilterType;
9+
onFilterChange: (newFilter: FilterType) => void;
10+
}
11+
12+
export const FilterButtons = ({ currentFilter, onFilterChange }: FilterButtonsProps) => {
13+
14+
const [t] = useTranslation();
15+
16+
const theme = useTheme();
17+
18+
// Won't call onFilterChange if the filter stays the same
19+
const handleFilterChange = (newFilter: FilterType) => {
20+
if (currentFilter !== newFilter) {
21+
onFilterChange(newFilter);
22+
}
23+
};
24+
25+
return (
26+
<ButtonGroup variant="outlined" sx={{ mb: 2 }}>
27+
<Button
28+
onClick={() => handleFilterChange('')}
29+
color={currentFilter === '' ? 'primary' : 'inherit'}
30+
variant={currentFilter === '' ? 'contained' : 'outlined'}
31+
sx={{ fontFamily: theme.typography.fontFamily }}
32+
>
33+
{t('all')}
34+
</Button>
35+
<Button
36+
onClick={() => handleFilterChange('lastYear')}
37+
color={currentFilter === 'lastYear' ? 'primary' : 'inherit'}
38+
variant={currentFilter === 'lastYear' ? 'contained' : 'outlined'}
39+
sx={{ fontFamily: theme.typography.fontFamily }}
40+
>
41+
{t('lastYear')}
42+
</Button>
43+
<Button
44+
onClick={() => handleFilterChange('lastHalfYear')}
45+
color={currentFilter === 'lastHalfYear' ? 'primary' : 'inherit'}
46+
variant={currentFilter === 'lastHalfYear' ? 'contained' : 'outlined'}
47+
sx={{ fontFamily: theme.typography.fontFamily }}
48+
>
49+
{t('lastHalfYear')}
50+
</Button>
51+
<Button
52+
onClick={() => handleFilterChange('lastMonth')}
53+
color={currentFilter === 'lastMonth' ? 'primary' : 'inherit'}
54+
variant={currentFilter === 'lastMonth' ? 'contained' : 'outlined'}
55+
sx={{ fontFamily: theme.typography.fontFamily }}
56+
>
57+
{t('lastMonth')}
58+
</Button>
59+
<Button
60+
onClick={() => handleFilterChange('lastWeek')}
61+
color={currentFilter === 'lastWeek' ? 'primary' : 'inherit'}
62+
variant={currentFilter === 'lastWeek' ? 'contained' : 'outlined'}
63+
sx={{ fontFamily: theme.typography.fontFamily }}
64+
>
65+
{t('lastWeek')}
66+
</Button>
67+
</ButtonGroup>
68+
);
69+
};

src/components/Dashboard/WeightCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { makeLink, WgerLink } from "utils/url";
1616
export const WeightCard = () => {
1717

1818
const [t] = useTranslation();
19-
const weightyQuery = useBodyWeightQuery();
19+
const weightyQuery = useBodyWeightQuery('lastYear');
2020

2121
return (<>{weightyQuery.isLoading
2222
? <LoadingPlaceholder />

src/services/weight.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@ import { WeightAdapter, WeightEntry } from "components/BodyWeight/model";
33
import { ApiBodyWeightType } from 'types';
44
import { makeHeader, makeUrl } from "utils/url";
55
import { ResponseType } from "./responseType";
6+
import { FilterType } from '../components/BodyWeight/widgets/FilterButtons';
7+
import { calculatePastDate } from '../utils/date';
68

79
export const WEIGHT_PATH = 'weightentry';
810

911
/*
10-
* Fetch all weight entries
12+
* Fetch weight entries based on filter value
1113
*/
12-
export const getWeights = async (): Promise<WeightEntry[]> => {
13-
const url = makeUrl(WEIGHT_PATH, { query: { ordering: '-date', limit: 900 } });
14+
export const getWeights = async (filter: FilterType = ''): Promise<WeightEntry[]> => {
15+
16+
const date__gte = calculatePastDate(filter);
17+
18+
const url = makeUrl(WEIGHT_PATH, { query: { ordering: '-date', limit: 900, ...(date__gte && { date__gte }) } });
1419
const { data: receivedWeights } = await axios.get<ResponseType<ApiBodyWeightType>>(url, {
1520
headers: makeHeader(),
1621
});

src/utils/date.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { calculatePastDate } from "utils/date";
12
import { dateToYYYYMMDD } from "utils/date";
23

34
describe("test date utility", () => {
@@ -19,3 +20,33 @@ describe("test date utility", () => {
1920

2021

2122
});
23+
24+
25+
26+
27+
describe('calculatePastDate', () => {
28+
29+
it('should return undefined for empty string filter', () => {
30+
expect(calculatePastDate('', new Date('2023-08-14'))).toBeUndefined();
31+
});
32+
33+
it('should return the correct date for lastWeek filter', () => {
34+
const result = calculatePastDate('lastWeek', new Date('2023-02-14'));
35+
expect(result).toStrictEqual('2023-02-07');
36+
});
37+
38+
it('should return the correct date for lastMonth filter', () => {
39+
const result = calculatePastDate('lastMonth', new Date('2023-02-14'));
40+
expect(result).toStrictEqual('2023-01-14');
41+
});
42+
43+
it('should return the correct date for lastHalfYear filter', () => {
44+
const result = calculatePastDate('lastHalfYear', new Date('2023-08-14'));
45+
expect(result).toStrictEqual('2023-02-14');
46+
});
47+
48+
it('should return the correct date for lastYear filter', () => {
49+
const result = calculatePastDate('lastYear', new Date('2023-02-14'));
50+
expect(result).toStrictEqual('2022-02-14');
51+
});
52+
});

0 commit comments

Comments
 (0)