698 lines
24 KiB
TypeScript
698 lines
24 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
AppBar,
|
|
Box,
|
|
CssBaseline,
|
|
Drawer,
|
|
IconButton,
|
|
List,
|
|
ListItem,
|
|
ListItemButton,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
Toolbar,
|
|
Typography,
|
|
Avatar,
|
|
Menu,
|
|
MenuItem,
|
|
Divider,
|
|
useTheme as useMuiTheme,
|
|
useMediaQuery,
|
|
Badge,
|
|
} from '@mui/material';
|
|
import {
|
|
Menu as MenuIcon,
|
|
Dashboard as DashboardIcon,
|
|
CloudUpload as UploadIcon,
|
|
Search as SearchIcon,
|
|
Folder as FolderIcon,
|
|
Settings as SettingsIcon,
|
|
Notifications as NotificationsIcon,
|
|
AccountCircle as AccountIcon,
|
|
Logout as LogoutIcon,
|
|
Description as DocumentIcon,
|
|
Storage as StorageIcon,
|
|
Error as ErrorIcon,
|
|
Label as LabelIcon,
|
|
Block as BlockIcon,
|
|
Api as ApiIcon,
|
|
ManageAccounts as ManageIcon,
|
|
BugReport as BugReportIcon,
|
|
} from '@mui/icons-material';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
import { useNotifications } from '../../contexts/NotificationContext';
|
|
import GlobalSearchBar from '../GlobalSearchBar';
|
|
import ThemeToggle from '../ThemeToggle/ThemeToggle';
|
|
import NotificationPanel from '../Notifications/NotificationPanel';
|
|
import LanguageSwitcher from '../LanguageSwitcher';
|
|
import BottomNavigation from './BottomNavigation';
|
|
import { usePWA } from '../../hooks/usePWA';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
const drawerWidth = 280;
|
|
|
|
interface NavigationItem {
|
|
textKey: string;
|
|
icon: React.ComponentType<any>;
|
|
path: string;
|
|
}
|
|
|
|
interface AppLayoutProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
interface User {
|
|
username?: string;
|
|
email?: string;
|
|
}
|
|
|
|
const getNavigationItems = (t: (key: string) => string): NavigationItem[] => [
|
|
{ textKey: 'navigation.dashboard', icon: DashboardIcon, path: '/dashboard' },
|
|
{ textKey: 'navigation.upload', icon: UploadIcon, path: '/upload' },
|
|
{ textKey: 'navigation.documents', icon: DocumentIcon, path: '/documents' },
|
|
{ textKey: 'navigation.search', icon: SearchIcon, path: '/search' },
|
|
{ textKey: 'navigation.labels', icon: LabelIcon, path: '/labels' },
|
|
{ textKey: 'navigation.sources', icon: StorageIcon, path: '/sources' },
|
|
{ textKey: 'navigation.watchFolder', icon: FolderIcon, path: '/watch' },
|
|
{ textKey: 'navigation.documentManagement', icon: ManageIcon, path: '/documents/management' },
|
|
{ textKey: 'navigation.ignoredFiles', icon: BlockIcon, path: '/ignored-files' },
|
|
];
|
|
|
|
const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
|
|
const theme = useMuiTheme();
|
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
const isPWA = usePWA();
|
|
const [mobileOpen, setMobileOpen] = useState<boolean>(false);
|
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
const [notificationAnchorEl, setNotificationAnchorEl] = useState<null | HTMLElement>(null);
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { user, logout } = useAuth();
|
|
const { unreadCount } = useNotifications();
|
|
const { t } = useTranslation();
|
|
|
|
const navigationItems = getNavigationItems(t);
|
|
|
|
const handleDrawerToggle = (): void => {
|
|
setMobileOpen(!mobileOpen);
|
|
};
|
|
|
|
const handleProfileMenuOpen = (event: React.MouseEvent<HTMLElement>): void => {
|
|
setAnchorEl(event.currentTarget);
|
|
};
|
|
|
|
const handleProfileMenuClose = (): void => {
|
|
setAnchorEl(null);
|
|
};
|
|
|
|
const handleLogout = (): void => {
|
|
logout();
|
|
handleProfileMenuClose();
|
|
navigate('/login');
|
|
};
|
|
|
|
const handleNotificationClick = (event: React.MouseEvent<HTMLElement>): void => {
|
|
setNotificationAnchorEl(notificationAnchorEl ? null : event.currentTarget);
|
|
};
|
|
|
|
const handleNotificationClose = (): void => {
|
|
setNotificationAnchorEl(null);
|
|
};
|
|
|
|
const drawer = (
|
|
<Box sx={{
|
|
height: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(180deg, rgba(255,255,255,0.95) 0%, rgba(248,250,252,0.95) 100%)'
|
|
: 'linear-gradient(180deg, rgba(30,30,30,0.95) 0%, rgba(18,18,18,0.95) 100%)',
|
|
backdropFilter: 'blur(20px)',
|
|
borderRight: theme.palette.mode === 'light'
|
|
? '1px solid rgba(226,232,240,0.5)'
|
|
: '1px solid rgba(255,255,255,0.1)',
|
|
}}>
|
|
{/* Logo Section */}
|
|
<Box sx={{
|
|
p: 3,
|
|
borderBottom: theme.palette.mode === 'light'
|
|
? '1px solid rgba(226,232,240,0.3)'
|
|
: '1px solid rgba(255,255,255,0.1)',
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, rgba(99,102,241,0.05) 0%, rgba(139,92,246,0.05) 100%)'
|
|
: 'linear-gradient(135deg, rgba(99,102,241,0.1) 0%, rgba(139,92,246,0.1) 100%)',
|
|
}}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<Box
|
|
sx={{
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 3,
|
|
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #ec4899 100%)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: 'white',
|
|
fontWeight: 800,
|
|
fontSize: '1.3rem',
|
|
boxShadow: '0 8px 32px rgba(99,102,241,0.3)',
|
|
position: 'relative',
|
|
'&::before': {
|
|
content: '""',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
borderRadius: 3,
|
|
background: 'linear-gradient(135deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.1) 100%)',
|
|
backdropFilter: 'blur(10px)',
|
|
},
|
|
}}
|
|
>
|
|
<Box sx={{ position: 'relative', zIndex: 1 }}>
|
|
<img
|
|
src="/readur-32.png"
|
|
srcSet="/readur-32.png 1x, /readur-64.png 2x"
|
|
alt="Readur Logo"
|
|
style={{
|
|
width: '32px',
|
|
height: '32px',
|
|
objectFit: 'contain',
|
|
}}
|
|
onError={(e) => {
|
|
// Fallback to "R" if image fails to load
|
|
e.currentTarget.style.display = 'none';
|
|
e.currentTarget.parentElement!.innerHTML = 'R';
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
<Box>
|
|
<Typography variant="h6" sx={{
|
|
fontWeight: 800,
|
|
color: 'text.primary',
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, #1e293b 0%, #6366f1 100%)'
|
|
: 'linear-gradient(135deg, #f8fafc 0%, #a855f7 100%)',
|
|
backgroundClip: 'text',
|
|
WebkitBackgroundClip: 'text',
|
|
WebkitTextFillColor: 'transparent',
|
|
letterSpacing: '-0.025em',
|
|
}}>
|
|
{t('common.appName')}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{
|
|
color: 'text.secondary',
|
|
fontWeight: 500,
|
|
letterSpacing: '0.05em',
|
|
textTransform: 'uppercase',
|
|
fontSize: '0.7rem',
|
|
}}>
|
|
{t('common.appTagline')}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Navigation */}
|
|
<List sx={{
|
|
flex: 1,
|
|
px: 3,
|
|
py: 2,
|
|
overflowY: 'auto',
|
|
// Custom scrollbar styling
|
|
'&::-webkit-scrollbar': {
|
|
width: '8px',
|
|
borderRadius: '4px',
|
|
},
|
|
'&::-webkit-scrollbar-track': {
|
|
background: theme.palette.mode === 'light'
|
|
? 'rgba(226,232,240,0.3)'
|
|
: 'rgba(255,255,255,0.1)',
|
|
borderRadius: '4px',
|
|
margin: '8px 0',
|
|
},
|
|
'&::-webkit-scrollbar-thumb': {
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, rgba(99,102,241,0.6) 0%, rgba(139,92,246,0.6) 100%)'
|
|
: 'linear-gradient(135deg, rgba(99,102,241,0.8) 0%, rgba(139,92,246,0.8) 100%)',
|
|
borderRadius: '4px',
|
|
border: theme.palette.mode === 'light'
|
|
? '1px solid rgba(255,255,255,0.3)'
|
|
: '1px solid rgba(255,255,255,0.1)',
|
|
transition: 'all 0.2s ease-in-out',
|
|
'&:hover': {
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, rgba(99,102,241,0.8) 0%, rgba(139,92,246,0.8) 100%)'
|
|
: 'linear-gradient(135deg, rgba(99,102,241,1) 0%, rgba(139,92,246,1) 100%)',
|
|
transform: 'scale(1.1)',
|
|
},
|
|
},
|
|
'&::-webkit-scrollbar-thumb:active': {
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, rgba(99,102,241,0.9) 0%, rgba(139,92,246,0.9) 100%)'
|
|
: 'linear-gradient(135deg, rgba(99,102,241,1) 0%, rgba(139,92,246,1) 100%)',
|
|
},
|
|
// Firefox scrollbar styling
|
|
scrollbarWidth: 'thin',
|
|
scrollbarColor: theme.palette.mode === 'light'
|
|
? 'rgba(99,102,241,0.6) rgba(226,232,240,0.3)'
|
|
: 'rgba(99,102,241,0.8) rgba(255,255,255,0.1)',
|
|
}}>
|
|
{navigationItems.map((item) => {
|
|
const isActive = location.pathname === item.path;
|
|
const Icon = item.icon;
|
|
|
|
return (
|
|
<ListItem key={item.textKey} sx={{ px: 0, mb: 1 }}>
|
|
<ListItemButton
|
|
onClick={() => navigate(item.path)}
|
|
sx={{
|
|
borderRadius: 3,
|
|
minHeight: 52,
|
|
px: 2.5,
|
|
py: 1.5,
|
|
background: isActive
|
|
? 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)'
|
|
: 'transparent',
|
|
color: isActive ? 'white' : 'text.primary',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
transition: 'all 0.2s ease-in-out',
|
|
'&:hover': {
|
|
backgroundColor: isActive ? 'transparent' : 'rgba(99,102,241,0.08)',
|
|
transform: isActive ? 'none' : 'translateX(4px)',
|
|
'&::before': isActive ? {} : {
|
|
content: '""',
|
|
position: 'absolute',
|
|
left: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
width: '3px',
|
|
background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)',
|
|
borderRadius: '0 2px 2px 0',
|
|
},
|
|
},
|
|
'&::after': isActive ? {
|
|
content: '""',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
|
backdropFilter: 'blur(10px)',
|
|
} : {},
|
|
'& .MuiListItemIcon-root': {
|
|
color: isActive ? 'white' : 'text.secondary',
|
|
minWidth: 36,
|
|
position: 'relative',
|
|
zIndex: 1,
|
|
},
|
|
'& .MuiListItemText-root': {
|
|
position: 'relative',
|
|
zIndex: 1,
|
|
},
|
|
...(isActive && {
|
|
boxShadow: '0 8px 32px rgba(99,102,241,0.3)',
|
|
}),
|
|
}}
|
|
>
|
|
<ListItemIcon>
|
|
<Icon sx={{ fontSize: '1.25rem' }} />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary={t(item.textKey)}
|
|
primaryTypographyProps={{
|
|
fontSize: '0.9rem',
|
|
fontWeight: isActive ? 600 : 500,
|
|
letterSpacing: '0.025em',
|
|
}}
|
|
/>
|
|
</ListItemButton>
|
|
</ListItem>
|
|
);
|
|
})}
|
|
</List>
|
|
|
|
{/* User Info */}
|
|
<Box sx={{
|
|
p: 3,
|
|
borderTop: theme.palette.mode === 'light'
|
|
? '1px solid rgba(226,232,240,0.3)'
|
|
: '1px solid rgba(255,255,255,0.1)',
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, rgba(99,102,241,0.03) 0%, rgba(139,92,246,0.03) 100%)'
|
|
: 'linear-gradient(135deg, rgba(99,102,241,0.08) 0%, rgba(139,92,246,0.08) 100%)',
|
|
}}>
|
|
<Box sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 2.5,
|
|
p: 2,
|
|
borderRadius: 3,
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(248,250,252,0.6) 100%)'
|
|
: 'linear-gradient(135deg, rgba(50,50,50,0.8) 0%, rgba(30,30,30,0.6) 100%)',
|
|
backdropFilter: 'blur(10px)',
|
|
border: theme.palette.mode === 'light'
|
|
? '1px solid rgba(255,255,255,0.3)'
|
|
: '1px solid rgba(255,255,255,0.1)',
|
|
boxShadow: theme.palette.mode === 'light'
|
|
? '0 4px 16px rgba(0,0,0,0.04)'
|
|
: '0 4px 16px rgba(0,0,0,0.2)',
|
|
}}>
|
|
<Avatar
|
|
sx={{
|
|
width: 42,
|
|
height: 42,
|
|
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
|
fontSize: '1rem',
|
|
fontWeight: 600,
|
|
boxShadow: '0 4px 16px rgba(99,102,241,0.3)',
|
|
}}
|
|
>
|
|
{user?.username?.charAt(0).toUpperCase()}
|
|
</Avatar>
|
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
<Typography variant="body2" sx={{
|
|
fontWeight: 600,
|
|
color: 'text.primary',
|
|
letterSpacing: '0.025em',
|
|
}}>
|
|
{user?.username}
|
|
</Typography>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{
|
|
color: 'text.secondary',
|
|
display: 'block',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
fontSize: '0.75rem',
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{user?.email}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
|
|
return (
|
|
<Box sx={{ display: 'flex' }}>
|
|
<CssBaseline />
|
|
|
|
{/* App Bar */}
|
|
<AppBar
|
|
position="fixed"
|
|
sx={{
|
|
width: { md: `calc(100% - ${drawerWidth}px)` },
|
|
ml: { md: `${drawerWidth}px` },
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(248,250,252,0.90) 100%)'
|
|
: 'linear-gradient(135deg, rgba(30,30,30,0.95) 0%, rgba(18,18,18,0.90) 100%)',
|
|
backdropFilter: 'blur(20px)',
|
|
borderBottom: theme.palette.mode === 'light'
|
|
? '1px solid rgba(226,232,240,0.5)'
|
|
: '1px solid rgba(255,255,255,0.1)',
|
|
boxShadow: theme.palette.mode === 'light'
|
|
? '0 4px 32px rgba(0,0,0,0.04)'
|
|
: '0 4px 32px rgba(0,0,0,0.2)',
|
|
}}
|
|
>
|
|
<Toolbar>
|
|
<IconButton
|
|
color="inherit"
|
|
aria-label="open drawer"
|
|
edge="start"
|
|
onClick={handleDrawerToggle}
|
|
sx={{ mr: 2, display: { md: 'none' } }}
|
|
>
|
|
<MenuIcon />
|
|
</IconButton>
|
|
|
|
<Typography variant="h6" noWrap component="div" sx={{
|
|
fontWeight: 700,
|
|
mr: 1,
|
|
fontSize: '1.1rem',
|
|
display: isPWA ? 'none' : 'block',
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, #1e293b 0%, #6366f1 100%)'
|
|
: 'linear-gradient(135deg, #f8fafc 0%, #a855f7 100%)',
|
|
backgroundClip: 'text',
|
|
WebkitBackgroundClip: 'text',
|
|
WebkitTextFillColor: 'transparent',
|
|
letterSpacing: '-0.025em',
|
|
}}>
|
|
{navigationItems.find(item => item.path === location.pathname)?.textKey
|
|
? t(navigationItems.find(item => item.path === location.pathname)!.textKey)
|
|
: t('navigation.dashboard')}
|
|
</Typography>
|
|
|
|
{/* Global Search Bar */}
|
|
<Box sx={{
|
|
flexGrow: 2,
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
mx: { xs: 0.5, md: 1 },
|
|
flex: '1 1 auto',
|
|
minWidth: { xs: 0, md: 'auto' },
|
|
overflow: 'hidden',
|
|
}}>
|
|
<GlobalSearchBar />
|
|
</Box>
|
|
|
|
{/* Notifications */}
|
|
<IconButton
|
|
onClick={handleNotificationClick}
|
|
sx={{
|
|
mr: { xs: 1, md: 2 },
|
|
display: isPWA ? 'none' : 'flex',
|
|
color: 'text.secondary',
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(248,250,252,0.6) 100%)'
|
|
: 'linear-gradient(135deg, rgba(50,50,50,0.8) 0%, rgba(30,30,30,0.6) 100%)',
|
|
backdropFilter: 'blur(10px)',
|
|
border: theme.palette.mode === 'light'
|
|
? '1px solid rgba(255,255,255,0.3)'
|
|
: '1px solid rgba(255,255,255,0.1)',
|
|
borderRadius: 2.5,
|
|
width: 44,
|
|
height: 44,
|
|
transition: 'all 0.2s ease-in-out',
|
|
'&:hover': {
|
|
background: 'linear-gradient(135deg, rgba(99,102,241,0.1) 0%, rgba(139,92,246,0.1) 100%)',
|
|
transform: 'translateY(-2px)',
|
|
boxShadow: '0 8px 24px rgba(99,102,241,0.15)',
|
|
},
|
|
}}
|
|
>
|
|
<Badge
|
|
badgeContent={unreadCount}
|
|
sx={{
|
|
'& .MuiBadge-badge': {
|
|
background: 'linear-gradient(135deg, #ef4444 0%, #f97316 100%)',
|
|
color: 'white',
|
|
fontWeight: 600,
|
|
fontSize: '0.7rem',
|
|
},
|
|
}}
|
|
>
|
|
<NotificationsIcon sx={{ fontSize: '1.25rem' }} />
|
|
</Badge>
|
|
</IconButton>
|
|
|
|
{/* Language Switcher */}
|
|
<Box sx={{
|
|
mr: { xs: 1, md: 2 },
|
|
display: isPWA ? 'none' : { xs: 'none', sm: 'block' },
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(248,250,252,0.6) 100%)'
|
|
: 'linear-gradient(135deg, rgba(50,50,50,0.8) 0%, rgba(30,30,30,0.6) 100%)',
|
|
backdropFilter: 'blur(10px)',
|
|
border: theme.palette.mode === 'light'
|
|
? '1px solid rgba(255,255,255,0.3)'
|
|
: '1px solid rgba(255,255,255,0.1)',
|
|
borderRadius: 2.5,
|
|
transition: 'all 0.2s ease-in-out',
|
|
'&:hover': {
|
|
background: 'linear-gradient(135deg, rgba(99,102,241,0.1) 0%, rgba(139,92,246,0.1) 100%)',
|
|
transform: 'translateY(-2px)',
|
|
boxShadow: '0 8px 24px rgba(99,102,241,0.15)',
|
|
},
|
|
}}>
|
|
<LanguageSwitcher size="medium" color="inherit" />
|
|
</Box>
|
|
|
|
{/* Theme Toggle */}
|
|
<Box sx={{
|
|
mr: { xs: 1, md: 2 },
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(248,250,252,0.6) 100%)'
|
|
: 'linear-gradient(135deg, rgba(50,50,50,0.8) 0%, rgba(30,30,30,0.6) 100%)',
|
|
backdropFilter: 'blur(10px)',
|
|
border: theme.palette.mode === 'light'
|
|
? '1px solid rgba(255,255,255,0.3)'
|
|
: '1px solid rgba(255,255,255,0.1)',
|
|
borderRadius: 2.5,
|
|
transition: 'all 0.2s ease-in-out',
|
|
'&:hover': {
|
|
background: 'linear-gradient(135deg, rgba(99,102,241,0.1) 0%, rgba(139,92,246,0.1) 100%)',
|
|
transform: 'translateY(-2px)',
|
|
boxShadow: '0 8px 24px rgba(99,102,241,0.15)',
|
|
},
|
|
}}>
|
|
<ThemeToggle size="medium" color="inherit" />
|
|
</Box>
|
|
|
|
{/* Profile Menu */}
|
|
<IconButton
|
|
onClick={handleProfileMenuOpen}
|
|
sx={{
|
|
color: 'text.secondary',
|
|
background: theme.palette.mode === 'light'
|
|
? 'linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(248,250,252,0.6) 100%)'
|
|
: 'linear-gradient(135deg, rgba(50,50,50,0.8) 0%, rgba(30,30,30,0.6) 100%)',
|
|
backdropFilter: 'blur(10px)',
|
|
border: theme.palette.mode === 'light'
|
|
? '1px solid rgba(255,255,255,0.3)'
|
|
: '1px solid rgba(255,255,255,0.1)',
|
|
borderRadius: 2.5,
|
|
width: 44,
|
|
height: 44,
|
|
transition: 'all 0.2s ease-in-out',
|
|
'&:hover': {
|
|
background: 'linear-gradient(135deg, rgba(99,102,241,0.1) 0%, rgba(139,92,246,0.1) 100%)',
|
|
transform: 'translateY(-2px)',
|
|
boxShadow: '0 8px 24px rgba(99,102,241,0.15)',
|
|
},
|
|
}}
|
|
>
|
|
<AccountIcon sx={{ fontSize: '1.25rem' }} />
|
|
</IconButton>
|
|
|
|
<Menu
|
|
anchorEl={anchorEl}
|
|
open={Boolean(anchorEl)}
|
|
onClose={handleProfileMenuClose}
|
|
onClick={handleProfileMenuClose}
|
|
PaperProps={{
|
|
elevation: 0,
|
|
sx: {
|
|
overflow: 'visible',
|
|
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
|
|
mt: 1.5,
|
|
'& .MuiAvatar-root': {
|
|
width: 32,
|
|
height: 32,
|
|
ml: -0.5,
|
|
mr: 1,
|
|
},
|
|
'&:before': {
|
|
content: '""',
|
|
display: 'block',
|
|
position: 'absolute',
|
|
top: 0,
|
|
right: 14,
|
|
width: 10,
|
|
height: 10,
|
|
bgcolor: 'background.paper',
|
|
transform: 'translateY(-50%) rotate(45deg)',
|
|
zIndex: 0,
|
|
},
|
|
},
|
|
}}
|
|
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
|
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
|
>
|
|
<MenuItem onClick={() => navigate('/profile')}>
|
|
<Avatar /> {t('auth.profile')}
|
|
</MenuItem>
|
|
<MenuItem onClick={() => navigate('/settings')}>
|
|
<SettingsIcon sx={{ mr: 2 }} /> {t('settings.title')}
|
|
</MenuItem>
|
|
<MenuItem onClick={() => navigate('/debug')}>
|
|
<BugReportIcon sx={{ mr: 2 }} /> {t('settings.debug')}
|
|
</MenuItem>
|
|
<Divider />
|
|
<MenuItem onClick={() => window.open('/swagger-ui', '_blank')}>
|
|
<ApiIcon sx={{ mr: 2 }} /> {t('settings.apiDocumentation')}
|
|
</MenuItem>
|
|
<Divider />
|
|
<MenuItem onClick={handleLogout}>
|
|
<LogoutIcon sx={{ mr: 2 }} /> {t('auth.logout')}
|
|
</MenuItem>
|
|
</Menu>
|
|
</Toolbar>
|
|
</AppBar>
|
|
|
|
{/* Navigation Drawer */}
|
|
<Box
|
|
component="nav"
|
|
sx={{ width: { md: drawerWidth }, flexShrink: { md: 0 } }}
|
|
>
|
|
<Drawer
|
|
variant="temporary"
|
|
open={mobileOpen}
|
|
onClose={handleDrawerToggle}
|
|
ModalProps={{
|
|
keepMounted: true, // Better open performance on mobile.
|
|
}}
|
|
sx={{
|
|
display: { xs: 'block', md: 'none' },
|
|
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
|
}}
|
|
>
|
|
{drawer}
|
|
</Drawer>
|
|
<Drawer
|
|
variant="permanent"
|
|
sx={{
|
|
display: { xs: 'none', md: 'block' },
|
|
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
|
}}
|
|
open
|
|
>
|
|
{drawer}
|
|
</Drawer>
|
|
</Box>
|
|
|
|
{/* Main Content */}
|
|
<Box
|
|
component="main"
|
|
sx={{
|
|
flexGrow: 1,
|
|
width: { md: `calc(100% - ${drawerWidth}px)` },
|
|
minHeight: '100vh',
|
|
backgroundColor: 'background.default',
|
|
}}
|
|
>
|
|
<Toolbar />
|
|
<Box sx={{
|
|
p: 3,
|
|
// Add bottom padding when bottom nav is visible (PWA mode on mobile)
|
|
pb: isPWA && isMobile ? 'calc(64px + 24px + 8px + env(safe-area-inset-bottom, 0px))' : 3,
|
|
}}>
|
|
{children}
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Notification Panel */}
|
|
<NotificationPanel
|
|
anchorEl={notificationAnchorEl}
|
|
onClose={handleNotificationClose}
|
|
/>
|
|
|
|
{/* Bottom Navigation (PWA only) */}
|
|
<BottomNavigation />
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default AppLayout; |