mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
refactor: header component
This commit is contained in:
parent
ee64ac964c
commit
25bed8b538
@ -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();
|
||||||
|
|||||||
@ -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);
|
|
||||||
14
src/components/Header/Logo.tsx
Normal file
14
src/components/Header/Logo.tsx
Normal 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 };
|
||||||
53
src/components/Header/Notifications.tsx
Normal file
53
src/components/Header/Notifications.tsx
Normal 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 };
|
||||||
@ -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);
|
|
||||||
@ -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 };
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import Header from './Header';
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
48
src/components/Header/index.tsx
Normal file
48
src/components/Header/index.tsx
Normal 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;
|
||||||
@ -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'>
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import CourseList from './CourseList';
|
|
||||||
import CourseItem from './CourseItem';
|
|
||||||
|
|
||||||
export { CourseList, CourseItem };
|
|
||||||
Loading…
x
Reference in New Issue
Block a user