Merge branch 'develop' into feat/fe/statuspage-3
This commit is contained in:
commit
feaf1e3272
66 files changed
+3168
-2702
No files matched your search
@@ -115,7 +115,7 @@ TablePaginationActions.propTypes = {
|
||||
* @param {boolean} [props.paginated=false] - Flag to enable pagination.
|
||||
* @param {boolean} [props.reversed=false] - Flag to enable reverse order.
|
||||
* @param {number} props.rowsPerPage- Number of rows per page (table).
|
||||
*
|
||||
* @param {string} props.emptyMessage - Message to display when there is no data.
|
||||
* @example
|
||||
* const data = {
|
||||
* cols: [
|
||||
@@ -149,7 +149,7 @@ TablePaginationActions.propTypes = {
|
||||
* <BasicTable data={data} rows={rows} paginated={true} />
|
||||
*/
|
||||
|
||||
const BasicTable = ({ data, paginated, reversed, table }) => {
|
||||
const BasicTable = ({ data, paginated, reversed, table, emptyMessage = "No data" }) => {
|
||||
const DEFAULT_ROWS_PER_PAGE = 5;
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
@@ -216,7 +216,9 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
|
||||
sx={{
|
||||
cursor: row.handleClick ? "pointer" : "default",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
filter: "brightness(.75)",
|
||||
opacity: 0.75,
|
||||
transition: "filter 0.3s ease, opacity 0.3s ease",
|
||||
},
|
||||
}}
|
||||
key={row.id}
|
||||
@@ -228,6 +230,16 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{displayData.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ textAlign: "center" }}
|
||||
colSpan={data.cols.length}
|
||||
>
|
||||
{emptyMessage}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
@@ -317,6 +329,7 @@ BasicTable.propTypes = {
|
||||
reversed: PropTypes.bool,
|
||||
rowPerPage: PropTypes.number,
|
||||
table: PropTypes.string,
|
||||
emptyMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
export default BasicTable;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Stack, styled } from "@mui/material";
|
||||
|
||||
const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing(20),
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.spacing(2),
|
||||
"& > *": {
|
||||
paddingTop: theme.spacing(12),
|
||||
paddingBottom: theme.spacing(18),
|
||||
},
|
||||
"& > div:first-of-type": {
|
||||
flex: 0.7,
|
||||
borderRight: 1,
|
||||
borderRightStyle: "solid",
|
||||
borderRightColor: theme.palette.border.light,
|
||||
paddingRight: theme.spacing(15),
|
||||
paddingLeft: theme.spacing(15),
|
||||
},
|
||||
"& > div:last-of-type": {
|
||||
flex: 1,
|
||||
paddingRight: theme.spacing(20),
|
||||
paddingLeft: theme.spacing(18),
|
||||
},
|
||||
"& h1, & h2": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
"& p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}));
|
||||
|
||||
export default ConfigBox;
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Box, styled } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* IconBox - A styled box component for rendering icons with consistent sizing and styling
|
||||
*
|
||||
* @component
|
||||
* @param {Object} [props] - Configuration options for the IconBox
|
||||
* @param {number} [props.height=34] - Height of the icon box
|
||||
* @param {number} [props.width=34] - Width of the icon box
|
||||
* @param {number} [props.minWidth=34] - Minimum width of the icon box
|
||||
* @param {number} [props.borderRadius=4] - Border radius of the icon box
|
||||
* @param {number} [props.svgWidth=20] - Width of the SVG icon
|
||||
* @param {number} [props.svgHeight=20] - Height of the SVG icon
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* <IconBox>
|
||||
* <SomeIcon />
|
||||
* </IconBox>
|
||||
*
|
||||
* @example
|
||||
* // Customized usage
|
||||
* <IconBox
|
||||
* height={40}
|
||||
* width={40}
|
||||
* svgWidth={24}
|
||||
* svgHeight={24}
|
||||
* >
|
||||
* <CustomIcon />
|
||||
* </IconBox>
|
||||
*
|
||||
* @returns {React.ReactElement} A styled box containing an icon
|
||||
*/
|
||||
const IconBox = styled(Box)(
|
||||
({
|
||||
theme,
|
||||
height = 34,
|
||||
width = 34,
|
||||
minWidth = 34,
|
||||
borderRadius = 4,
|
||||
svgWidth = 20,
|
||||
svgHeight = 20,
|
||||
}) => ({
|
||||
height: height,
|
||||
minWidth: minWidth,
|
||||
width: width,
|
||||
position: "relative",
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: borderRadius,
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
"& svg": {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: svgWidth,
|
||||
height: svgHeight,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
IconBox.propTypes = {
|
||||
height: PropTypes.number,
|
||||
width: PropTypes.number,
|
||||
minWidth: PropTypes.number,
|
||||
borderRadius: PropTypes.number,
|
||||
svgWidth: PropTypes.number,
|
||||
svgHeight: PropTypes.number,
|
||||
};
|
||||
|
||||
export default IconBox;
|
||||
@@ -42,6 +42,9 @@ const Radio = (props) => {
|
||||
width: 16,
|
||||
height: 16,
|
||||
boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`,
|
||||
"&:not(.Mui-checked)": {
|
||||
boxShadow: `inset 0 0 0 1px ${theme.palette.text.primary}70`, // Use theme text color for the outline
|
||||
},
|
||||
mt: theme.spacing(0.5),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -40,6 +40,7 @@ const SearchAdornment = () => {
|
||||
|
||||
//TODO keep search state inside of component
|
||||
const Search = ({
|
||||
label,
|
||||
id,
|
||||
options,
|
||||
filteredBy,
|
||||
@@ -76,6 +77,14 @@ const Search = ({
|
||||
getOptionLabel={(option) => option[filteredBy]}
|
||||
renderInput={(params) => (
|
||||
<Stack>
|
||||
<Typography
|
||||
component="h3"
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
color={theme.palette.text.secondary}
|
||||
fontWeight={500}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<TextField
|
||||
{...params}
|
||||
error={Boolean(error)}
|
||||
|
||||
@@ -26,7 +26,7 @@ const ProtectedRoute = ({ children }) => {
|
||||
};
|
||||
|
||||
ProtectedRoute.propTypes = {
|
||||
children: PropTypes.elementType.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
@@ -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";
|
||||
@@ -43,6 +44,7 @@ import ChangeLog from "../../assets/icons/changeLog.svg?react";
|
||||
import Docs from "../../assets/icons/docs.svg?react";
|
||||
import Folder from "../../assets/icons/folder.svg?react";
|
||||
import StatusPages from "../../assets/icons/status-pages.svg?react";
|
||||
import ChatBubbleOutlineRoundedIcon from "@mui/icons-material/ChatBubbleOutlineRounded";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
@@ -64,20 +66,31 @@ const menu = [
|
||||
{ name: "Team", path: "account/team", icon: <TeamSvg /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
icon: <Settings />,
|
||||
path: "settings",
|
||||
},
|
||||
{
|
||||
name: "Other",
|
||||
icon: <Folder />,
|
||||
nested: [
|
||||
{ name: "Settings", path: "settings", icon: <Settings /> },
|
||||
{ name: "Support", path: "support", icon: <Support /> },
|
||||
{
|
||||
name: "Discussions",
|
||||
path: "discussions",
|
||||
icon: <ChatBubbleOutlineRoundedIcon />,
|
||||
},
|
||||
{ name: "Docs", path: "docs", icon: <Docs /> },
|
||||
{ name: "Changelog", path: "changelog", icon: <ChangeLog /> },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/* TODO this could be a key in nested Path would be the link */
|
||||
const URL_MAP = {
|
||||
support: "https://github.com/bluewave-labs/bluewave-uptime/issues",
|
||||
support: "https://discord.com/invite/NAb6H3UTjK",
|
||||
discussions: "https://github.com/bluewave-labs/checkmate/discussions",
|
||||
docs: "https://bluewavelabs.gitbook.io/checkmate",
|
||||
changelog: "https://github.com/bluewave-labs/bluewave-uptime/releases",
|
||||
};
|
||||
@@ -546,28 +559,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
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* StatBox Component
|
||||
*
|
||||
* A reusable component that displays a statistic with a heading and subheading
|
||||
* in a styled box with a gradient background.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - The component props
|
||||
* @param {string} props.heading - The primary heading/title of the statistic
|
||||
* @param {string|React.ReactNode} props.subHeading - The value or description of the statistic
|
||||
* @param {Object} [props.sx] - Additional custom styling to be applied to the box
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <StatBox
|
||||
* heading="Total Users"
|
||||
* subHeading="1,234"
|
||||
* sx={{ width: 300 }}
|
||||
* />
|
||||
* )
|
||||
*
|
||||
* @returns {React.ReactElement} A styled box containing the statistic
|
||||
*/
|
||||
|
||||
const StatBox = ({ heading, subHeading, sx }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
minWidth: 200,
|
||||
width: 225,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
|
||||
"& h2": {
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
"& p": {
|
||||
fontSize: 18,
|
||||
color: theme.palette.text.primary,
|
||||
marginTop: theme.spacing(2),
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
marginLeft: theme.spacing(2),
|
||||
fontSize: 15,
|
||||
},
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Typography component="h2">{heading}</Typography>
|
||||
<Typography>{subHeading}</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
StatBox.propTypes = {
|
||||
heading: PropTypes.string.isRequired,
|
||||
subHeading: PropTypes.node.isRequired,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
export default StatBox;
|
||||
@@ -64,7 +64,7 @@ const TeamPanel = () => {
|
||||
};
|
||||
|
||||
fetchTeam();
|
||||
}, [user]);
|
||||
}, [authToken]);
|
||||
|
||||
useEffect(() => {
|
||||
let team = members;
|
||||
@@ -81,8 +81,6 @@ const TeamPanel = () => {
|
||||
{ id: 1, name: "NAME" },
|
||||
{ id: 2, name: "EMAIL" },
|
||||
{ id: 3, name: "ROLE" },
|
||||
// FEATURE STILL TO BE IMPLEMENTED
|
||||
// { id: 4, name: "ACTION" },
|
||||
],
|
||||
rows: team?.map((member, idx) => {
|
||||
const roles = member.role.map((role) => roleMap[role]).join(",");
|
||||
@@ -104,45 +102,19 @@ const TeamPanel = () => {
|
||||
},
|
||||
{ id: idx + 1, data: member.email },
|
||||
{
|
||||
// TODO - Add select dropdown
|
||||
id: idx + 2,
|
||||
data: roles,
|
||||
},
|
||||
// FEATURE STILL TO BE IMPLEMENTED
|
||||
// {
|
||||
// // TODO - Add delete onClick
|
||||
// id: idx + 3,
|
||||
// data: (
|
||||
// <IconButton
|
||||
// aria-label="remove member"
|
||||
// sx={{
|
||||
// "&:focus": {
|
||||
// outline: "none",
|
||||
// },
|
||||
// }}
|
||||
// >
|
||||
// <Remove />
|
||||
// </IconButton>
|
||||
// ),
|
||||
// },
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
setTableData(data);
|
||||
}, [members, filter]);
|
||||
}, [filter, members, roleMap, theme]);
|
||||
useEffect(() => {
|
||||
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
|
||||
}, [errors, toInvite.email]);
|
||||
|
||||
// RENAME ORGANIZATION
|
||||
// const toggleEdit = () => {
|
||||
// setOrgStates((prev) => ({ ...prev, isEdit: !prev.isEdit }));
|
||||
// };
|
||||
// const handleRename = () => {};
|
||||
|
||||
// INVITE MEMBER
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleChange = (event) => {
|
||||
@@ -226,58 +198,11 @@ const TeamPanel = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* FEATURE STILL TO BE IMPLEMENTED */}
|
||||
{/* <Stack component="form">
|
||||
<Box sx={{ alignSelf: "flex-start" }}>
|
||||
<Typography component="h1">Organization name</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
sx={{ height: "34px" }}
|
||||
>
|
||||
<TextField
|
||||
value={orgStates.name}
|
||||
onChange={(event) =>
|
||||
setOrgStates((prev) => ({
|
||||
...prev,
|
||||
name: event.target.value,
|
||||
}))
|
||||
}
|
||||
disabled={!orgStates.isEdit}
|
||||
sx={{
|
||||
color: theme.palette.otherColors.bluishGray,
|
||||
"& .Mui-disabled": {
|
||||
WebkitTextFillColor: "initial !important",
|
||||
},
|
||||
"& .Mui-disabled fieldset": {
|
||||
borderColor: "transparent !important",
|
||||
},
|
||||
}}
|
||||
inputProps={{
|
||||
sx: { textAlign: "end", padding: theme.spacing(4) },
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level={orgStates.isEdit ? "secondary" : "tertiary"}
|
||||
label={orgStates.isEdit ? "Save" : ""}
|
||||
img={!orgStates.isEdit ? <EditSvg /> : ""}
|
||||
onClick={() => toggleEdit()}
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
paddingX: theme.spacing(4),
|
||||
ml: orgStates.isEdit ? theme.spacing(4) : 0,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Divider aria-hidden="true" sx={{ marginY: theme.spacing(4) }} /> */}
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={SPACING_GAP}
|
||||
gap={SPACING_GAP}
|
||||
>
|
||||
<Typography component="h1">Team members</Typography>
|
||||
<Stack
|
||||
@@ -328,6 +253,7 @@ const TeamPanel = () => {
|
||||
paginated={false}
|
||||
reversed={true}
|
||||
table={"team"}
|
||||
emptyMessage={"There are no team members with this role"}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -341,7 +267,7 @@ const TeamPanel = () => {
|
||||
theme={theme}
|
||||
>
|
||||
<TextInput
|
||||
marginBottom={SPACING_GAP}
|
||||
marginBottom={SPACING_GAP}
|
||||
type="email"
|
||||
id="input-team-member"
|
||||
placeholder="Email"
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user