diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 18ae19dc8..d6d25fbc2 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -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} - - openPopup(event, "logout")} + + - - - + openPopup(event, "logout")} + > + + + + )} { + const theme = useTheme(); + + return ( + + ); +}; + +export default SunAndMoonIcon; diff --git a/Client/src/Components/ThemeSwitch/index.css b/Client/src/Components/ThemeSwitch/index.css new file mode 100644 index 000000000..d5db1c1ca --- /dev/null +++ b/Client/src/Components/ThemeSwitch/index.css @@ -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; + } +} diff --git a/Client/src/Components/ThemeSwitch/index.jsx b/Client/src/Components/ThemeSwitch/index.jsx new file mode 100644 index 000000000..b751f542a --- /dev/null +++ b/Client/src/Components/ThemeSwitch/index.jsx @@ -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 ( + + + + ); +}; + +export default ThemeSwitch; diff --git a/Client/src/Pages/Auth/Login/Components/EmailStep.jsx b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx index 08ae51c4f..ac796e627 100644 --- a/Client/src/Pages/Auth/Login/Components/EmailStep.jsx +++ b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx @@ -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 ( <> { - - - Forgot password? - - - Reset password - - ); diff --git a/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx b/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx new file mode 100644 index 000000000..0a5fa8153 --- /dev/null +++ b/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx @@ -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 ( + + + Forgot password? + + + Reset password + + + ); +}; + +ForgotPasswordLabel.proptype = { + email: PropTypes.string.isRequired, + emailError: PropTypes.string.isRequired, +}; + +export default ForgotPasswordLabel; diff --git a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx index 34df4959d..b7384ba98 100644 --- a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx +++ b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx @@ -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 ( <> { - - - Forgot password? - - - Reset password - - ); @@ -156,4 +121,4 @@ PasswordStep.propTypes = { onBack: PropTypes.func.isRequired, }; -export default PasswordStep \ No newline at end of file +export default PasswordStep; diff --git a/Client/src/Pages/Auth/Login/Login.jsx b/Client/src/Pages/Auth/Login/Login.jsx index c1687a0ec..890c87eb7 100644 --- a/Client/src/Pages/Auth/Login/Login.jsx +++ b/Client/src/Pages/Auth/Login/Login.jsx @@ -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 = () => { /> ) )} + + + + );