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 { 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 { 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';
|
||||
|
||||
const ProfileMenu: React.FC = () => {
|
||||
const { userStore } = useRootStore();
|
||||
const Profile: React.FC = observer(() => {
|
||||
const {
|
||||
userStore: { me },
|
||||
} = useRootStore();
|
||||
const { logout } = useLogout();
|
||||
|
||||
if (userStore.me === null) {
|
||||
if (me === null) {
|
||||
return <Skeleton width='260px' height='40px' borderRadius='md' />;
|
||||
}
|
||||
|
||||
@ -37,15 +39,15 @@ const ProfileMenu: React.FC = () => {
|
||||
<Menu>
|
||||
<MenuButton as={Button} pr={6}>
|
||||
<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}>
|
||||
{userStore.me.fullname}
|
||||
{me.fullname}
|
||||
</Text>
|
||||
<UserBadge roles={userStore.me.roles} />
|
||||
<UserBadge roles={me.roles} />
|
||||
</Flex>
|
||||
</MenuButton>
|
||||
<MenuList mr='1rem'>
|
||||
<MenuGroup title={userStore.me.fullname}>
|
||||
<MenuGroup title={me.fullname}>
|
||||
<MenuDivider />
|
||||
<Link to='/profile'>
|
||||
<MenuItem>
|
||||
@ -62,6 +64,6 @@ const ProfileMenu: React.FC = () => {
|
||||
</Menu>
|
||||
</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';
|
||||
|
||||
type LayoutPropsType = { roles: UserRoleEnum[] | undefined };
|
||||
interface ILayoutProps {
|
||||
roles?: UserRoleEnum[] | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the current page using layout
|
||||
*/
|
||||
const Layout: React.FC<LayoutPropsType> = ({ children, roles }) => {
|
||||
const Layout: React.FC<ILayoutProps> = ({ children, roles }) => {
|
||||
const { userStore } = useRootStore();
|
||||
const { socket } = useContext(SocketContext);
|
||||
const handleError = useErrorHandler();
|
||||
@ -69,7 +71,14 @@ const Layout: React.FC<LayoutPropsType> = ({ children, roles }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Header>
|
||||
<Header.Logo />
|
||||
<Flex>
|
||||
<Header.ThemeSwitcher />
|
||||
<Header.Notifications />
|
||||
<Header.Profile />
|
||||
</Flex>
|
||||
</Header>
|
||||
<NavDesktop />
|
||||
<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'>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
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 { Flex, Box, Text, Link } from '@chakra-ui/layout';
|
||||
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody } from '@chakra-ui/modal';
|
||||
@ -36,16 +36,19 @@ const NavMobile: React.FC = () => {
|
||||
<Drawer placement='left' onClose={onClose} isOpen={isOpen}>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}>
|
||||
<DrawerBody>
|
||||
<DrawerBody p='0'>
|
||||
<Flex flexDirection='column' height='100%'>
|
||||
<Box mt='2'>
|
||||
<Box px='6' py='2'>
|
||||
<Logo h='10' ml='2' />
|
||||
</Box>
|
||||
<Flex flexDir='column' mt='3' w='full'>
|
||||
<Divider />
|
||||
<Flex px='6' flexDir='column' mt='3' w='full'>
|
||||
<NavList links={config.links} onCloseDrawer={onClose} />
|
||||
</Flex>
|
||||
{me !== null ? (
|
||||
<Box
|
||||
mx='6'
|
||||
my='3'
|
||||
minHeight='60px'
|
||||
padding='10px'
|
||||
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