refactor: header component

This commit is contained in:
Sergey Yarkov 2022-02-07 09:56:59 +03:00
parent ee64ac964c
commit 25bed8b538
13 changed files with 146 additions and 230 deletions

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import { useColorMode, useColorModeValue, IconButton, IconButtonProps } from '@chakra-ui/react'; import { useColorMode, useColorModeValue, IconButton, IconButtonProps } from '@chakra-ui/react';
import { FaMoon, FaSun } from 'react-icons/fa'; import { FaMoon, FaSun } from 'react-icons/fa';
type ColorModeSwitcherProps = Omit<IconButtonProps, 'aria-label'>; export type ColorModeSwitcherProps = Omit<IconButtonProps, 'aria-label'>;
const ColorModeSwitcher: React.FC<ColorModeSwitcherProps> = props => { const ColorModeSwitcher: React.FC<ColorModeSwitcherProps> = props => {
const { toggleColorMode } = useColorMode(); const { toggleColorMode } = useColorMode();

View File

@ -1,55 +0,0 @@
import React from 'react';
import { observer } from 'mobx-react';
import { Flex, Box } from '@chakra-ui/react';
/**
* Components
*/
import Logo from '@educt/components/Logo';
import NavMobile from '@educt/components/Nav/Mobile';
import ColorModeSwitcher from '@educt/components/ColorModeSwitcher';
import ProfileMenu from './ProfileMenu';
import NotificationsMenu from './NotificationsMenu';
/**
* Hooks
*/
import { useMediaQuery } from '@chakra-ui/media-query';
import { useColorModeValue } from '@chakra-ui/color-mode';
const Header: React.FC = () => {
const [isDesktop] = useMediaQuery('(min-width: 992px)');
return (
<Box
as='header'
position='fixed'
top={0}
left={0}
right={0}
borderBottomWidth='1px'
backgroundColor={useColorModeValue('white', 'gray.800')}
zIndex={4}
>
<Flex
justifyContent='space-between'
width='100%'
height='100%'
alignItems='center'
padding='5px 1.5rem 5px 1.5rem'
>
<Logo h='10' />
{isDesktop && (
<Flex>
<ColorModeSwitcher />
<NotificationsMenu />
<ProfileMenu />
</Flex>
)}
{!isDesktop && <NavMobile />}
</Flex>
</Box>
);
};
export default observer(Header);

View File

@ -0,0 +1,14 @@
import React from 'react';
import { Link as ReactRouterLink } from 'react-router-dom';
import { Box, BoxProps, Link } from '@chakra-ui/react';
import LogoIcon from '@educt/components/Logo';
const Logo: React.FC<BoxProps> = props => (
<Box {...props}>
<Link as={ReactRouterLink} to='/' _focus={{ outline: 'none' }}>
<LogoIcon h='10' />
</Link>
</Box>
);
export { Logo };

View File

@ -0,0 +1,53 @@
import React from 'react';
import { Menu, MenuButton, MenuList, MenuGroup, IconButton, MenuDivider } from '@chakra-ui/react';
import { Skeleton } from '@chakra-ui/skeleton';
import { MdNotifications } from 'react-icons/md';
import { useRootStore } from '@educt/hooks/useRootStore';
import { observer } from 'mobx-react';
export interface INotificationsProps {
count?: number | undefined;
}
const Notifications: React.FC<INotificationsProps> = observer(props => {
const {
userStore: { me },
} = useRootStore();
const { count, ...menuProps } = props;
if (me === null) {
return <Skeleton width='50px' height='40px' mr={3} ml={3} borderRadius='md' />;
}
return (
<Menu isLazy {...menuProps}>
<MenuButton
as={IconButton}
mx='2'
aria-label='Show my notifications'
icon={<MdNotifications />}
_after={{
content: `"${count && count > 0 && count}"`,
display: `${count && count > 0 ? 'block' : 'none'}`,
position: 'absolute',
top: '5px',
right: '5px',
background: '#E53E3E',
color: '#fff',
fontSize: '10px',
width: '15px',
height: '15px',
borderRadius: '100%',
}}
/>
<MenuList>
<MenuGroup title={'Notifications'}>
<MenuDivider />
</MenuGroup>
</MenuList>
</Menu>
);
});
export { Notifications };

View File

@ -1,42 +0,0 @@
import React from 'react';
import { Box } from '@chakra-ui/layout';
import { Skeleton } from '@chakra-ui/skeleton';
import { Button } from '@chakra-ui/button';
import { MdNotifications } from 'react-icons/md';
import { useRootStore } from '@educt/hooks/useRootStore';
import { observer } from 'mobx-react';
const NotificationsMenu: React.FC = () => {
const { userStore } = useRootStore();
if (userStore.me === null) {
return <Skeleton width='50px' height='40px' mr={3} ml={3} borderRadius='md' />;
}
return (
<Button
mr={3}
ml={3}
variant='solid'
outline='none'
border='none'
_after={{
content: '"2"',
display: 'block',
position: 'absolute',
top: '8px',
right: '10px',
background: '#E53E3E',
color: '#fff',
fontSize: '11px',
width: '15px',
height: '15px',
borderRadius: '100%',
}}
>
<Box as={MdNotifications} boxSize='21px' />
</Button>
);
};
export default observer(NotificationsMenu);

View File

@ -24,11 +24,13 @@ import UserBadge from '@educt/components/UserBadge';
*/ */
import { useLogout } from '@educt/hooks/queries'; import { useLogout } from '@educt/hooks/queries';
const ProfileMenu: React.FC = () => { const Profile: React.FC = observer(() => {
const { userStore } = useRootStore(); const {
userStore: { me },
} = useRootStore();
const { logout } = useLogout(); const { logout } = useLogout();
if (userStore.me === null) { if (me === null) {
return <Skeleton width='260px' height='40px' borderRadius='md' />; return <Skeleton width='260px' height='40px' borderRadius='md' />;
} }
@ -37,15 +39,15 @@ const ProfileMenu: React.FC = () => {
<Menu> <Menu>
<MenuButton as={Button} pr={6}> <MenuButton as={Button} pr={6}>
<Flex alignItems='center'> <Flex alignItems='center'>
<Avatar size='sm' name={userStore.me.fullname} marginRight={3} /> <Avatar size='sm' name={me.fullname} marginRight={3} />
<Text as='span' mr={2}> <Text as='span' mr={2}>
{userStore.me.fullname} {me.fullname}
</Text> </Text>
<UserBadge roles={userStore.me.roles} /> <UserBadge roles={me.roles} />
</Flex> </Flex>
</MenuButton> </MenuButton>
<MenuList mr='1rem'> <MenuList mr='1rem'>
<MenuGroup title={userStore.me.fullname}> <MenuGroup title={me.fullname}>
<MenuDivider /> <MenuDivider />
<Link to='/profile'> <Link to='/profile'>
<MenuItem> <MenuItem>
@ -62,6 +64,6 @@ const ProfileMenu: React.FC = () => {
</Menu> </Menu>
</Flex> </Flex>
); );
}; });
export default observer(ProfileMenu); export { Profile };

View File

@ -1,3 +0,0 @@
import Header from './Header';
export default Header;

View File

@ -0,0 +1,48 @@
import React from 'react';
import { Flex, Box, BoxProps } from '@chakra-ui/react';
import { Logo } from './Logo';
import { Profile } from './Profile';
import { INotificationsProps, Notifications } from './Notifications';
import ColorModeSwitcher from '@educt/components/ColorModeSwitcher';
import NavMobile from '@educt/components/Nav/Mobile';
import type { ColorModeSwitcherProps } from '../ColorModeSwitcher/ColorModeSwitcher';
/**
* Hooks
*/
import { useColorModeValue, useMediaQuery } from '@chakra-ui/react';
interface IHeaderComposition {
Logo: React.FC<BoxProps>;
ThemeSwitcher: React.FC<ColorModeSwitcherProps>;
Notifications: React.FC<INotificationsProps>;
Profile: React.FC;
}
const Header: React.FC<BoxProps> & IHeaderComposition = ({ children }) => {
const [isDesktop] = useMediaQuery('(min-width: 992px)');
return (
<Box
as={'header'}
position={'fixed'}
top={0}
left={0}
right={0}
borderBottomWidth={'1px'}
backgroundColor={useColorModeValue('white', 'gray.800')}
zIndex={4}
>
<Flex justifyContent={'space-between'} w={'full'} height={'full'} alignItems={'center'} py={'2'} px={'6'}>
{isDesktop ? children : <NavMobile />}
</Flex>
</Box>
);
};
Header.Logo = Logo;
Header.ThemeSwitcher = props => <ColorModeSwitcher {...props} />;
Header.Notifications = Notifications;
Header.Profile = Profile;
export default Header;

View File

@ -28,12 +28,14 @@ import { useRootStore } from '@educt/hooks/useRootStore';
*/ */
import { SocketContext } from '@educt/contexts'; import { SocketContext } from '@educt/contexts';
type LayoutPropsType = { roles: UserRoleEnum[] | undefined }; interface ILayoutProps {
roles?: UserRoleEnum[] | undefined;
}
/** /**
* Render the current page using layout * Render the current page using layout
*/ */
const Layout: React.FC<LayoutPropsType> = ({ children, roles }) => { const Layout: React.FC<ILayoutProps> = ({ children, roles }) => {
const { userStore } = useRootStore(); const { userStore } = useRootStore();
const { socket } = useContext(SocketContext); const { socket } = useContext(SocketContext);
const handleError = useErrorHandler(); const handleError = useErrorHandler();
@ -69,7 +71,14 @@ const Layout: React.FC<LayoutPropsType> = ({ children, roles }) => {
return ( return (
<> <>
<Header /> <Header>
<Header.Logo />
<Flex>
<Header.ThemeSwitcher />
<Header.Notifications />
<Header.Profile />
</Flex>
</Header>
<NavDesktop /> <NavDesktop />
<Flex minH='100vh' justifyContent='center'> <Flex minH='100vh' justifyContent='center'>
<Box as='main' flex='1 0' maxW='85rem' ml={{ base: '0', lg: '20rem' }} mt='7rem' mb='1rem' pr='2rem' pl='2rem'> <Box as='main' flex='1 0' maxW='85rem' ml={{ base: '0', lg: '20rem' }} mt='7rem' mb='1rem' pr='2rem' pl='2rem'>

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Link as ReactRouterLink } from 'react-router-dom'; import { Link as ReactRouterLink } from 'react-router-dom';
import { IconButton, Skeleton } from '@chakra-ui/react'; import { IconButton, Skeleton, Divider } from '@chakra-ui/react';
import { Avatar } from '@chakra-ui/avatar'; import { Avatar } from '@chakra-ui/avatar';
import { Flex, Box, Text, Link } from '@chakra-ui/layout'; import { Flex, Box, Text, Link } from '@chakra-ui/layout';
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody } from '@chakra-ui/modal'; import { Drawer, DrawerOverlay, DrawerContent, DrawerBody } from '@chakra-ui/modal';
@ -36,16 +36,19 @@ const NavMobile: React.FC = () => {
<Drawer placement='left' onClose={onClose} isOpen={isOpen}> <Drawer placement='left' onClose={onClose} isOpen={isOpen}>
<DrawerOverlay /> <DrawerOverlay />
<DrawerContent backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}> <DrawerContent backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}>
<DrawerBody> <DrawerBody p='0'>
<Flex flexDirection='column' height='100%'> <Flex flexDirection='column' height='100%'>
<Box mt='2'> <Box px='6' py='2'>
<Logo h='10' ml='2' /> <Logo h='10' ml='2' />
</Box> </Box>
<Flex flexDir='column' mt='3' w='full'> <Divider />
<Flex px='6' flexDir='column' mt='3' w='full'>
<NavList links={config.links} onCloseDrawer={onClose} /> <NavList links={config.links} onCloseDrawer={onClose} />
</Flex> </Flex>
{me !== null ? ( {me !== null ? (
<Box <Box
mx='6'
my='3'
minHeight='60px' minHeight='60px'
padding='10px' padding='10px'
borderRadius='md' borderRadius='md'

View File

@ -1,69 +0,0 @@
import React from 'react';
import moment from 'moment';
import { Link as ReactRouterLink } from 'react-router-dom';
import { Tag, TagLabel, LinkBox, Text, LinkOverlay, Icon } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/layout';
/**
* Types
*/
import { UserCourseType } from '@educt/interfaces';
import { TimeIcon } from '@chakra-ui/icons';
/**
* Hooks
*/
import { useColorModeValue } from '@chakra-ui/color-mode';
import { MdOutlineVideoLibrary } from 'react-icons/md';
type CourseItemPropsType = {
course: UserCourseType;
};
const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => {
return (
<LinkBox as='div' p='3' borderBottomWidth='1px' borderRadius='lg' bg={useColorModeValue('gray.50', 'gray.700')}>
<Flex justifyContent='space-between' mt='2'>
<Text>
<LinkOverlay
as={ReactRouterLink}
fontSize='md'
fontWeight='medium'
to={`/course/${course.id}`}
overflowWrap='anywhere'
>
{course.title}
</LinkOverlay>
</Text>
<Box>
<Tag
size='sm'
color='white'
boxShadow={`inset 0 0 0px 1px ${course.category.color?.hex}`}
bg={course.category.color?.hex}
alignSelf='flex-start'
variant='outline'
>
<TagLabel>{course.category.title}</TagLabel>
</Tag>
</Box>
</Flex>
<Flex mt='2' alignItems='center' fontSize='sm' sx={{ gap: '9px' }}>
<Icon as={MdOutlineVideoLibrary} w='14px' h='14px' />
<Text mr='1'>{course.lessons_count} lessons</Text>
<Icon as={TimeIcon} w='14px' h='14px' />
<Box
as='time'
verticalAlign='middle'
dateTime={course.created_at}
color={useColorModeValue('gray.600', 'gray.400')}
>
Last update {moment(course.created_at).format('ll')}
</Box>
</Flex>
</LinkBox>
);
};
export default CourseItem;

View File

@ -1,40 +0,0 @@
import React from 'react';
import { Box, Text, Stack } from '@chakra-ui/layout';
/**
* Types
*/
import { IMe } from '@educt/interfaces';
/**
* Components
*/
import CourseItem from './CourseItem';
type CourseListPropsType = {
courses: IMe['courses'];
};
const CourseList: React.FC<CourseListPropsType> = ({ courses }) => {
if (courses === undefined) return null;
if (courses.length === 0) {
return (
<Box textAlign='center' mt='10'>
<Text color='gray.500' userSelect='none'>
You are not a member of any course
</Text>
</Box>
);
}
return (
<Stack>
{courses.map(course => (
<CourseItem course={course} key={course.id} />
))}
</Stack>
);
};
export default CourseList;

View File

@ -1,4 +0,0 @@
import CourseList from './CourseList';
import CourseItem from './CourseItem';
export { CourseList, CourseItem };