diff --git a/packages/web/package.json b/packages/web/package.json
index da95eb5f..b29b032a 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -6,6 +6,7 @@
"@apollo/client": "^3.4.15",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
+ "@mui/icons-material": "^5.0.1",
"@mui/material": "^5.0.2",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
diff --git a/packages/web/src/components/App/index.tsx b/packages/web/src/components/App/index.tsx
index 5128d756..449db383 100644
--- a/packages/web/src/components/App/index.tsx
+++ b/packages/web/src/components/App/index.tsx
@@ -1,10 +1,10 @@
-import React from 'react';
import { FormattedMessage } from 'react-intl';
+import Layout from 'components/Layout';
-function App() {
+export default function App() {
return (
-
+
+
+
);
}
-
-export default App;
diff --git a/packages/web/src/components/AppBar/index.tsx b/packages/web/src/components/AppBar/index.tsx
new file mode 100644
index 00000000..824117fe
--- /dev/null
+++ b/packages/web/src/components/AppBar/index.tsx
@@ -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 (
+
+
+ theme.zIndex.drawer + 1 }}>
+
+
+ {/* TODO: make Drawer in Layout togglable. */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/web/src/components/AppBar/style.ts b/packages/web/src/components/AppBar/style.ts
new file mode 100644
index 00000000..3cd5bd23
--- /dev/null
+++ b/packages/web/src/components/AppBar/style.ts
@@ -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',
+ },
+ },
+ },
+}));
\ No newline at end of file
diff --git a/packages/web/src/components/Drawer/index.tsx b/packages/web/src/components/Drawer/index.tsx
new file mode 100644
index 00000000..98d428df
--- /dev/null
+++ b/packages/web/src/components/Drawer/index.tsx
@@ -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 (
+
+
+
+
+
+ {['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
+
+
+ {index % 2 === 0 ? : }
+
+
+
+ ))}
+
+
+
+ {['All mail', 'Trash', 'Spam'].map((text, index) => (
+
+
+ {index % 2 === 0 ? : }
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/packages/web/src/components/Drawer/style.ts b/packages/web/src/components/Drawer/style.ts
new file mode 100644
index 00000000..6bd47537
--- /dev/null
+++ b/packages/web/src/components/Drawer/style.ts
@@ -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),
+ }),
+ }),
+);
diff --git a/packages/web/src/components/HideOnScroll/index.tsx b/packages/web/src/components/HideOnScroll/index.tsx
new file mode 100644
index 00000000..c6723dd1
--- /dev/null
+++ b/packages/web/src/components/HideOnScroll/index.tsx
@@ -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 (
+
+ );
+};
diff --git a/packages/web/src/components/Layout/index.tsx b/packages/web/src/components/Layout/index.tsx
new file mode 100644
index 00000000..455746dc
--- /dev/null
+++ b/packages/web/src/components/Layout/index.tsx
@@ -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 (
+ <>
+
+
+
+
+
+
+
+
+ {children}
+
+
+ >
+ );
+}
diff --git a/packages/web/src/hooks/useFormatMessage.tsx b/packages/web/src/hooks/useFormatMessage.tsx
new file mode 100644
index 00000000..c999941a
--- /dev/null
+++ b/packages/web/src/hooks/useFormatMessage.tsx
@@ -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);
+}
diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json
index dce38dd0..9eb806a7 100644
--- a/packages/web/src/locales/en.json
+++ b/packages/web/src/locales/en.json
@@ -1,3 +1,5 @@
{
- "welcomeText": "Hello world!"
+ "brandText": "automatisch",
+ "searchPlaceholder": "Search...",
+ "welcomeText": "Here comes the dashboard homepage."
}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 33d37f4d..ad1d8fae 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2386,6 +2386,13 @@
prop-types "^15.7.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":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.0.2.tgz#380cf0ef42c538a68158b4da19c317178b22d10f"