Skip to content
Closed
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
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
"@emotion/styled": "^11.14.1",
"@hello-pangea/dnd": "^18.0.1",
"@mui/icons-material": "^7.3.4",
"@mui/lab": "^7.0.1-beta.18",
"@mui/lab": "^7.0.0-beta.10",
"@mui/material": "^7.3.4",
"@mui/system": "^7.3.3",
"@mui/x-data-grid": "^8.14.0",
"@mui/x-date-pickers": "^8.14.0",
"@tanstack/react-query": "^5.90.2",
"@vitejs/plugin-react": "^5.0.4",
"@mui/system": "^7.0.1",
"@mui/x-data-grid": "^8.2.0",
"@mui/x-date-pickers": "^8.2.0",
"@tanstack/react-query": "^5.71.5",
"@vitejs/plugin-react": "^5.0.3",
"axios": "^1.12.2",
"formik": "^2.4.6",
"history": "^5.3.0",
Expand Down
33 changes: 17 additions & 16 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import Grid from '@mui/material/Grid';
import { Header, } from 'components';
import { Notifications } from 'components/Core/Notifications';
import React from 'react';
import Grid from "@mui/material/Grid";
import { Header } from "components";
import { Notifications } from "components/Core/Notifications";
import React from "react";
import { WgerRoutes } from "routes";

import { PreferencesProvider } from "state/PreferencesContext";

function App() {

return (
(<Grid container>
<Grid size={12}>
<Header />
</Grid>
<Grid size={12}>
<Notifications />
</Grid>
<Grid size={12}>
<WgerRoutes />
<PreferencesProvider>
<Grid container>
<Grid size={12}>
<Header />
</Grid>
<Grid size={12}>
<Notifications />
</Grid>
<Grid size={12}>
<WgerRoutes />
</Grid>
</Grid>
</Grid>)
</PreferencesProvider>
);
}

Expand Down
20 changes: 20 additions & 0 deletions src/components/Header/SubMenus/PreferenceButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IconButton } from "@mui/material";
import { Link } from "react-router-dom";
import { Settings } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { makeLink, WgerLink } from "utils/url";

export const PreferenceButton = () => {
const { i18n } = useTranslation();

return (
<IconButton
color="inherit"
component={Link}
to={makeLink(WgerLink.USER_PREFERENCES, i18n.language)}
sx={{ mr: 2 }}
>
<Settings />
</IconButton>
);
};
67 changes: 42 additions & 25 deletions src/components/Header/SubMenus/TrainingSubMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,59 @@
import { Button, Menu, MenuItem } from "@mui/material";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from 'react-router-dom';
import { Link } from "react-router-dom";
import { usePreferences } from "state/PreferencesContext";
import { makeLink, WgerLink } from "utils/url";

export const TrainingSubMenu = () => {

const { i18n } = useTranslation();
const [anchorElRoutine, setAnchorElRoutine] = React.useState<null | HTMLElement>(null);

const {
showRoutineOverview,
showPrivateTemplate,
showPublicTemplate,
showExerciseOverview,
showExerciseContribute,
showCalendar,
} = usePreferences();

return (
<>
<Button color="inherit" onClick={(event) => setAnchorElRoutine(event.currentTarget)}>
Routines
</Button>
<Menu
anchorEl={anchorElRoutine}
open={Boolean(anchorElRoutine)}
onClose={() => setAnchorElRoutine(null)}
>
<MenuItem component={Link} to={makeLink(WgerLink.ROUTINE_OVERVIEW, i18n.language)}>
Routine overview
</MenuItem>
<MenuItem component={Link} to={makeLink(WgerLink.PRIVATE_TEMPLATE_OVERVIEW, i18n.language)}>
Private template overview
</MenuItem>
<MenuItem component={Link} to={makeLink(WgerLink.PUBLIC_TEMPLATE_OVERVIEW, i18n.language)}>
Public template overview
</MenuItem>
<MenuItem component={Link} to={makeLink(WgerLink.EXERCISE_OVERVIEW, i18n.language)}>
Exercise overview
</MenuItem>
<MenuItem component={Link} to={makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language)}>
Contribute exercise
</MenuItem>
<MenuItem component={Link} to={makeLink(WgerLink.CALENDAR, i18n.language)}>
Calendar
</MenuItem>
<Menu anchorEl={anchorElRoutine} open={Boolean(anchorElRoutine)} onClose={() => setAnchorElRoutine(null)}>
{showRoutineOverview && (
<MenuItem component={Link} to={makeLink(WgerLink.ROUTINE_OVERVIEW, i18n.language)}>
Routine overview
</MenuItem>
)}
{showPrivateTemplate && (
<MenuItem component={Link} to={makeLink(WgerLink.PRIVATE_TEMPLATE_OVERVIEW, i18n.language)}>
Private template overview
</MenuItem>
)}
{showPublicTemplate && (
<MenuItem component={Link} to={makeLink(WgerLink.PUBLIC_TEMPLATE_OVERVIEW, i18n.language)}>
Public template overview
</MenuItem>
)}
{showExerciseOverview && (
<MenuItem component={Link} to={makeLink(WgerLink.EXERCISE_OVERVIEW, i18n.language)}>
Exercise overview
</MenuItem>
)}
{showExerciseContribute && (
<MenuItem component={Link} to={makeLink(WgerLink.EXERCISE_CONTRIBUTE, i18n.language)}>
Contribute exercise
</MenuItem>
)}
{showCalendar && (
<MenuItem component={Link} to={makeLink(WgerLink.CALENDAR, i18n.language)}>
Calendar
</MenuItem>
)}
</Menu>
</>
);
Expand Down
28 changes: 18 additions & 10 deletions src/components/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,33 @@ import { BodyWeightSubMenu } from "components/Header/SubMenus/BodyWeightSubMenu"
import { MeasurementsSubMenu } from "components/Header/SubMenus/MeasurementsSubMenu";
import { NutritionSubMenu } from "components/Header/SubMenus/NutritionSubMenu";
import { TrainingSubMenu } from "components/Header/SubMenus/TrainingSubMenu";
import React from 'react';

import { PreferenceButton } from "./SubMenus/PreferenceButton";
import { usePreferences } from "state/PreferencesContext";
import React from "react";

export const Header = () => {
const { showTraining, showBodyWeight, showMeasurements, showNutrition } = usePreferences();

return (
<AppBar position="static">
<AppBar
position="static"
sx={{
flex: "auto",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Toolbar>
<Typography variant="h6" component="div" mr={3}>
wger
</Typography>

<TrainingSubMenu />
<BodyWeightSubMenu />
<MeasurementsSubMenu />
<NutritionSubMenu />


{showTraining && <TrainingSubMenu />}
{showBodyWeight && <BodyWeightSubMenu />}
{showMeasurements && <MeasurementsSubMenu />}
{showNutrition && <NutritionSubMenu />}
</Toolbar>
<PreferenceButton />
</AppBar>
);
};
};
113 changes: 113 additions & 0 deletions src/pages/Preferences/Preferences.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Preferences.test.tsx
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { PreferencesProvider } from "state/PreferencesContext";
import { Preferences } from ".";

const renderWithProvider = (ui: React.ReactElement) => render(<PreferencesProvider>{ui}</PreferencesProvider>);

const getSwitchByLabel = (label: string) => {
const listItem = screen.getByText(label).closest("li");
if (!listItem) throw new Error(`No list item found for label: ${label}`);
const input = listItem.querySelector('input[type="checkbox"]') as HTMLInputElement;
if (!input) throw new Error(`No input found for label: ${label}`);
return input;
};

// Using test doubles (mocks/stubs/fakes)
describe("Preferences component", () => {
it("renders all submenu switches with correct default values", () => {
renderWithProvider(<Preferences />);

expect(getSwitchByLabel("Show Training").checked).toBe(true);
expect(getSwitchByLabel("Show Body Weight").checked).toBe(true);
expect(getSwitchByLabel("Show Measurements").checked).toBe(true);
expect(getSwitchByLabel("Show Nutrition").checked).toBe(true);

expect(getSwitchByLabel("Routine overview").checked).toBe(true);
expect(getSwitchByLabel("Private template").checked).toBe(true);
expect(getSwitchByLabel("Public template").checked).toBe(true);
expect(getSwitchByLabel("Exercise overview").checked).toBe(true);
expect(getSwitchByLabel("Contribute exercise").checked).toBe(true);
expect(getSwitchByLabel("Calendar").checked).toBe(true);
});

it("toggles a main submenu switch", () => {
renderWithProvider(<Preferences />);
const trainingSwitch = getSwitchByLabel("Show Training");

fireEvent.click(trainingSwitch);
expect(trainingSwitch.checked).toBe(false);

fireEvent.click(trainingSwitch);
expect(trainingSwitch.checked).toBe(true);
});

it("toggles a training submenu switch", () => {
renderWithProvider(<Preferences />);
const routineSwitch = getSwitchByLabel("Routine overview");

fireEvent.click(routineSwitch);
expect(routineSwitch.checked).toBe(false);

fireEvent.click(routineSwitch);
expect(routineSwitch.checked).toBe(true);
});

it("prevents hiding last visible main submenu", () => {
renderWithProvider(<Preferences />);

const trainingSwitch = getSwitchByLabel("Show Training");
const bodyWeightSwitch = getSwitchByLabel("Show Body Weight");
const measurementsSwitch = getSwitchByLabel("Show Measurements");
const nutritionSwitch = getSwitchByLabel("Show Nutrition");

// Hide 3 of 4 menus
fireEvent.click(bodyWeightSwitch);
fireEvent.click(measurementsSwitch);
fireEvent.click(nutritionSwitch);

const alertMock = jest.spyOn(window, "alert").mockImplementation(() => {});
fireEvent.click(trainingSwitch); // should be blocked

expect(trainingSwitch.checked).toBe(true);
expect(alertMock).toHaveBeenCalledWith("At least one item must remain visible.");
alertMock.mockRestore();
});

it("restores training submenu switches when main menu toggled", () => {
renderWithProvider(<Preferences />);

const trainingSwitch = getSwitchByLabel("Show Training");
const routineSwitch = getSwitchByLabel("Routine overview");

// Turn off a training submenu
fireEvent.click(routineSwitch);
expect(routineSwitch.checked).toBe(false);

// Turn off main menu
fireEvent.click(trainingSwitch);
expect(trainingSwitch.checked).toBe(false);
expect(routineSwitch.checked).toBe(false);

// Turn main menu back on, should restore previous submenu state
fireEvent.click(trainingSwitch);
expect(trainingSwitch.checked).toBe(true);
expect(routineSwitch.checked).toBe(false); // restored to previous state
});
});

//Profiling
test("renders all submenu switches with correct default values", () => {
const t0 = performance.now();

renderWithProvider(<Preferences />);

const t1 = performance.now();
console.log("Render time:", t1 - t0, "ms");

// rest of your test assertions
expect(getSwitchByLabel("Show Training")).toBeInTheDocument();
expect(getSwitchByLabel("Show Training")).toBeChecked();
expect(getSwitchByLabel("Show Body Weight")).toBeChecked();
});
66 changes: 61 additions & 5 deletions src/pages/Preferences/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,65 @@
import React from 'react';
import { List, ListItem, ListItemText, Switch, Paper, Divider, Typography } from "@mui/material";
import { WgerContainerRightSidebar } from "components/Core/Widgets/Container";
import { usePreferences } from "state/PreferencesContext";
import React from "react";

export const Preferences = () => {
const prefs = usePreferences();

return (
<div>
Preferences Page
</div>
<WgerContainerRightSidebar
title={"User Preferences"}
mainContent={
<Paper>
<List sx={{ py: 0 }}>
<Typography variant="h6" sx={{ px: 2, pt: 1 }}>
Submenus
</Typography>
{(
[
["Show Training", prefs.showTraining, prefs.setShowTraining],
["Show Body Weight", prefs.showBodyWeight, prefs.setShowBodyWeight],
["Show Measurements", prefs.showMeasurements, prefs.setShowMeasurements],
["Show Nutrition", prefs.showNutrition, prefs.setShowNutrition],
] as [string, boolean, (v: boolean) => void][]
).map(([label, value, setter]) => (
<ListItem
key={label}
secondaryAction={
<Switch edge="end" checked={value} onChange={(e) => setter(e.target.checked)} />
}
>
<ListItemText primary={label} />
</ListItem>
))}

<Divider sx={{ my: 1 }} />
<Typography variant="h6" sx={{ px: 2, pt: 1 }}>
Training submenu
</Typography>

{(
[
["Routine overview", prefs.showRoutineOverview, prefs.setShowRoutineOverview],
["Private template", prefs.showPrivateTemplate, prefs.setShowPrivateTemplate],
["Public template", prefs.showPublicTemplate, prefs.setShowPublicTemplate],
["Exercise overview", prefs.showExerciseOverview, prefs.setShowExerciseOverview],
["Contribute exercise", prefs.showExerciseContribute, prefs.setShowExerciseContribute],
["Calendar", prefs.showCalendar, prefs.setShowCalendar],
] as [string, boolean, (v: boolean) => void][]
).map(([label, value, setter]) => (
<ListItem
key={label}
secondaryAction={
<Switch edge="end" checked={value} onChange={(e) => setter(e.target.checked)} />
}
>
<ListItemText primary={label} />
</ListItem>
))}
</List>
</Paper>
}
/>
);
};
};
Loading