optimizations

This commit is contained in:
proddy
2025-10-28 22:19:08 +01:00
parent 55b893362c
commit 3abfb7bb9c
93 changed files with 3953 additions and 3361 deletions

View File

@@ -19,6 +19,4 @@ const ButtonRow = memo<BoxProps>(({ children, ...rest }) => (
</Box>
));
ButtonRow.displayName = 'ButtonRow';
export default ButtonRow;

View File

@@ -1,11 +1,11 @@
import type { FC } from 'react';
import { type FC, memo, useMemo } from 'react';
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
import ErrorIcon from '@mui/icons-material/Error';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined';
import { Box, Typography, useTheme } from '@mui/material';
import type { BoxProps, SvgIconProps, Theme } from '@mui/material';
import type { BoxProps, SvgIconProps } from '@mui/material';
type MessageBoxLevel = 'warning' | 'success' | 'info' | 'error';
@@ -14,22 +14,18 @@ export interface MessageBoxProps extends BoxProps {
message?: string;
}
const LEVEL_ICONS: {
[type in MessageBoxLevel]: React.ComponentType<SvgIconProps>;
} = {
const LEVEL_ICONS: Record<MessageBoxLevel, React.ComponentType<SvgIconProps>> = {
success: CheckCircleOutlineOutlinedIcon,
info: InfoOutlinedIcon,
warning: ReportProblemOutlinedIcon,
error: ErrorIcon
};
const LEVEL_BACKGROUNDS: {
[type in MessageBoxLevel]: (theme: Theme) => string;
} = {
success: (theme: Theme) => theme.palette.success.dark,
info: (theme: Theme) => theme.palette.info.main,
warning: (theme: Theme) => theme.palette.warning.dark,
error: (theme: Theme) => theme.palette.error.dark
const LEVEL_PALETTE_PATHS: Record<MessageBoxLevel, string> = {
success: 'success.dark',
info: 'info.main',
warning: 'warning.dark',
error: 'error.dark'
};
const MessageBox: FC<MessageBoxProps> = ({
@@ -40,25 +36,38 @@ const MessageBox: FC<MessageBoxProps> = ({
...rest
}) => {
const theme = useTheme();
const Icon = LEVEL_ICONS[level];
const backgroundColor = LEVEL_BACKGROUNDS[level](theme);
const color = 'white';
const { Icon, backgroundColor } = useMemo(() => {
const Icon = LEVEL_ICONS[level];
const palettePath = LEVEL_PALETTE_PATHS[level];
const [key, shade] = palettePath.split('.') as [
keyof typeof theme.palette,
string
];
const paletteKey = theme.palette[key] as unknown as Record<string, string>;
const backgroundColor = paletteKey[shade];
return { Icon, backgroundColor };
}, [level, theme]);
return (
<Box
p={2}
display="flex"
alignItems="center"
borderRadius={1}
sx={{ backgroundColor, color, ...sx }}
sx={{ backgroundColor, color: 'white', ...sx }}
{...rest}
>
<Icon />
<Typography sx={{ ml: 2 }} variant="body1">
{message ?? ''}
</Typography>
{children}
{(message || children) && (
<Typography sx={{ ml: 2 }} variant="body1">
{message}
{children}
</Typography>
)}
</Box>
);
};
export default MessageBox;
export default memo(MessageBox);

View File

@@ -1,6 +1,8 @@
import { memo } from 'react';
import type { FC } from 'react';
import { Paper } from '@mui/material';
import type { SxProps, Theme } from '@mui/material/styles';
import type { RequiredChildrenProps } from 'utils';
@@ -8,16 +10,19 @@ interface SectionContentProps extends RequiredChildrenProps {
id?: string;
}
const SectionContent: FC<SectionContentProps> = (props) => {
const { children, id } = props;
return (
<Paper
id={id}
sx={{ p: 1.5, m: 1.5, borderRadius: 3, border: '1px solid rgb(65, 65, 65)' }}
>
{children}
</Paper>
);
// Extract styles to avoid recreation on every render
const paperStyles: SxProps<Theme> = {
p: 1.5,
m: 1.5,
borderRadius: 3,
border: '1px solid rgb(65, 65, 65)'
};
export default SectionContent;
const SectionContent: FC<SectionContentProps> = ({ children, id }) => (
<Paper id={id} sx={paperStyles}>
{children}
</Paper>
);
// Memoize to prevent unnecessary re-renders
export default memo(SectionContent);

View File

@@ -1,3 +1,4 @@
import { memo } from 'react';
import type { FC } from 'react';
import { FormControlLabel } from '@mui/material';
@@ -9,4 +10,4 @@ const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
</div>
);
export default BlockFormControlLabel;
export default memo(BlockFormControlLabel);

View File

@@ -1,4 +1,6 @@
import { type ChangeEventHandler, useContext } from 'react';
import { memo, useCallback, useContext, useMemo } from 'react';
import type { ChangeEventHandler } from 'react';
import type { CSSProperties } from 'react';
import { MenuItem, TextField } from '@mui/material';
@@ -17,17 +19,54 @@ import { I18nContext } from 'i18n/i18n-react';
import type { Locales } from 'i18n/i18n-types';
import { loadLocaleAsync } from 'i18n/i18n-util.async';
// Extract style to constant to prevent recreation
const flagStyle: CSSProperties = { width: 16, verticalAlign: 'middle' };
// Define language options outside component to prevent recreation
interface LanguageOption {
key: Locales;
flag: string;
label: string;
}
const LANGUAGE_OPTIONS: LanguageOption[] = [
{ key: 'cz', flag: CZflag, label: 'CZ' },
{ key: 'de', flag: DEflag, label: 'DE' },
{ key: 'en', flag: GBflag, label: 'EN' },
{ key: 'fr', flag: FRflag, label: 'FR' },
{ key: 'it', flag: ITflag, label: 'IT' },
{ key: 'nl', flag: NLflag, label: 'NL' },
{ key: 'no', flag: NOflag, label: 'NO' },
{ key: 'pl', flag: PLflag, label: 'PL' },
{ key: 'sk', flag: SKflag, label: 'SK' },
{ key: 'sv', flag: SVflag, label: 'SV' },
{ key: 'tr', flag: TRflag, label: 'TR' }
];
const LanguageSelector = () => {
const { setLocale, locale } = useContext(I18nContext);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({
target
}) => {
const loc = target.value as Locales;
localStorage.setItem('lang', loc);
await loadLocaleAsync(loc);
setLocale(loc);
};
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = useCallback(
async ({ target }) => {
const loc = target.value as Locales;
localStorage.setItem('lang', loc);
await loadLocaleAsync(loc);
setLocale(loc);
},
[setLocale]
);
// Memoize menu items to prevent recreation on every render
const menuItems = useMemo(
() =>
LANGUAGE_OPTIONS.map(({ key, flag, label }) => (
<MenuItem key={key} value={key}>
<img src={flag} style={flagStyle} alt={label} />
&nbsp;{label}
</MenuItem>
)),
[]
);
return (
<TextField
@@ -38,52 +77,9 @@ const LanguageSelector = () => {
size="small"
select
>
<MenuItem key="cz" value="cz">
<img src={CZflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;CZ
</MenuItem>
<MenuItem key="de" value="de">
<img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;DE
</MenuItem>
<MenuItem key="en" value="en">
<img src={GBflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;EN
</MenuItem>
<MenuItem key="fr" value="fr">
<img src={FRflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;FR
</MenuItem>
<MenuItem key="it" value="it">
<img src={ITflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;IT
</MenuItem>
<MenuItem key="nl" value="nl">
<img src={NLflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;NL
</MenuItem>
<MenuItem key="no" value="no">
<img src={NOflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;NO
</MenuItem>
<MenuItem key="pl" value="pl">
<img src={PLflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;PL
</MenuItem>
<MenuItem key="sk" value="sk">
<img src={SKflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;SK
</MenuItem>
<MenuItem key="sv" value="sv">
<img src={SVflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;SV
</MenuItem>
<MenuItem key="tr" value="tr">
<img src={TRflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;TR
</MenuItem>
{menuItems}
</TextField>
);
};
export default LanguageSelector;
export default memo(LanguageSelector);

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { memo, useCallback, useState } from 'react';
import type { FC } from 'react';
import VisibilityIcon from '@mui/icons-material/Visibility';
@@ -13,6 +13,10 @@ type ValidatedPasswordFieldProps = Omit<ValidatedTextFieldProps, 'type'>;
const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({ ...props }) => {
const [showPassword, setShowPassword] = useState<boolean>(false);
const togglePasswordVisibility = useCallback(() => {
setShowPassword((prev) => !prev);
}, []);
return (
<ValidatedTextField
{...props}
@@ -21,7 +25,7 @@ const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({ ...props }) =
input: {
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword(!showPassword)} edge="end">
<IconButton onClick={togglePasswordVisibility} edge="end">
{showPassword ? <VisibilityIcon /> : <VisibilityOffIcon />}
</IconButton>
</InputAdornment>
@@ -32,4 +36,4 @@ const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({ ...props }) =
);
};
export default ValidatedPasswordField;
export default memo(ValidatedPasswordField);

View File

@@ -1,3 +1,4 @@
import { memo } from 'react';
import type { FC } from 'react';
import { FormHelperText, TextField } from '@mui/material';
@@ -28,4 +29,4 @@ const ValidatedTextField: FC<ValidatedTextFieldProps> = ({
);
};
export default ValidatedTextField;
export default memo(ValidatedTextField);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -1,3 +1,5 @@
import { memo } from 'react';
import RefreshIcon from '@mui/icons-material/Refresh';
import { Box, Button, CircularProgress } from '@mui/material';
@@ -9,7 +11,7 @@ interface FormLoaderProps {
onRetry?: () => void;
}
const FormLoader = ({ errorMessage, onRetry }: FormLoaderProps) => {
const FormLoaderComponent = ({ errorMessage, onRetry }: FormLoaderProps) => {
const { LL } = useI18nContext();
if (errorMessage) {
@@ -38,4 +40,6 @@ const FormLoader = ({ errorMessage, onRetry }: FormLoaderProps) => {
);
};
const FormLoader = memo(FormLoaderComponent);
export default FormLoader;

View File

@@ -1,20 +1,22 @@
import { memo } from 'react';
import { Box, CircularProgress } from '@mui/material';
import type { SxProps, Theme } from '@mui/material';
// Extract styles to prevent recreation on every render
const containerStyles: SxProps<Theme> = {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '200px',
backgroundColor: 'background.default',
borderRadius: 1,
border: '1px solid',
borderColor: 'divider'
};
const LazyLoader = memo(() => (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="200px"
sx={{
backgroundColor: 'background.default',
borderRadius: 1,
border: '1px solid',
borderColor: 'divider'
}}
>
<Box sx={containerStyles}>
<CircularProgress size={40} />
</Box>
));

View File

@@ -1,10 +1,18 @@
import { memo } from 'react';
import { Box, CircularProgress } from '@mui/material';
import type { Theme } from '@mui/material';
import type { SxProps, Theme } from '@mui/material';
interface LoadingSpinnerProps {
height?: number | string;
}
// Extract styles to prevent recreation on every render
const circularProgressStyles: SxProps<Theme> = (theme: Theme) => ({
margin: theme.spacing(4),
color: theme.palette.text.secondary
});
const LoadingSpinner = ({ height = '100%' }: LoadingSpinnerProps) => {
return (
<Box
@@ -15,15 +23,9 @@ const LoadingSpinner = ({ height = '100%' }: LoadingSpinnerProps) => {
padding={2}
height={height}
>
<CircularProgress
sx={(theme: Theme) => ({
margin: theme.spacing(4),
color: theme.palette.text.secondary
})}
size={100}
/>
<CircularProgress sx={circularProgressStyles} size={100} />
</Box>
);
};
export default LoadingSpinner;
export default memo(LoadingSpinner);

View File

@@ -1,3 +1,4 @@
import { memo, useCallback } from 'react';
import type { Blocker } from 'react-router';
import {
@@ -14,23 +15,23 @@ import { useI18nContext } from 'i18n/i18n-react';
const BlockNavigation = ({ blocker }: { blocker: Blocker }) => {
const { LL } = useI18nContext();
const handleReset = useCallback(() => {
blocker.reset?.();
}, [blocker]);
const handleProceed = useCallback(() => {
blocker.proceed?.();
}, [blocker]);
return (
<Dialog sx={dialogStyle} open={blocker.state === 'blocked'}>
<DialogTitle>{LL.BLOCK_NAVIGATE_1()}</DialogTitle>
<DialogContent dividers>{LL.BLOCK_NAVIGATE_2()}</DialogContent>
<DialogActions>
<Button
variant="outlined"
onClick={() => blocker.reset?.()}
color="secondary"
>
<Button variant="outlined" onClick={handleReset} color="secondary">
{LL.STAY()}
</Button>
<Button
variant="contained"
onClick={() => blocker.proceed?.()}
color="primary"
>
<Button variant="contained" onClick={handleProceed} color="primary">
{LL.LEAVE()}
</Button>
</DialogActions>
@@ -38,4 +39,4 @@ const BlockNavigation = ({ blocker }: { blocker: Blocker }) => {
);
};
export default BlockNavigation;
export default memo(BlockNavigation);

View File

@@ -1,4 +1,4 @@
import { useContext } from 'react';
import { memo, useContext } from 'react';
import type { FC } from 'react';
import { Navigate } from 'react-router';
@@ -14,4 +14,4 @@ const RequireAdmin: FC<RequiredChildrenProps> = ({ children }) => {
);
};
export default RequireAdmin;
export default memo(RequireAdmin);

View File

@@ -1,4 +1,4 @@
import { useContext, useEffect } from 'react';
import { memo, useContext, useEffect } from 'react';
import type { FC } from 'react';
import { Navigate, useLocation } from 'react-router';
@@ -18,7 +18,7 @@ const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
if (!authenticationContext.me) {
storeLoginRedirect(location);
}
});
}, [authenticationContext.me, location]);
return authenticationContext.me ? (
<AuthenticatedContext.Provider
@@ -31,4 +31,4 @@ const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
);
};
export default RequireAuthenticated;
export default memo(RequireAuthenticated);

View File

@@ -1,4 +1,4 @@
import { useContext } from 'react';
import { memo, useContext } from 'react';
import type { FC } from 'react';
import { Navigate } from 'react-router';
@@ -16,4 +16,4 @@ const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
);
};
export default RequireUnauthenticated;
export default memo(RequireUnauthenticated);

View File

@@ -1,3 +1,4 @@
import { memo, useCallback } from 'react';
import type { FC } from 'react';
import { useNavigate } from 'react-router';
@@ -15,9 +16,12 @@ const RouterTabs: FC<RouterTabsProps> = ({ value, children }) => {
const theme = useTheme();
const smallDown = useMediaQuery(theme.breakpoints.down('sm'));
const handleTabChange = (_event: unknown, path: string) => {
void navigate(path);
};
const handleTabChange = useCallback(
(_event: unknown, path: string) => {
void navigate(path);
},
[navigate]
);
return (
<Tabs
@@ -30,4 +34,4 @@ const RouterTabs: FC<RouterTabsProps> = ({ value, children }) => {
);
};
export default RouterTabs;
export default memo(RouterTabs);

View File

@@ -13,8 +13,14 @@ export const verifyAuthorization = () =>
export const signIn = (request: SignInRequest) =>
alovaInstance.Post<SignInResponse>('/rest/signIn', request);
// Cache storage reference to avoid repeated checks
let cachedStorage: Storage | undefined;
export function getStorage() {
return localStorage || sessionStorage;
if (!cachedStorage) {
cachedStorage = localStorage || sessionStorage;
}
return cachedStorage;
}
export function storeLoginRedirect(location?: { pathname: string; search: string }) {