mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 02:09:08 +03:00
feat: desktop & mobile navigation
This commit is contained in:
parent
d28cb97d30
commit
d5fb033017
21
package-lock.json
generated
21
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
<UserNotifications />
|
||||
<UserInfo />
|
||||
{isDesktop ? (
|
||||
<>
|
||||
<UserNotifications />
|
||||
<UserInfo />
|
||||
</>
|
||||
) : (
|
||||
<NavMobile />
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
@ -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} />
|
||||
|
||||
32
src/components/Nav/Desktop/Nav.tsx
Normal file
32
src/components/Nav/Desktop/Nav.tsx
Normal 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;
|
||||
@ -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%'
|
||||
32
src/components/Nav/Mobile/Nav.tsx
Normal file
32
src/components/Nav/Mobile/Nav.tsx
Normal 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;
|
||||
@ -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;
|
||||
@ -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 },
|
||||
|
||||
28
src/hooks/useWindowDimensions.ts
Normal file
28
src/hooks/useWindowDimensions.ts
Normal 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;
|
||||
@ -9,6 +9,10 @@ export type LinkType = {
|
||||
};
|
||||
|
||||
export interface IAppConfig {
|
||||
metaData: {
|
||||
appName: string;
|
||||
appDescription: string;
|
||||
};
|
||||
links: LinkType[];
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user