feat: Add Layout with AppBar and Drawer

This commit is contained in:
Ali BARIN
2021-10-05 20:35:13 +02:00
parent f032dea77e
commit 3f56da5efb
11 changed files with 262 additions and 6 deletions

View File

@@ -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",

View File

@@ -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;

View 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>
);
}

View 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',
},
},
},
}));

View 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>
);
}

View 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),
}),
}),
);

View 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} />
);
};

View 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>
</>
);
}

View 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);
}

View File

@@ -1,3 +1,5 @@
{ {
"welcomeText": "Hello world!" "brandText": "automatisch",
"searchPlaceholder": "Search...",
"welcomeText": "Here comes the dashboard homepage."
} }

View File

@@ -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"