feat: Add Layout with AppBar and Drawer
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
"@apollo/client": "^3.4.15",
|
"@apollo/client": "^3.4.15",
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
|
"@mui/icons-material": "^5.0.1",
|
||||||
"@mui/material": "^5.0.2",
|
"@mui/material": "^5.0.2",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Layout from 'components/Layout';
|
||||||
|
|
||||||
function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<FormattedMessage id="welcomeText" />
|
<Layout>
|
||||||
|
<FormattedMessage id="welcomeText" />
|
||||||
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
62
packages/web/src/components/AppBar/index.tsx
Normal file
62
packages/web/src/components/AppBar/index.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import MuiAppBar from '@mui/material/AppBar';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
|
||||||
|
import HideOnScroll from 'components/HideOnScroll';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import { Search, SearchIconWrapper, InputBase } from './style';
|
||||||
|
|
||||||
|
type AppBarProps = {
|
||||||
|
onMenuClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AppBar({ onMenuClick }: AppBarProps) {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<HideOnScroll>
|
||||||
|
<MuiAppBar sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}>
|
||||||
|
<Toolbar>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="open drawer"
|
||||||
|
onClick={onMenuClick}
|
||||||
|
sx={{ mr: 2 }}
|
||||||
|
>
|
||||||
|
{/* TODO: make Drawer in Layout togglable. */}
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
noWrap
|
||||||
|
component="div"
|
||||||
|
sx={{ flexGrow: 1, display: { xs: 'none', sm: 'block' } }}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="automatisch" />
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Search>
|
||||||
|
<SearchIconWrapper>
|
||||||
|
<SearchIcon />
|
||||||
|
</SearchIconWrapper>
|
||||||
|
|
||||||
|
<InputBase
|
||||||
|
placeholder={formatMessage('searchPlaceholder')}
|
||||||
|
inputProps={{ 'aria-label': 'search' }}
|
||||||
|
/>
|
||||||
|
</Search>
|
||||||
|
</Toolbar>
|
||||||
|
</MuiAppBar>
|
||||||
|
</HideOnScroll>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
44
packages/web/src/components/AppBar/style.ts
Normal file
44
packages/web/src/components/AppBar/style.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { styled, alpha } from '@mui/material/styles';
|
||||||
|
import MuiInputBase from '@mui/material/InputBase';
|
||||||
|
|
||||||
|
export const Search = styled('div')(({ theme }) => ({
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
backgroundColor: alpha(theme.palette.common.white, 0.15),
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: alpha(theme.palette.common.white, 0.25),
|
||||||
|
},
|
||||||
|
marginLeft: 0,
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
width: 'auto',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const SearchIconWrapper = styled('div')(({ theme }) => ({
|
||||||
|
padding: theme.spacing(0, 2),
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const InputBase = styled(MuiInputBase)(({ theme }) => ({
|
||||||
|
color: 'inherit',
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
padding: theme.spacing(1, 1, 1, 0),
|
||||||
|
// vertical padding + font size from searchIcon
|
||||||
|
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
|
||||||
|
transition: theme.transitions.create('width'),
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
width: '12ch',
|
||||||
|
'&:focus': {
|
||||||
|
width: '20ch',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
47
packages/web/src/components/Drawer/index.tsx
Normal file
47
packages/web/src/components/Drawer/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { DrawerProps } from '@mui/material/Drawer';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import InboxIcon from '@mui/icons-material/MoveToInbox';
|
||||||
|
import MailIcon from '@mui/icons-material/Mail';
|
||||||
|
|
||||||
|
import HideOnScroll from 'components/HideOnScroll';
|
||||||
|
import { Drawer as BaseDrawer } from './style';
|
||||||
|
|
||||||
|
export default function Drawer(props: DrawerProps) {
|
||||||
|
return (
|
||||||
|
<BaseDrawer
|
||||||
|
{...props}
|
||||||
|
variant="permanent"
|
||||||
|
>
|
||||||
|
<HideOnScroll unmountOnExit>
|
||||||
|
<Toolbar />
|
||||||
|
</HideOnScroll>
|
||||||
|
<List>
|
||||||
|
{['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
|
||||||
|
<ListItem button key={text}>
|
||||||
|
<ListItemIcon>
|
||||||
|
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={text} />
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
{['All mail', 'Trash', 'Spam'].map((text, index) => (
|
||||||
|
<ListItem button key={text}>
|
||||||
|
<ListItemIcon>
|
||||||
|
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={text} />
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</BaseDrawer>
|
||||||
|
);
|
||||||
|
}
|
42
packages/web/src/components/Drawer/style.ts
Normal file
42
packages/web/src/components/Drawer/style.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { styled, Theme, CSSObject } from '@mui/material/styles';
|
||||||
|
import MuiDrawer from '@mui/material/Drawer';
|
||||||
|
|
||||||
|
const drawerWidth = 240;
|
||||||
|
|
||||||
|
const openedMixin = (theme: Theme): CSSObject => ({
|
||||||
|
width: drawerWidth,
|
||||||
|
transition: theme.transitions.create('width', {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
overflowX: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
const closedMixin = (theme: Theme): CSSObject => ({
|
||||||
|
transition: theme.transitions.create('width', {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
overflowX: 'hidden',
|
||||||
|
width: `calc(${theme.spacing(7)} + 1px)`,
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
width: `calc(${theme.spacing(9)} + 1px)`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })(
|
||||||
|
({ theme, open }) => ({
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
...(open && {
|
||||||
|
...openedMixin(theme),
|
||||||
|
'& .MuiDrawer-paper': openedMixin(theme),
|
||||||
|
}),
|
||||||
|
...(!open && {
|
||||||
|
...closedMixin(theme),
|
||||||
|
'& .MuiDrawer-paper': closedMixin(theme),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
10
packages/web/src/components/HideOnScroll/index.tsx
Normal file
10
packages/web/src/components/HideOnScroll/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||||
|
import useScrollTrigger from '@mui/material/useScrollTrigger';
|
||||||
|
|
||||||
|
export default function HideOnScroll(props: SlideProps) {
|
||||||
|
const trigger = useScrollTrigger();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slide appear={false} direction="down" in={!trigger} {...props} />
|
||||||
|
);
|
||||||
|
};
|
30
packages/web/src/components/Layout/index.tsx
Normal file
30
packages/web/src/components/Layout/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import AppBar from 'components/AppBar';
|
||||||
|
import Drawer from 'components/Drawer';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
|
||||||
|
type LayoutProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Layout({ children }: LayoutProps) {
|
||||||
|
const [isDrawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const onMenuClick = useCallback(() => { setDrawerOpen(value => !value) }, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppBar onMenuClick={onMenuClick} />
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', }}>
|
||||||
|
<Drawer open={isDrawerOpen} />
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Toolbar />
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
11
packages/web/src/hooks/useFormatMessage.tsx
Normal file
11
packages/web/src/hooks/useFormatMessage.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
type Values = {
|
||||||
|
[key: string]: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useFormatMessage() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (id: string, values: Values = {}) => formatMessage({ id }, values);
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"welcomeText": "Hello world!"
|
"brandText": "automatisch",
|
||||||
|
"searchPlaceholder": "Search...",
|
||||||
|
"welcomeText": "Here comes the dashboard homepage."
|
||||||
}
|
}
|
@@ -2386,6 +2386,13 @@
|
|||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
react-is "^17.0.2"
|
react-is "^17.0.2"
|
||||||
|
|
||||||
|
"@mui/icons-material@^5.0.1":
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.0.1.tgz#fb7ffeba0b3604aab4a9b91644d2fc1aabb3b4f1"
|
||||||
|
integrity sha512-AZehR/Uvi9VodsNPk9ae1lENKrf1evqx9suiP6VIqu7NxjZOlw/m/yA2gRAMmLEmIGr7EChfi/wcXuq6BpM9vw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.15.4"
|
||||||
|
|
||||||
"@mui/material@^5.0.2":
|
"@mui/material@^5.0.2":
|
||||||
version "5.0.2"
|
version "5.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.0.2.tgz#380cf0ef42c538a68158b4da19c317178b22d10f"
|
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.0.2.tgz#380cf0ef42c538a68158b4da19c317178b22d10f"
|
||||||
|
Reference in New Issue
Block a user