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 20 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 @@ -544,28 +545,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;
}

.theme-dark .sun-and-moon > .sun {
transform: scale(1.75);
}

.theme-dark .sun-and-moon > .sun-beams {
opacity: 0;
}

.theme-dark .sun-and-moon > .moon > circle {
transform: translateX(-7px);
}

@supports (cx: 1) {
.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 cubic-bezier(0.68, -0.55, 0.27, 1.55);
}

.sun-and-moon > .sun-beams {
transition:
transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55),
opacity 0.5s cubic-bezier(0.25, 0.1, 0.25, 1);
}

.sun-and-moon .moon > circle {
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

@supports (cx: 1) {
.sun-and-moon .moon > circle {
transition: cx 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
}

.theme-dark .sun-and-moon > .sun {
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
transition-duration: 0.25s;
transform: scale(1.75);
}

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

.theme-dark .sun-and-moon > .moon > circle {
transition-duration: 0.5s;
transition-delay: 0.25s;
}
}
47 changes: 47 additions & 0 deletions Client/src/Components/ThemeSwitch/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* ThemeSwitch Component
* Dark and Light Theme Switch
* Original Code: https://web.dev/patterns/theming/theme-switch
* License: Apache License 2.0
* Copyright © Google LLC
*
* This code has been adapted for use in this project.
* Apache License: https://www.apache.org/licenses/LICENSE-2.0
*/

import { IconButton } from "@mui/material";
import SunAndMoonIcon from "./SunAndMoonIcon";
import { useDispatch, useSelector } from "react-redux";
import { setMode } from "../../Features/UI/uiSlice";
import "./index.css";

const ThemeSwitch = ({ width = 48, height = 48 }) => {
const mode = useSelector((state) => state.ui.mode);
const dispatch = useDispatch();

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

return (
<IconButton
id="theme-toggle"
title="Toggles light & dark"
className={`theme-${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 really liked this approach =)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Awesome! Thank you. I really appreciate that.

aria-label="auto"
aria-live="polite"
onClick={toggleTheme}
sx={{
width,
height,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<SunAndMoonIcon />
</IconButton>
);
};

export default ThemeSwitch;
34 changes: 0 additions & 34 deletions Client/src/Pages/Auth/Login/Components/EmailStep.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,13 @@ import { useNavigate } from "react-router";
const EmailStep = ({ form, errors, onSubmit, onChange }) => {
const theme = useTheme();
const inputRef = useRef(null);
const navigate = useNavigate();

useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);

const handleNavigate = () => {
if (form.email !== "" && !errors.email) {
sessionStorage.setItem("email", form.email);
}
navigate("/forgot-password");
};

return (
<>
<Stack
Expand Down Expand Up @@ -90,32 +82,6 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
</Button>
</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>
</>
);
Expand Down
43 changes: 43 additions & 0 deletions Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Box, Typography, useTheme } from "@mui/material";
import PropTypes from "prop-types";
import { useNavigate } from "react-router";

const ForgotPasswordLabel = ({ email, errorEmail }) => {
const theme = useTheme();
const navigate = useNavigate();

const handleNavigate = () => {
if (email !== "" && !errorEmail) {
sessionStorage.setItem("email", email);
}
navigate("/forgot-password");
};

return (
<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>
Comment on lines +25 to +34
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Yo, let's make this reset password link more accessible!

The reset password text should be more interactive and accessible:

  • Missing hover state for better UX
  • Should be a button or link for keyboard navigation
  • Needs cursor pointer style
 <Typography
-  component="span"
+  component="button"
   color={theme.palette.primary.main}
   ml={theme.spacing(2)}
-  sx={{ userSelect: "none" }}
+  sx={{
+    userSelect: "none",
+    cursor: "pointer",
+    background: "none",
+    border: "none",
+    padding: 0,
+    '&:hover': {
+      textDecoration: 'underline'
+    }
+  }}
   onClick={handleNavigate}
 >
   Reset password
 </Typography>
📝 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
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
Reset password
</Typography>
</Box>
<Typography
component="button"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{
userSelect: "none",
cursor: "pointer",
background: "none",
border: "none",
padding: 0,
'&:hover': {
textDecoration: 'underline'
}
}}
onClick={handleNavigate}
>
Reset password
</Typography>
</Box>

);
};

ForgotPasswordLabel.proptype = {
email: PropTypes.string.isRequired,
emailError: PropTypes.string.isRequired,
};
Comment on lines +38 to +41
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Yo, we got some PropTypes issues that's making me nervous!

There are two critical issues in the PropTypes declaration:

  1. proptype is misspelled (should be propTypes)
  2. emailError prop type doesn't match the errorEmail prop used in the component
-ForgotPasswordLabel.proptype = {
+ForgotPasswordLabel.propTypes = {
   email: PropTypes.string.isRequired,
-  emailError: PropTypes.string.isRequired,
+  errorEmail: PropTypes.string.isRequired,
 };
📝 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
ForgotPasswordLabel.proptype = {
email: PropTypes.string.isRequired,
emailError: PropTypes.string.isRequired,
};
ForgotPasswordLabel.propTypes = {
email: PropTypes.string.isRequired,
errorEmail: PropTypes.string.isRequired,
};


export default ForgotPasswordLabel;
Loading