feat: desktop & mobile navigation

This commit is contained in:
Sergey Yarkov 2021-08-24 17:53:08 +03:00
parent d28cb97d30
commit d5fb033017
14 changed files with 176 additions and 92 deletions

21
package-lock.json generated
View File

@ -1408,6 +1408,27 @@
"@chakra-ui/utils": "1.8.2"
}
},
"@chakra-ui/icons": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/@chakra-ui/icons/-/icons-1.0.15.tgz",
"integrity": "sha512-MMuPwmeCil9vAXceIN/Fxn6CNHbhkLofFQaKUfs+UaBsviiU2tvS0nqGaxm/9FNzLr5ithPVWpbz3uV7DXc77g==",
"requires": {
"@chakra-ui/icon": "1.1.11",
"@types/react": "^17.0.0"
},
"dependencies": {
"@types/react": {
"version": "17.0.19",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.19.tgz",
"integrity": "sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
}
}
},
"@chakra-ui/image": {
"version": "1.0.18",
"resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-1.0.18.tgz",

View File

@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@chakra-ui/icons": "^1.0.15",
"@chakra-ui/react": "^1.0.0",
"@emotion/react": "^11.0.0",
"@emotion/styled": "^11.0.0",

View File

@ -1,6 +1,7 @@
import React from 'react';
import { Flex, Box, Text, Heading } from '@chakra-ui/react';
import { MdSchool } from 'react-icons/md';
import config from '../../config';
const AppName: React.FC = () => {
return (
@ -8,11 +9,11 @@ const AppName: React.FC = () => {
<Box mr='4'>
<Box as={MdSchool} color='blue.500' size='36px' />
</Box>
<Box lineHeight='1'>
<Heading as='p' fontSize='2xl'>
App name
<Box>
<Heading as='p' fontSize='2xl' lineHeight='1'>
{config.metaData.appName}
</Heading>
<Text as='small'>App description</Text>
<Text as='small'>{config.metaData.appDescription}</Text>
</Box>
</Flex>
);

View File

@ -3,8 +3,12 @@ import { Flex, Box } from '@chakra-ui/react';
import AppName from './AppName';
import UserInfo from './User/UserInfo';
import UserNotifications from './User/UserNotifications';
import useWindowDimensions from '../../hooks/useWindowDimensions';
import NavMobile from '../Nav/Mobile/Nav';
const Header: React.FC = () => {
const { isDesktop } = useWindowDimensions();
return (
<Box
as='header'
@ -27,8 +31,14 @@ const Header: React.FC = () => {
>
<AppName />
<Flex>
{isDesktop ? (
<>
<UserNotifications />
<UserInfo />
</>
) : (
<NavMobile />
)}
</Flex>
</Flex>
</Box>

View File

@ -1,11 +1,12 @@
import React from 'react';
import { Flex, Box } from '@chakra-ui/react';
import Header from '../Header/Header';
import Nav from '../Nav/Nav';
import NavDesktop from '../Nav/Desktop/Nav';
import { observer } from 'mobx-react';
import { useRootStore } from '../../hooks/useRootStore';
import { useErrorHandler } from 'react-error-boundary';
import { ColorModeSwitcher } from '../ColorModeSwitcher/ColorModeSwitcher';
import useWindowDimensions from '../../hooks/useWindowDimensions';
/**
*
@ -13,6 +14,7 @@ import { ColorModeSwitcher } from '../ColorModeSwitcher/ColorModeSwitcher';
* Сomponent for rendering the current page.
*/
const Layout: React.FC = ({ children }) => {
const { isDesktop } = useWindowDimensions();
const { authStore, userStore } = useRootStore();
const handleError = useErrorHandler();
@ -27,10 +29,19 @@ const Layout: React.FC = ({ children }) => {
return (
<>
<Header />
<Nav />
{isDesktop && <NavDesktop />}
<Flex minH='100vh' flexDirection='column'>
<Box pl='18rem' mt='4rem' mb='4rem' flex='1 0 auto'>
<Box
pl='80'
mt='20'
mb='10'
flex='1 0 auto'
sx={{
'@media (max-width: 768px)': {
paddingLeft: 0,
},
}}
>
<Box as='main' mr='auto' ml='auto' maxW='85rem' pl={5} pr={5} pt='2rem' pb='2rem'>
{children}
<ColorModeSwitcher position='absolute' right={0} bottom={0} margin={5} />

View File

@ -0,0 +1,32 @@
import React from 'react';
import NavLink from './NavLink';
import config from '../../../config';
import { Flex, Box } from '@chakra-ui/react';
/**
*
* Nav component
* Component for navigating the application.
*/
const Nav: React.FC = () => {
return (
<Box display='block' position='fixed' left={0} right={0} top={0} width='100%' height='100%' maxW='18rem'>
<Box top='4rem' position='relative' overflowY='auto' borderRightWidth={1}>
<Flex
as='nav'
flexDirection='column'
alignItems='flex-start'
textDecoration='none'
height='calc(100vh - 4rem)'
padding='5'
>
{config.links.map((link, i) => (
<NavLink link={link} key={i} />
))}
</Flex>
</Box>
</Box>
);
};
export default Nav;

View File

@ -1,43 +1,35 @@
import React from 'react';
import { Box, useColorMode } from '@chakra-ui/react';
import type { History } from 'history';
import type { LinkType } from '../../interfaces';
import type { ActiveLinkState } from './Nav';
import type { LinkType } from '../../../interfaces';
import { useHistory } from 'react-router-dom';
type ActiveLinkState = {
title: string | null;
};
/**
*
* NavLink component
* Returns the link component for navigation.
*
*/
interface NavLinkProps {
handleOnHoverLink: (title: string) => void;
handleOnLeaveLink: () => void;
handleRoute: (location: string) => void;
history: History;
activeLink: ActiveLinkState;
link: LinkType;
}
const NavLink: React.FC<NavLinkProps> = ({
handleOnHoverLink,
handleOnLeaveLink,
handleRoute,
history,
activeLink,
link,
}) => {
const NavLink: React.FC<{ link: LinkType; onCloseDrawer?: () => void }> = ({ link, onCloseDrawer }) => {
const [activeLink, setActiveLink] = React.useState<ActiveLinkState>({ title: null });
const { colorMode } = useColorMode();
const onMouseEnter = () => handleOnHoverLink(link.title);
const onClick = () => handleRoute(link.location);
const history = useHistory();
const handleClick = () => {
history.push(link.location);
/* Close mobile drawer on click link */
onCloseDrawer && onCloseDrawer();
};
return (
<Box
as='a'
onMouseEnter={onMouseEnter}
onMouseLeave={handleOnLeaveLink}
onClick={onClick}
onMouseEnter={() => setActiveLink({ title: link.title })}
onMouseLeave={() => setActiveLink({ title: null })}
onClick={handleClick}
display='flex'
margin='3px 0'
width='100%'

View File

@ -0,0 +1,32 @@
import React from 'react';
import { Flex } from '@chakra-ui/layout';
import { Drawer, DrawerOverlay, DrawerContent, DrawerHeader, DrawerBody } from '@chakra-ui/modal';
import { HamburgerIcon } from '@chakra-ui/icons';
import { useDisclosure, IconButton } from '@chakra-ui/react';
import config from '../../../config';
import NavLink from '../Desktop/NavLink';
const NavMobile: React.FC = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<IconButton aria-label='Navigation' icon={<HamburgerIcon />} onClick={onOpen} />
<Drawer placement='left' onClose={onClose} isOpen={isOpen}>
<DrawerOverlay />
<DrawerContent>
<DrawerHeader borderBottomWidth='1px'>Navigation</DrawerHeader>
<DrawerBody>
<Flex as='nav' flexDirection='column' alignItems='flex-start' textDecoration='none'>
{config.links.map((link, i) => (
<NavLink link={link} key={i} onCloseDrawer={onClose} />
))}
</Flex>
</DrawerBody>
</DrawerContent>
</Drawer>
</>
);
};
export default NavMobile;

View File

@ -1,52 +0,0 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import NavLink from './NavLink';
import config from '../../config';
import { Flex, Box } from '@chakra-ui/react';
/**
*
* Nav component
* Component for navigating the application.
*/
export type ActiveLinkState = {
title: string | null;
};
const Nav: React.FC = () => {
const [activeLink, setActiveLink] = useState<ActiveLinkState>({ title: null });
const history = useHistory();
const handleRoute = (location: string): void => history.push(location);
const handleOnHoverLink = (title: string): void => setActiveLink({ title });
const handleOnLeaveLink = (): void => setActiveLink({ title: null });
return (
<Box display='block' position='fixed' left={0} right={0} top={0} width='100%' height='100%' maxWidth='18rem'>
<Box top='4rem' position='relative' overflowY='auto' borderRightWidth={1}>
<Flex
as='nav'
flexDirection='column'
alignItems='flex-start'
textDecoration='none'
height='calc(100vh - 4rem)'
padding='5'
>
{config.links.map((link, i) => (
<NavLink
handleOnHoverLink={handleOnHoverLink}
handleOnLeaveLink={handleOnLeaveLink}
handleRoute={handleRoute}
activeLink={activeLink}
history={history}
link={link}
key={i}
/>
))}
</Flex>
</Box>
</Box>
);
};
export default Nav;

View File

@ -2,6 +2,10 @@ import { MdAccountCircle, MdCollectionsBookmark, MdHome, MdMessage } from 'react
import { IAppConfig } from './interfaces';
const config: IAppConfig = {
metaData: {
appName: 'Educt',
appDescription: 'Learn management system',
},
links: [
{ location: '/', title: 'Home', icon: MdHome },
{ location: '/courses', title: 'Courses', icon: MdCollectionsBookmark },

View File

@ -0,0 +1,28 @@
import React from 'react';
const useWindowDimensions = () => {
const [windowDimensions, setWindowDimensions] = React.useState({
width: window.innerWidth,
height: window.innerHeight,
});
React.useEffect(() => {
/**
* Update state on resize
*/
const handleResize = () => setWindowDimensions({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
return {
windowDimensions,
isDesktop: windowDimensions.width > 768,
isTablet: windowDimensions.width < 768,
isMobile: windowDimensions.width < 568,
};
};
export default useWindowDimensions;

View File

@ -9,6 +9,10 @@ export type LinkType = {
};
export interface IAppConfig {
metaData: {
appName: string;
appDescription: string;
};
links: LinkType[];
}

View File

@ -28,7 +28,7 @@ const AuthPage: React.FC<IPageProps> = () => {
<Box p={8} width='full' maxWidth='450px' borderWidth={1} borderRadius={8}>
<Box textAlign='center'>
<Flex justifyContent='center'>
<MdSchool fill='blue' size='64px' />
<Box as={MdSchool} color='blue.500' size='64px' />
</Flex>
</Box>
<Box>

View File

@ -1,5 +1,5 @@
import { AxiosInstance } from 'axios';
import { autorun, makeAutoObservable } from 'mobx';
import { makeAutoObservable, runInAction } from 'mobx';
import { IUserResult } from '../interfaces';
import UserService from '../services/UserService';
import RootStore from './RootStore';
@ -30,7 +30,7 @@ export default class UserStore {
try {
const result = await this.userService.fetchMe();
autorun(() => {
runInAction(() => {
this.me = result.data;
});