All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m35s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 27s
Deploy to Staging / Verify Staging (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 5s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
- Add Dashboard to desktop sidebar navigation (first item) - Add /garage/dashboard route for desktop - Change default redirect from /garage/vehicles to /garage/dashboard - Change mobile default screen from Vehicles to Dashboard - Create DashboardPage wrapper for desktop route 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
294 lines
9.5 KiB
TypeScript
294 lines
9.5 KiB
TypeScript
/**
|
|
* @ai-summary Main layout component with navigation (desktop/mobile adaptive)
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { useAuth0 } from '@auth0/auth0-react';
|
|
import { Link, useLocation } from 'react-router-dom';
|
|
import { Container, Paper, Typography, Box, IconButton, Avatar } from '@mui/material';
|
|
import HomeRoundedIcon from '@mui/icons-material/HomeRounded';
|
|
import DirectionsCarRoundedIcon from '@mui/icons-material/DirectionsCarRounded';
|
|
import LocalGasStationRoundedIcon from '@mui/icons-material/LocalGasStationRounded';
|
|
import BuildRoundedIcon from '@mui/icons-material/BuildRounded';
|
|
import PlaceRoundedIcon from '@mui/icons-material/PlaceRounded';
|
|
import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded';
|
|
import DescriptionRoundedIcon from '@mui/icons-material/DescriptionRounded';
|
|
import MenuIcon from '@mui/icons-material/Menu';
|
|
import CloseIcon from '@mui/icons-material/Close';
|
|
import { useAppStore } from '../core/store';
|
|
import { Button } from '../shared-minimal/components/Button';
|
|
import { NotificationBell } from '../features/notifications';
|
|
import { useThemeSync } from '../shared-minimal/theme/useThemeSync';
|
|
|
|
interface LayoutProps {
|
|
children: React.ReactNode;
|
|
mobileMode?: boolean;
|
|
}
|
|
|
|
export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false }) => {
|
|
const { user, logout } = useAuth0();
|
|
const { sidebarOpen, toggleSidebar, setSidebarOpen } = useAppStore();
|
|
const location = useLocation();
|
|
|
|
// Sync theme preference with backend
|
|
useThemeSync();
|
|
|
|
// Ensure desktop has a visible navigation by default (only on mount)
|
|
React.useEffect(() => {
|
|
if (!mobileMode && !sidebarOpen) {
|
|
setSidebarOpen(true);
|
|
}
|
|
}, [mobileMode, setSidebarOpen]); // Removed sidebarOpen from dependencies
|
|
|
|
const navigation = [
|
|
{ name: 'Dashboard', href: '/garage/dashboard', icon: <HomeRoundedIcon sx={{ fontSize: 20 }} /> },
|
|
{ name: 'Vehicles', href: '/garage/vehicles', icon: <DirectionsCarRoundedIcon sx={{ fontSize: 20 }} /> },
|
|
{ name: 'Fuel Logs', href: '/garage/fuel-logs', icon: <LocalGasStationRoundedIcon sx={{ fontSize: 20 }} /> },
|
|
{ name: 'Maintenance', href: '/garage/maintenance', icon: <BuildRoundedIcon sx={{ fontSize: 20 }} /> },
|
|
{ name: 'Gas Stations', href: '/garage/stations', icon: <PlaceRoundedIcon sx={{ fontSize: 20 }} /> },
|
|
{ name: 'Documents', href: '/garage/documents', icon: <DescriptionRoundedIcon sx={{ fontSize: 20 }} /> },
|
|
{ name: 'Settings', href: '/garage/settings', icon: <SettingsRoundedIcon sx={{ fontSize: 20 }} /> },
|
|
];
|
|
|
|
// Mobile layout
|
|
if (mobileMode) {
|
|
return (
|
|
<div className="w-full min-h-screen bg-gradient-to-br from-slate-50 via-white to-rose-50 dark:from-paper dark:via-nero dark:to-paper">
|
|
<Container
|
|
maxWidth={false}
|
|
sx={{
|
|
borderRadius: 0,
|
|
p: 0,
|
|
boxShadow: 0,
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
width: '100%',
|
|
maxWidth: '100% !important'
|
|
}}
|
|
>
|
|
{/* App header */}
|
|
<div className="px-5 pt-5 pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="bg-primary-500 dark:bg-transparent rounded px-2 py-1 inline-flex items-center">
|
|
<img
|
|
src="/images/logos/motovaultpro-title-slogan.png"
|
|
alt="MotoVaultPro"
|
|
className="h-6 w-auto"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<NotificationBell />
|
|
<div className="text-xs text-slate-500 dark:text-titanio">v1.0</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Content area */}
|
|
<div className="flex-1 px-5 pb-20 space-y-5 overflow-y-auto">
|
|
<div className="min-h-[560px]">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Desktop layout
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-rose-50 dark:from-paper dark:via-nero dark:to-paper">
|
|
{/* Sidebar */}
|
|
<Paper
|
|
elevation={2}
|
|
sx={{
|
|
position: 'fixed',
|
|
top: 0,
|
|
left: 0,
|
|
height: '100vh',
|
|
width: 256,
|
|
zIndex: 1000,
|
|
borderRadius: 0,
|
|
borderRight: 1,
|
|
borderColor: 'divider',
|
|
transform: sidebarOpen ? 'translateX(0)' : 'translateX(-100%)',
|
|
transition: 'transform 0.2s ease-in-out',
|
|
display: 'flex',
|
|
flexDirection: 'column'
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
height: 64,
|
|
px: 2,
|
|
borderBottom: 1,
|
|
borderColor: 'divider',
|
|
gap: 1
|
|
}}
|
|
>
|
|
<Box
|
|
sx={(theme) => ({
|
|
backgroundColor: 'primary.main',
|
|
...theme.applyStyles('dark', {
|
|
backgroundColor: 'transparent',
|
|
}),
|
|
borderRadius: 1,
|
|
px: 1,
|
|
py: 0.5,
|
|
display: 'inline-flex',
|
|
alignItems: 'center'
|
|
})}
|
|
>
|
|
<img
|
|
src="/images/logos/motovaultpro-title-slogan.png"
|
|
alt="MotoVaultPro"
|
|
style={{ height: 24, width: 'auto', maxWidth: 180 }}
|
|
/>
|
|
</Box>
|
|
<IconButton
|
|
onClick={toggleSidebar}
|
|
size="small"
|
|
sx={{ color: 'text.secondary', flexShrink: 0 }}
|
|
>
|
|
<CloseIcon />
|
|
</IconButton>
|
|
</Box>
|
|
|
|
<Box sx={{ mt: 3, px: 2, flex: 1 }}>
|
|
{navigation.map((item) => {
|
|
const isActive = location.pathname.startsWith(item.href);
|
|
return (
|
|
<Link
|
|
key={item.name}
|
|
to={item.href}
|
|
style={{ textDecoration: 'none' }}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
px: 2,
|
|
py: 1.5,
|
|
mb: 0.5,
|
|
borderRadius: 2,
|
|
transition: 'all 0.2s',
|
|
backgroundColor: isActive
|
|
? 'primary.main'
|
|
: 'transparent',
|
|
color: isActive
|
|
? 'primary.contrastText'
|
|
: 'text.primary',
|
|
'&:hover': {
|
|
backgroundColor: isActive
|
|
? 'primary.dark'
|
|
: 'action.hover',
|
|
}
|
|
}}
|
|
>
|
|
<Box sx={{ mr: 2, display: 'flex', alignItems: 'center' }}>
|
|
{item.icon}
|
|
</Box>
|
|
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
|
{item.name}
|
|
</Typography>
|
|
</Box>
|
|
</Link>
|
|
);
|
|
})}
|
|
</Box>
|
|
|
|
<Box sx={{ p: 2, borderTop: 1, borderColor: 'divider', mt: 'auto' }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<Avatar
|
|
sx={{
|
|
width: 32,
|
|
height: 32,
|
|
bgcolor: 'primary.main',
|
|
fontSize: '0.875rem',
|
|
fontWeight: 600
|
|
}}
|
|
>
|
|
{user?.name?.charAt(0) || user?.email?.charAt(0)}
|
|
</Avatar>
|
|
<Box sx={{ ml: 1.5, flex: 1, minWidth: 0 }}>
|
|
<Typography variant="body2" sx={{ fontWeight: 500 }} noWrap>
|
|
{user?.name || user?.email}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
className="w-full"
|
|
onClick={() => logout({ logoutParams: { returnTo: window.location.origin } })}
|
|
>
|
|
Sign Out
|
|
</Button>
|
|
</Box>
|
|
</Paper>
|
|
|
|
{/* Main content */}
|
|
<Box
|
|
sx={{
|
|
ml: sidebarOpen ? '256px' : '0',
|
|
transition: 'margin-left 0.2s ease-in-out',
|
|
}}
|
|
>
|
|
{/* Top bar */}
|
|
<Paper
|
|
elevation={1}
|
|
sx={{
|
|
borderRadius: 0,
|
|
borderBottom: 1,
|
|
borderColor: 'divider'
|
|
}}
|
|
>
|
|
<Box sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
height: 64,
|
|
px: 3
|
|
}}>
|
|
<IconButton
|
|
onClick={toggleSidebar}
|
|
sx={{ color: 'text.secondary' }}
|
|
>
|
|
<MenuIcon />
|
|
</IconButton>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<NotificationBell />
|
|
<Typography variant="body2" color="text.secondary">
|
|
Welcome back, {user?.name || user?.email}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</Paper>
|
|
|
|
{/* Page content */}
|
|
<Box component="main" sx={{ p: 3 }}>
|
|
<Container maxWidth="xl">
|
|
{children}
|
|
</Container>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Backdrop */}
|
|
{sidebarOpen && (
|
|
<Box
|
|
sx={{
|
|
position: 'fixed',
|
|
inset: 0,
|
|
zIndex: 999,
|
|
bgcolor: 'rgba(0,0,0,0.5)',
|
|
display: { lg: 'none' }
|
|
}}
|
|
onClick={toggleSidebar}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|