Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add dark mode switch #1409

Merged
merged 21 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
48 changes: 28 additions & 20 deletions Client/src/Components/Sidebar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useDispatch, useSelector } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { toggleSidebar } from "../../Features/UI/uiSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import ThemeSwitch from "../ThemeSwitch";
import Avatar from "../Avatar";
import LockSvg from "../../assets/icons/lock.svg?react";
import UserSvg from "../../assets/icons/user.svg?react";
Expand Down Expand Up @@ -551,28 +552,35 @@ function Sidebar() {
{authState.user?.role}
</Typography>
</Box>
<Tooltip
title="Controls"
disableInteractive
<Stack
flexDirection={"row"}
marginLeft={"auto"}
columnGap={theme.spacing(2)}
>
<IconButton
sx={{
ml: "auto",
mr: "-8px",
"&:focus": { outline: "none" },
"& svg": {
width: "20px",
height: "20px",
},
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
onClick={(event) => openPopup(event, "logout")}
<ThemeSwitch />

This comment was marked as off-topic.

<Tooltip
title="Controls"
disableInteractive
>
<DotsVertical />
</IconButton>
</Tooltip>
<IconButton
sx={{
ml: "auto",
mr: "-8px",
"&:focus": { outline: "none" },
"& svg": {
width: "20px",
height: "20px",
},
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
onClick={(event) => openPopup(event, "logout")}
>
<DotsVertical />
</IconButton>
</Tooltip>
</Stack>
</>
)}
<Menu
Expand Down
98 changes: 98 additions & 0 deletions Client/src/Components/ThemeSwitch/SunAndMoonIcon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useTheme } from "@mui/material";
import "./index.css";

const SunAndMoonIcon = () => {
const theme = useTheme();

return (
<svg
className="sun-and-moon"
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 24 24"
>
<mask
className="moon"
id="moon-mask"
>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="#fff"
/>
<circle
cx="24"
cy="10"
r="6"
fill="#000"
/>
</mask>
<circle
className="sun"
cx="12"
cy="12"
r="6"
fill={theme.palette.text.secondary}
mask="url(#moon-mask)"
/>
<g
className="sun-beams"
stroke={theme.palette.text.secondary}
>
<line
x1="12"
y1="1"
x2="12"
y2="3"
/>
<line
x1="12"
y1="21"
x2="12"
y2="23"
/>
<line
x1="4.22"
y1="4.22"
x2="5.64"
y2="5.64"
/>
<line
x1="18.36"
y1="18.36"
x2="19.78"
y2="19.78"
/>
<line
x1="1"
y1="12"
x2="3"
y2="12"
/>
<line
x1="21"
y1="12"
x2="23"
y2="12"
/>
<line
x1="4.22"
y1="19.78"
x2="5.64"
y2="18.36"
/>
<line
x1="18.36"
y1="5.64"
x2="19.78"
y2="4.22"
/>
</g>
</svg>
);
};

export default SunAndMoonIcon;
64 changes: 64 additions & 0 deletions Client/src/Components/ThemeSwitch/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.sun-and-moon > :is(.moon, .sun, .sun-beams) {
transform-origin: center;
}

.theme-toggle .sun-and-moon > .sun-beams {
stroke-width: 2px;
}

[data-theme="dark"] .sun-and-moon > .sun {
transform: scale(1.75);
}

[data-theme="dark"] .sun-and-moon > .sun-beams {
opacity: 0;
}

[data-theme="dark"] .sun-and-moon > .moon > circle {
transform: translateX(-7px);
}

@supports (cx: 1) {
[data-theme="dark"] .sun-and-moon > .moon > circle {
cx: 17;
transform: translateX(0);
}
}

@media (prefers-reduced-motion: no-preference) {
.sun-and-moon > .sun {
transition: transform 0.5s var(--ease-elastic-3);
}

.sun-and-moon > .sun-beams {
transition:
transform 0.5s var(--ease-elastic-4),
opacity 0.5s var(--ease-3);
}

.sun-and-moon .moon > circle {
transition: transform 0.25s var(--ease-out-5);
}

@supports (cx: 1) {
.sun-and-moon .moon > circle {
transition: cx 0.25s var(--ease-out-5);
}
}

[data-theme="dark"] .sun-and-moon > .sun {
transition-timing-function: var(--ease-3);
transition-duration: 0.25s;
transform: scale(1.75);
}

[data-theme="dark"] .sun-and-moon > .sun-beams {
transition-duration: 0.15s;
transform: rotateZ(-25deg);
}

[data-theme="dark"] .sun-and-moon > .moon > circle {
transition-duration: 0.5s;
transition-delay: 0.25s;
}
}
40 changes: 40 additions & 0 deletions Client/src/Components/ThemeSwitch/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { useEffect, useState } from "react";
import { IconButton } from "@mui/material";
import SunAndMoonIcon from "./SunAndMoonIcon";
import "./index.css";
import { useDispatch, useSelector } from "react-redux";
import { setMode } from "../../Features/UI/uiSlice";

const ThemeSwitch = () => {
const mode = useSelector((state) => state.ui.mode);
const dispatch = useDispatch();

const toggleTheme = () => {
dispatch(setMode(mode === "light" ? "dark" : "light"));
};

useEffect(() => {
document.body.setAttribute("data-theme", mode);
}, [mode]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the logic from this implementation relies on data-attributes. But we already have a theme on redux. I suggest adapting that to rely on our theme as well. This way you can ditch this useEffect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Let me update the component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the commit for the changes I made: 7c5c350


return (
<IconButton
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
aria-live="polite"
onClick={toggleTheme}
sx={{
width: 48,
height: 48,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<SunAndMoonIcon />
</IconButton>
);
};

export default ThemeSwitch;
49 changes: 23 additions & 26 deletions Client/src/Pages/Auth/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import PropTypes from "prop-types";
import { logger } from "../../Utils/Logger";
import "./index.css";
import ThemeSwitch from "../../Components/ThemeSwitch";
const DEMO = import.meta.env.VITE_APP_DEMO;

/**
Expand Down Expand Up @@ -327,33 +328,25 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
</LoadingButton>
</Stack>
</Box>
<Box
textAlign="center"
sx={{
position: "absolute",
bottom: 0,
left: "50%",
transform: `translate(-50%, 150%)`,
}}
>
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
Forgot password?
</Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
Reset password
</Typography>
</Box>
</Stack>
<Box textAlign="center">
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
Forgot password?
</Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
Reset password
</Typography>
</Box>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve keyboard accessibility for password reset

The password reset section could benefit from better keyboard accessibility.

Consider this improvement:

 <Typography
   component="span"
   color={theme.palette.primary.main}
   ml={theme.spacing(2)}
-  sx={{ userSelect: "none" }}
+  sx={{ 
+    userSelect: "none",
+    cursor: "pointer",
+    "&:focus-visible": {
+      outline: `2px solid ${theme.palette.primary.main}`,
+      outlineOffset: "2px",
+    }
+  }}
+  tabIndex={0}
+  role="button"
+  onKeyDown={(e) => e.key === "Enter" && handleNavigate()}
   onClick={handleNavigate}
 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Box textAlign="center">
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
Forgot password?
</Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
Reset password
</Typography>
</Box>
<Box textAlign="center">
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
Forgot password?
</Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{
userSelect: "none",
cursor: "pointer",
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: "2px",
}
}}
tabIndex={0}
role="button"
onKeyDown={(e) => e.key === "Enter" && handleNavigate()}
onClick={handleNavigate}
>
Reset password
</Typography>
</Box>

</>
);
};
Expand Down Expand Up @@ -520,6 +513,7 @@ const Login = () => {
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
pb={theme.spacing(20)}
mx="auto"
gap={theme.spacing(8)}
sx={{
"& > .MuiStack-root": {
border: 1,
Expand Down Expand Up @@ -554,6 +548,9 @@ const Login = () => {
/>
)
)}
<Box marginX={"auto"}>
<ThemeSwitch />
</Box>
</Stack>
</Stack>
);
Expand Down
1 change: 1 addition & 0 deletions Client/src/index.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
@import "https://unpkg.com/open-props/easings.min.css";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

Copy link
Contributor Author

@peterpardo peterpardo Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one is responsible for importing the easing functions that is being used by the toggle switch. I found it here in the code of the css file https://web.dev/patterns/theming/theme-switch.

This are currently the easing variables that is being used:

  • --ease-elastic-3
  • --ease-elastic-4
  • --ease-out-5

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@peterpardo we don't want the project to depened on a CDN that we can't guarantee is going to be available at all times.

Can you please copy only the css that is needed from that css file and add it locally? We can drop the import then and ensure the code wil be working at all times.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. I'll check how they did the animation.

Copy link
Contributor Author

@peterpardo peterpardo Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are the new changes. I've removed the import and instead just took the code needed for the smooth animation of the toggle to work.

76dc4eb


* {
box-sizing: border-box;
Expand Down