mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +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/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": {
|
"@chakra-ui/image": {
|
||||||
"version": "1.0.18",
|
"version": "1.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-1.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-1.0.18.tgz",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@chakra-ui/icons": "^1.0.15",
|
||||||
"@chakra-ui/react": "^1.0.0",
|
"@chakra-ui/react": "^1.0.0",
|
||||||
"@emotion/react": "^11.0.0",
|
"@emotion/react": "^11.0.0",
|
||||||
"@emotion/styled": "^11.0.0",
|
"@emotion/styled": "^11.0.0",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex, Box, Text, Heading } from '@chakra-ui/react';
|
import { Flex, Box, Text, Heading } from '@chakra-ui/react';
|
||||||
import { MdSchool } from 'react-icons/md';
|
import { MdSchool } from 'react-icons/md';
|
||||||
|
import config from '../../config';
|
||||||
|
|
||||||
const AppName: React.FC = () => {
|
const AppName: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@ -8,11 +9,11 @@ const AppName: React.FC = () => {
|
|||||||
<Box mr='4'>
|
<Box mr='4'>
|
||||||
<Box as={MdSchool} color='blue.500' size='36px' />
|
<Box as={MdSchool} color='blue.500' size='36px' />
|
||||||
</Box>
|
</Box>
|
||||||
<Box lineHeight='1'>
|
<Box>
|
||||||
<Heading as='p' fontSize='2xl'>
|
<Heading as='p' fontSize='2xl' lineHeight='1'>
|
||||||
App name
|
{config.metaData.appName}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text as='small'>App description</Text>
|
<Text as='small'>{config.metaData.appDescription}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,8 +3,12 @@ import { Flex, Box } from '@chakra-ui/react';
|
|||||||
import AppName from './AppName';
|
import AppName from './AppName';
|
||||||
import UserInfo from './User/UserInfo';
|
import UserInfo from './User/UserInfo';
|
||||||
import UserNotifications from './User/UserNotifications';
|
import UserNotifications from './User/UserNotifications';
|
||||||
|
import useWindowDimensions from '../../hooks/useWindowDimensions';
|
||||||
|
import NavMobile from '../Nav/Mobile/Nav';
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
|
const { isDesktop } = useWindowDimensions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as='header'
|
as='header'
|
||||||
@ -27,8 +31,14 @@ const Header: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<AppName />
|
<AppName />
|
||||||
<Flex>
|
<Flex>
|
||||||
<UserNotifications />
|
{isDesktop ? (
|
||||||
<UserInfo />
|
<>
|
||||||
|
<UserNotifications />
|
||||||
|
<UserInfo />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<NavMobile />
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex, Box } from '@chakra-ui/react';
|
import { Flex, Box } from '@chakra-ui/react';
|
||||||
import Header from '../Header/Header';
|
import Header from '../Header/Header';
|
||||||
import Nav from '../Nav/Nav';
|
import NavDesktop from '../Nav/Desktop/Nav';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { useRootStore } from '../../hooks/useRootStore';
|
import { useRootStore } from '../../hooks/useRootStore';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
import { ColorModeSwitcher } from '../ColorModeSwitcher/ColorModeSwitcher';
|
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.
|
* Сomponent for rendering the current page.
|
||||||
*/
|
*/
|
||||||
const Layout: React.FC = ({ children }) => {
|
const Layout: React.FC = ({ children }) => {
|
||||||
|
const { isDesktop } = useWindowDimensions();
|
||||||
const { authStore, userStore } = useRootStore();
|
const { authStore, userStore } = useRootStore();
|
||||||
const handleError = useErrorHandler();
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
@ -27,10 +29,19 @@ const Layout: React.FC = ({ children }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
<Nav />
|
{isDesktop && <NavDesktop />}
|
||||||
|
|
||||||
<Flex minH='100vh' flexDirection='column'>
|
<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'>
|
<Box as='main' mr='auto' ml='auto' maxW='85rem' pl={5} pr={5} pt='2rem' pb='2rem'>
|
||||||
{children}
|
{children}
|
||||||
<ColorModeSwitcher position='absolute' right={0} bottom={0} margin={5} />
|
<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 React from 'react';
|
||||||
import { Box, useColorMode } from '@chakra-ui/react';
|
import { Box, useColorMode } from '@chakra-ui/react';
|
||||||
import type { History } from 'history';
|
import type { LinkType } from '../../../interfaces';
|
||||||
import type { LinkType } from '../../interfaces';
|
import { useHistory } from 'react-router-dom';
|
||||||
import type { ActiveLinkState } from './Nav';
|
|
||||||
|
type ActiveLinkState = {
|
||||||
|
title: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* NavLink component
|
* NavLink component
|
||||||
* Returns the link component for navigation.
|
* Returns the link component for navigation.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
const NavLink: React.FC<{ link: LinkType; onCloseDrawer?: () => void }> = ({ link, onCloseDrawer }) => {
|
||||||
interface NavLinkProps {
|
const [activeLink, setActiveLink] = React.useState<ActiveLinkState>({ title: null });
|
||||||
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 { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
const onMouseEnter = () => handleOnHoverLink(link.title);
|
const history = useHistory();
|
||||||
const onClick = () => handleRoute(link.location);
|
|
||||||
|
const handleClick = () => {
|
||||||
|
history.push(link.location);
|
||||||
|
|
||||||
|
/* Close mobile drawer on click link */
|
||||||
|
onCloseDrawer && onCloseDrawer();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as='a'
|
as='a'
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={() => setActiveLink({ title: link.title })}
|
||||||
onMouseLeave={handleOnLeaveLink}
|
onMouseLeave={() => setActiveLink({ title: null })}
|
||||||
onClick={onClick}
|
onClick={handleClick}
|
||||||
display='flex'
|
display='flex'
|
||||||
margin='3px 0'
|
margin='3px 0'
|
||||||
width='100%'
|
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';
|
import { IAppConfig } from './interfaces';
|
||||||
|
|
||||||
const config: IAppConfig = {
|
const config: IAppConfig = {
|
||||||
|
metaData: {
|
||||||
|
appName: 'Educt',
|
||||||
|
appDescription: 'Learn management system',
|
||||||
|
},
|
||||||
links: [
|
links: [
|
||||||
{ location: '/', title: 'Home', icon: MdHome },
|
{ location: '/', title: 'Home', icon: MdHome },
|
||||||
{ location: '/courses', title: 'Courses', icon: MdCollectionsBookmark },
|
{ 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 {
|
export interface IAppConfig {
|
||||||
|
metaData: {
|
||||||
|
appName: string;
|
||||||
|
appDescription: string;
|
||||||
|
};
|
||||||
links: LinkType[];
|
links: LinkType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const AuthPage: React.FC<IPageProps> = () => {
|
|||||||
<Box p={8} width='full' maxWidth='450px' borderWidth={1} borderRadius={8}>
|
<Box p={8} width='full' maxWidth='450px' borderWidth={1} borderRadius={8}>
|
||||||
<Box textAlign='center'>
|
<Box textAlign='center'>
|
||||||
<Flex justifyContent='center'>
|
<Flex justifyContent='center'>
|
||||||
<MdSchool fill='blue' size='64px' />
|
<Box as={MdSchool} color='blue.500' size='64px' />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { AxiosInstance } from 'axios';
|
import { AxiosInstance } from 'axios';
|
||||||
import { autorun, makeAutoObservable } from 'mobx';
|
import { makeAutoObservable, runInAction } from 'mobx';
|
||||||
import { IUserResult } from '../interfaces';
|
import { IUserResult } from '../interfaces';
|
||||||
import UserService from '../services/UserService';
|
import UserService from '../services/UserService';
|
||||||
import RootStore from './RootStore';
|
import RootStore from './RootStore';
|
||||||
@ -30,7 +30,7 @@ export default class UserStore {
|
|||||||
try {
|
try {
|
||||||
const result = await this.userService.fetchMe();
|
const result = await this.userService.fetchMe();
|
||||||
|
|
||||||
autorun(() => {
|
runInAction(() => {
|
||||||
this.me = result.data;
|
this.me = result.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user