feat: add Layout and ProtectedRoute components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 20:06:08 +01:00
parent d0ee7d2f23
commit f8a6e49038
2 changed files with 76 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import { ReactNode } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import Box from '@mui/material/Box'
import Drawer from '@mui/material/Drawer'
import List from '@mui/material/List'
import ListItemButton from '@mui/material/ListItemButton'
import ListItemIcon from '@mui/material/ListItemIcon'
import ListItemText from '@mui/material/ListItemText'
import Typography from '@mui/material/Typography'
import Divider from '@mui/material/Divider'
import IconButton from '@mui/material/IconButton'
import Tooltip from '@mui/material/Tooltip'
import DnsIcon from '@mui/icons-material/Dns'
import LogoutIcon from '@mui/icons-material/Logout'
import { useAuth } from '../store/auth'
const DRAWER_WIDTH = 240
interface Props { children: ReactNode; title: string }
export default function Layout({ children, title }: Props) {
const navigate = useNavigate()
const location = useLocation()
const { user, logout } = useAuth()
return (
<Box sx={{ display: 'flex', minHeight: '100vh' }}>
<Drawer variant="permanent" sx={{ width: DRAWER_WIDTH, '& .MuiDrawer-paper': { width: DRAWER_WIDTH } }}>
<Box sx={{ px: 2, py: 3 }}>
<Typography variant="h6" sx={{ color: '#e2e8f0', fontWeight: 700, letterSpacing: 1 }}>
Shorefront
</Typography>
<Typography variant="caption" sx={{ color: '#94a3b8' }}>
Shorewall Manager
</Typography>
</Box>
<Divider sx={{ borderColor: '#2d3748' }} />
<List sx={{ flex: 1 }}>
<ListItemButton
selected={location.pathname.startsWith('/configs')}
onClick={() => navigate('/configs')}
sx={{ '&.Mui-selected': { backgroundColor: '#2d3748' }, '&:hover': { backgroundColor: '#2d3748' } }}
>
<ListItemIcon sx={{ color: '#94a3b8', minWidth: 36 }}><DnsIcon fontSize="small" /></ListItemIcon>
<ListItemText primary="Configurations" primaryTypographyProps={{ sx: { color: '#e2e8f0', fontSize: 14 } }} />
</ListItemButton>
</List>
<Divider sx={{ borderColor: '#2d3748' }} />
<Box sx={{ px: 2, py: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="caption" sx={{ color: '#94a3b8' }}>{user?.username}</Typography>
<Tooltip title="Logout">
<IconButton onClick={logout} size="small" sx={{ color: '#94a3b8' }}><LogoutIcon fontSize="small" /></IconButton>
</Tooltip>
</Box>
</Drawer>
<Box component="main" sx={{ flex: 1, bgcolor: 'background.default' }}>
<Box sx={{ px: 4, py: 3, bgcolor: 'white', borderBottom: '1px solid #e2e8f0' }}>
<Typography variant="h5" fontWeight={600}>{title}</Typography>
</Box>
<Box sx={{ p: 4 }}>{children}</Box>
</Box>
</Box>
)
}

View File

@@ -0,0 +1,11 @@
import { Navigate, Outlet } from 'react-router-dom'
import { useAuth } from '../store/auth'
import CircularProgress from '@mui/material/CircularProgress'
import Box from '@mui/material/Box'
export default function ProtectedRoute() {
const { user, loading } = useAuth()
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>
if (!user) return <Navigate to="/login" replace />
return <Outlet />
}