mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-20 16:49:46 -06:00
Merge pull request #1409 from peterpardo/feat/dark-mode-switch
Feat: Add dark mode switch
This commit is contained in:
@@ -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";
|
||||
@@ -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 />
|
||||
<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
|
||||
|
||||
98
Client/src/Components/ThemeSwitch/SunAndMoonIcon.jsx
Normal file
98
Client/src/Components/ThemeSwitch/SunAndMoonIcon.jsx
Normal file
@@ -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
Client/src/Components/ThemeSwitch/index.css
Normal file
64
Client/src/Components/ThemeSwitch/index.css
Normal file
@@ -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
Client/src/Components/ThemeSwitch/index.jsx
Normal file
47
Client/src/Components/ThemeSwitch/index.jsx
Normal file
@@ -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}`}
|
||||
aria-label="auto"
|
||||
aria-live="polite"
|
||||
onClick={toggleTheme}
|
||||
sx={{
|
||||
width,
|
||||
height,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<SunAndMoonIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSwitch;
|
||||
@@ -3,7 +3,6 @@ import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import TextInput from "../../../../Components/Inputs/TextInput";
|
||||
import PropTypes from "prop-types";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
/**
|
||||
* Renders the email step of the login process which includes an email field.
|
||||
@@ -19,7 +18,6 @@ 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) {
|
||||
@@ -27,13 +25,6 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNavigate = () => {
|
||||
if (form.email !== "" && !errors.email) {
|
||||
sessionStorage.setItem("email", form.email);
|
||||
}
|
||||
navigate("/forgot-password");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
@@ -90,32 +81,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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
ForgotPasswordLabel.proptype = {
|
||||
email: PropTypes.string.isRequired,
|
||||
emailError: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ForgotPasswordLabel;
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
@@ -22,7 +21,6 @@ import PropTypes from "prop-types";
|
||||
*/
|
||||
const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const inputRef = useRef(null);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
|
||||
@@ -32,13 +30,6 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNavigate = () => {
|
||||
if (form.email !== "" && !errors.email) {
|
||||
sessionStorage.setItem("email", form.email);
|
||||
}
|
||||
navigate("/forgot-password");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
@@ -117,32 +108,6 @@ const PasswordStep = ({ 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>
|
||||
</>
|
||||
);
|
||||
@@ -156,4 +121,4 @@ PasswordStep.propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default PasswordStep
|
||||
export default PasswordStep;
|
||||
|
||||
@@ -13,6 +13,8 @@ import { logger } from "../../../Utils/Logger";
|
||||
import "../index.css";
|
||||
import EmailStep from "./Components/EmailStep";
|
||||
import PasswordStep from "./Components/PasswordStep";
|
||||
import ThemeSwitch from "../../../Components/ThemeSwitch";
|
||||
import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
|
||||
|
||||
const DEMO = import.meta.env.VITE_APP_DEMO;
|
||||
|
||||
@@ -174,6 +176,7 @@ const Login = () => {
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
rowGap={theme.spacing(8)}
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
@@ -205,6 +208,13 @@ const Login = () => {
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<ForgotPasswordLabel
|
||||
email={form.email}
|
||||
errorEmail={errors.email}
|
||||
/>
|
||||
<Box marginX={"auto"}>
|
||||
<ThemeSwitch />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user