This commit is contained in:
seb
2026-06-23 11:04:28 +02:00
commit 24f68c94f3
16 changed files with 2580 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import { useState } from 'react';
import Box from '@mui/material/Box';
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 UserMenu from './UserMenu';
import { NAV_ITEMS } from '../nav';
const SIDEBAR_WIDTH = 240;
// App layout. State lives in the children (UserMenu, page content) so the
// shell itself does not re-render when their state changes.
export default function AppShell() {
const [activeId, setActiveId] = useState(NAV_ITEMS[0].id);
const activeItem = NAV_ITEMS.find((item) => item.id === activeId) ?? NAV_ITEMS[0];
const ActivePage = activeItem.Page;
return (
<Box sx={{ display: 'flex', minHeight: '100vh', bgcolor: 'grey.50' }}>
<Box
component="nav"
sx={{
width: SIDEBAR_WIDTH,
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
bgcolor: 'background.paper',
}}
>
<List disablePadding sx={{ flexGrow: 1 }}>
{NAV_ITEMS.map(({ id, label, icon: Icon }) => (
<ListItemButton
key={id}
disableGutters
selected={id === activeId}
onClick={() => setActiveId(id)}
sx={{ borderRadius: 0, px: 2 }}
>
<ListItemIcon sx={{ minWidth: 40 }}>
<Icon />
</ListItemIcon>
<ListItemText primary={label} />
</ListItemButton>
))}
</List>
<UserMenu />
</Box>
<Box component="main" sx={{ flexGrow: 1, py: 4, px: 3 }}>
<ActivePage />
</Box>
</Box>
);
}

View File

@@ -0,0 +1,54 @@
import { useState } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import GoogleIcon from '@mui/icons-material/Google';
import { useSnackbar } from 'notistack';
import { useAuth } from '../auth/AuthContext';
import { requestGoogleIdToken } from '../auth/googleSignIn';
export default function LoginPage() {
const { loginWithGoogle, googleClientId, status } = useAuth();
const { enqueueSnackbar } = useSnackbar();
const [busy, setBusy] = useState(false);
const handleLogin = async () => {
setBusy(true);
try {
const idToken = await requestGoogleIdToken(googleClientId);
await loginWithGoogle(idToken);
} catch (err) {
enqueueSnackbar(`Login failed: ${err.message}`, { variant: 'error' });
} finally {
setBusy(false);
}
};
const loading = status === 'loading';
const ready = Boolean(googleClientId);
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
bgcolor: 'grey.100',
}}
>
<Button
variant="contained"
size="large"
startIcon={<GoogleIcon />}
disabled={busy || loading || !ready}
onClick={handleLogin}
>
{busy ? 'Signing in…' : 'Sign in with Google'}
</Button>
</Box>
);
}

View File

@@ -0,0 +1,52 @@
import { useState } from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import LogoutIcon from '@mui/icons-material/Logout';
import { useAuth } from '../auth/AuthContext';
// Menu open/close state is local; toggling it only re-renders this component.
export default function UserMenu() {
const { user, logout } = useAuth();
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
return (
<>
<Box sx={{ px: 2, py: 1.5, alignSelf: 'flex-start' }}>
<IconButton
onClick={(e) => setAnchorEl(e.currentTarget)}
sx={{ p: 0, width: 32, height: 32, borderRadius: '50%' }}
>
<Avatar alt={user?.email} sx={{ width: 32, height: 32 }}>
{user?.email?.[0]?.toUpperCase()}
</Avatar>
</IconButton>
</Box>
<Menu
anchorEl={anchorEl}
open={open}
onClose={() => setAnchorEl(null)}
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
transformOrigin={{ vertical: 'bottom', horizontal: 'left' }}
>
<MenuItem disabled>{user?.email}</MenuItem>
<MenuItem
onClick={() => {
setAnchorEl(null);
logout();
}}
>
<ListItemIcon>
<LogoutIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Sign out</ListItemText>
</MenuItem>
</Menu>
</>
);
}