mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-05-02 12:07:02 +00:00
optimizations
This commit is contained in:
@@ -13,7 +13,7 @@ import { LayoutContext } from './context';
|
||||
|
||||
export const DRAWER_WIDTH = 210;
|
||||
|
||||
const Layout: FC<RequiredChildrenProps> = memo(({ children }) => {
|
||||
const LayoutComponent: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [title, setTitle] = useState(PROJECT_NAME);
|
||||
const { pathname } = useLocation();
|
||||
@@ -41,6 +41,8 @@ const Layout: FC<RequiredChildrenProps> = memo(({ children }) => {
|
||||
</Box>
|
||||
</LayoutContext.Provider>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const Layout = memo(LayoutComponent);
|
||||
|
||||
export default Layout;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router';
|
||||
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import { AppBar, IconButton, Toolbar, Typography } from '@mui/material';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
@@ -13,30 +15,47 @@ interface LayoutAppBarProps {
|
||||
onToggleDrawer: () => void;
|
||||
}
|
||||
|
||||
const LayoutAppBar = ({ title, onToggleDrawer }: LayoutAppBarProps) => {
|
||||
// Extract static styles
|
||||
const appBarStyles: SxProps<Theme> = {
|
||||
width: { md: `calc(100% - ${DRAWER_WIDTH}px)` },
|
||||
ml: { md: `${DRAWER_WIDTH}px` },
|
||||
boxShadow: 'none',
|
||||
backgroundColor: '#2e586a'
|
||||
};
|
||||
|
||||
const menuButtonStyles: SxProps<Theme> = {
|
||||
mr: 2,
|
||||
display: { md: 'none' }
|
||||
};
|
||||
|
||||
const backButtonStyles: SxProps<Theme> = {
|
||||
mr: 1,
|
||||
fontSize: 20,
|
||||
verticalAlign: 'middle'
|
||||
};
|
||||
|
||||
const LayoutAppBarComponent = ({ title, onToggleDrawer }: LayoutAppBarProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const pathnames = useLocation()
|
||||
.pathname.split('/')
|
||||
.filter((x) => x);
|
||||
const pathnames = useMemo(
|
||||
() => location.pathname.split('/').filter((x) => x),
|
||||
[location.pathname]
|
||||
);
|
||||
|
||||
const handleBackClick = useCallback(() => {
|
||||
void navigate('/' + pathnames[0]);
|
||||
}, [navigate, pathnames]);
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position="fixed"
|
||||
sx={{
|
||||
width: { md: `calc(100% - ${DRAWER_WIDTH}px)` },
|
||||
ml: { md: `${DRAWER_WIDTH}px` },
|
||||
boxShadow: 'none',
|
||||
backgroundColor: '#2e586a'
|
||||
}}
|
||||
>
|
||||
<AppBar position="fixed" sx={appBarStyles}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
edge="start"
|
||||
onClick={onToggleDrawer}
|
||||
sx={{ mr: 2, display: { md: 'none' } }}
|
||||
sx={menuButtonStyles}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
@@ -44,10 +63,10 @@ const LayoutAppBar = ({ title, onToggleDrawer }: LayoutAppBarProps) => {
|
||||
{pathnames.length > 1 && (
|
||||
<>
|
||||
<IconButton
|
||||
sx={{ mr: 1, fontSize: 20, verticalAlign: 'middle' }}
|
||||
sx={backButtonStyles}
|
||||
color="primary"
|
||||
edge="start"
|
||||
onClick={() => navigate('/' + pathnames[0])}
|
||||
onClick={handleBackClick}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
@@ -70,4 +89,6 @@ const LayoutAppBar = ({ title, onToggleDrawer }: LayoutAppBarProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LayoutAppBar = memo(LayoutAppBarComponent);
|
||||
|
||||
export default LayoutAppBar;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
|
||||
|
||||
import { PROJECT_NAME } from 'env';
|
||||
@@ -21,19 +23,23 @@ interface LayoutDrawerProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const LayoutDrawerProps = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
|
||||
const drawer = (
|
||||
<>
|
||||
<Toolbar disableGutters>
|
||||
<Box display="flex" alignItems="center" px={2}>
|
||||
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
|
||||
<Typography variant="h6">{PROJECT_NAME}</Typography>
|
||||
</Box>
|
||||
<Divider absolute />
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<LayoutMenu />
|
||||
</>
|
||||
const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
|
||||
// Memoize drawer content to prevent unnecessary re-renders
|
||||
const drawer = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<Toolbar disableGutters>
|
||||
<Box display="flex" alignItems="center" px={2}>
|
||||
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
|
||||
<Typography variant="h6">{PROJECT_NAME}</Typography>
|
||||
</Box>
|
||||
<Divider absolute />
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<LayoutMenu />
|
||||
</>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -66,4 +72,6 @@ const LayoutDrawerProps = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutDrawerProps;
|
||||
const LayoutDrawer = memo(LayoutDrawerComponent);
|
||||
|
||||
export default LayoutDrawer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { memo, useCallback, useContext, useMemo, useState } from 'react';
|
||||
|
||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||
import AssessmentIcon from '@mui/icons-material/Assessment';
|
||||
@@ -30,24 +30,31 @@ import LayoutMenuItem from 'components/layout/LayoutMenuItem';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const LayoutMenu = () => {
|
||||
const LayoutMenuComponent = () => {
|
||||
const { me, signOut } = useContext(AuthenticatedContext);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = anchorEl ? 'app-menu-popover' : undefined;
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(true);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const open = useMemo(() => Boolean(anchorEl), [anchorEl]);
|
||||
const id = useMemo(() => (anchorEl ? 'app-menu-popover' : undefined), [anchorEl]);
|
||||
|
||||
const handleClose = () => {
|
||||
const handleClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSignOut = useCallback(() => {
|
||||
signOut(true);
|
||||
}, [signOut]);
|
||||
|
||||
const handleMenuToggle = useCallback(() => {
|
||||
setMenuOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -64,7 +71,7 @@ const LayoutMenu = () => {
|
||||
>
|
||||
<ListItemButton
|
||||
alignItems="flex-start"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
onClick={handleMenuToggle}
|
||||
sx={{
|
||||
pt: 2.5,
|
||||
pb: menuOpen ? 0 : 2.5,
|
||||
@@ -173,7 +180,7 @@ const LayoutMenu = () => {
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
color="primary"
|
||||
onClick={() => signOut(true)}
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
{LL.SIGN_OUT()}
|
||||
</Button>
|
||||
@@ -196,4 +203,6 @@ const LayoutMenu = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const LayoutMenu = memo(LayoutMenuComponent);
|
||||
|
||||
export default LayoutMenu;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Link, useLocation } from 'react-router';
|
||||
|
||||
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
|
||||
import type { SvgIconProps } from '@mui/material';
|
||||
import type { SvgIconProps, SxProps, Theme } from '@mui/material';
|
||||
|
||||
import { routeMatches } from 'utils';
|
||||
|
||||
@@ -12,7 +13,7 @@ interface LayoutMenuItemProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const LayoutMenuItem = ({
|
||||
const LayoutMenuItemComponent = ({
|
||||
icon: Icon,
|
||||
label,
|
||||
to,
|
||||
@@ -20,7 +21,53 @@ const LayoutMenuItem = ({
|
||||
}: LayoutMenuItemProps) => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const selected = routeMatches(to, pathname);
|
||||
const selected = useMemo(() => routeMatches(to, pathname), [to, pathname]);
|
||||
|
||||
// Memoize dynamic styles based on selected state
|
||||
const buttonStyles: SxProps<Theme> = useMemo(
|
||||
() => ({
|
||||
transition: 'all 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||
transform: selected ? 'scale(1.02)' : 'scale(1)',
|
||||
backgroundColor: selected ? 'rgba(144, 202, 249, 0.1)' : 'transparent',
|
||||
borderRadius: '8px',
|
||||
margin: '2px 8px',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(68, 82, 211, 0.39)',
|
||||
transform: selected ? 'scale(1.02)' : 'scale(1.01)'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: selected ? '4px' : '0px',
|
||||
backgroundColor: '#90caf9',
|
||||
borderRadius: '0 2px 2px 0',
|
||||
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
|
||||
}
|
||||
}),
|
||||
[selected]
|
||||
);
|
||||
|
||||
const iconStyles: SxProps<Theme> = useMemo(
|
||||
() => ({
|
||||
color: selected ? '#90caf9' : '#9e9e9e',
|
||||
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||
transform: selected ? 'scale(1.1)' : 'scale(1)',
|
||||
transitionProperty: 'color, transform'
|
||||
}),
|
||||
[selected]
|
||||
);
|
||||
|
||||
const textStyles: SxProps<Theme> = useMemo(
|
||||
() => ({
|
||||
color: selected ? '#90caf9' : '#f5f5f5',
|
||||
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||
transitionProperty: 'color, font-weight'
|
||||
}),
|
||||
[selected]
|
||||
);
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
@@ -28,51 +75,16 @@ const LayoutMenuItem = ({
|
||||
to={to}
|
||||
disabled={disabled || false}
|
||||
selected={selected}
|
||||
sx={{
|
||||
transition: 'all 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||
transform: selected ? 'scale(1.02)' : 'scale(1)',
|
||||
backgroundColor: selected ? 'rgba(144, 202, 249, 0.1)' : 'transparent',
|
||||
borderRadius: '8px',
|
||||
margin: '2px 8px',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(68, 82, 211, 0.39)',
|
||||
transform: selected ? 'scale(1.02)' : 'scale(1.01)'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: selected ? '4px' : '0px',
|
||||
backgroundColor: '#90caf9',
|
||||
borderRadius: '0 2px 2px 0',
|
||||
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
|
||||
}
|
||||
}}
|
||||
sx={buttonStyles}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
color: selected ? '#90caf9' : '#9e9e9e',
|
||||
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||
transform: selected ? 'scale(1.1)' : 'scale(1)',
|
||||
transitionProperty: 'color, transform'
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={iconStyles}>
|
||||
<Icon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
color: selected ? '#90caf9' : '#f5f5f5',
|
||||
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||
// fontWeight: selected ? '600' : '400',
|
||||
transitionProperty: 'color, font-weight'
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</ListItemText>
|
||||
<ListItemText sx={textStyles}>{label}</ListItemText>
|
||||
</ListItemButton>
|
||||
);
|
||||
};
|
||||
|
||||
const LayoutMenuItem = memo(LayoutMenuItemComponent);
|
||||
|
||||
export default LayoutMenuItem;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { memo } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
||||
@@ -20,8 +22,15 @@ interface ListMenuItemProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function RenderIcon({ icon: Icon, bgcolor, label, text }: ListMenuItemProps) {
|
||||
return (
|
||||
// Extract styles to prevent recreation
|
||||
const iconStyles: CSSProperties = {
|
||||
justifyContent: 'right',
|
||||
color: 'lightblue',
|
||||
verticalAlign: 'middle'
|
||||
};
|
||||
|
||||
const RenderIcon = memo(
|
||||
({ icon: Icon, bgcolor, label, text }: ListMenuItemProps) => (
|
||||
<>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor, color: 'white' }}>
|
||||
@@ -30,8 +39,8 @@ function RenderIcon({ icon: Icon, bgcolor, label, text }: ListMenuItemProps) {
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={label} secondary={text} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const LayoutMenuItem = ({
|
||||
icon,
|
||||
@@ -46,13 +55,7 @@ const LayoutMenuItem = ({
|
||||
<ListItem
|
||||
disablePadding
|
||||
secondaryAction={
|
||||
<ListItemIcon
|
||||
style={{
|
||||
justifyContent: 'right',
|
||||
color: 'lightblue',
|
||||
verticalAlign: 'middle'
|
||||
}}
|
||||
>
|
||||
<ListItemIcon style={iconStyles}>
|
||||
<NavigateNextIcon />
|
||||
</ListItemIcon>
|
||||
}
|
||||
@@ -79,4 +82,4 @@ const LayoutMenuItem = ({
|
||||
</>
|
||||
);
|
||||
|
||||
export default LayoutMenuItem;
|
||||
export default memo(LayoutMenuItem);
|
||||
|
||||
Reference in New Issue
Block a user