feat: profile page layout & fix html layout

This commit is contained in:
Sergey Yarkov 2021-08-29 23:47:26 +03:00
parent 960bec4d5e
commit 846d96473f
10 changed files with 312 additions and 129 deletions

View File

@ -5,39 +5,14 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Learn management system" />
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will <title>Application</title>
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body> </body>
</html> </html>

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Flex, Box } from '@chakra-ui/react'; import { Flex, Box, useColorMode } 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';
@ -11,6 +11,7 @@ const Header: React.FC = () => {
const { const {
uiStore: { isDesktop }, uiStore: { isDesktop },
} = useRootStore(); } = useRootStore();
const { colorMode } = useColorMode();
return ( return (
<Box <Box
@ -20,8 +21,7 @@ const Header: React.FC = () => {
left={0} left={0}
right={0} right={0}
borderBottomWidth={1} borderBottomWidth={1}
width='100%' backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}
height='4rem'
zIndex={4} zIndex={4}
> >
<Flex <Flex
@ -29,8 +29,7 @@ const Header: React.FC = () => {
width='100%' width='100%'
height='100%' height='100%'
alignItems='center' alignItems='center'
paddingLeft='1.5rem' padding='5px 1.5rem 5px 1.5rem'
paddingRight='1.5rem'
> >
<AppName /> <AppName />
<Flex> <Flex>

View File

@ -24,16 +24,6 @@ const UserInfo: React.FC = () => {
const { userStore } = useRootStore(); const { userStore } = useRootStore();
const { logout } = useLogoutQuery(); const { logout } = useLogoutQuery();
// const onLogoutHandler = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
// try {
// await authStore.logout();
// history.push('/auth');
// toast({ title: 'You are logged out.', isClosable: true, status: 'info' });
// } catch (error) {
// console.error(error);
// }
// };
if (userStore.me === null) { if (userStore.me === null) {
return <Skeleton width='260px' height='40px' borderRadius='md' />; return <Skeleton width='260px' height='40px' borderRadius='md' />;
} }

View File

@ -6,7 +6,6 @@ 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 'components/ColorModeSwitcher/ColorModeSwitcher'; import { ColorModeSwitcher } from 'components/ColorModeSwitcher/ColorModeSwitcher';
import LoadingPage from 'components/Loading/LoadingPage';
/** /**
* *
@ -24,24 +23,29 @@ const Layout: React.FC = ({ children }) => {
return ( return (
<> <>
<Header /> <Header />
<NavDesktop /> <Flex minH='100vh'>
<Flex minH='100vh' flexDirection='column'> <NavDesktop />
<Box <Box
pl='80' as='main'
mt='20'
mb='10'
flex='1 0 auto' flex='1 0 auto'
maxW='85rem'
ml='25rem'
mt='7rem'
mb='1rem'
pr='2rem'
pl='2rem'
sx={{ sx={{
'@media (max-width: 768px)': { '@media (max-width: 1280px)': {
paddingLeft: 0, ml: '20rem',
},
'@media (max-width: 991px)': {
ml: '0',
}, },
}} }}
> >
<Box as='main' mr='auto' ml='auto' maxW='85rem' pl={5} pr={5} pt='2rem' pb='2rem'> {children}
{userStore.me === null ? <LoadingPage /> : children}
<ColorModeSwitcher position='absolute' right={0} bottom={0} margin={5} />
</Box>
</Box> </Box>
<ColorModeSwitcher position='absolute' right={0} bottom={0} margin={5} />
</Flex> </Flex>
</> </>
); );

View File

@ -0,0 +1,24 @@
import React from 'react';
import { Box, Skeleton, SkeletonCircle, Flex, Spinner } from '@chakra-ui/react';
const LoadingProfilePage = () => {
return (
<Box maxW='900px'>
<Skeleton height='35px' width='250px' borderRadius='md' />
<Flex alignItems='center' mt='20'>
<Box>
<SkeletonCircle size='128px' />
</Box>
<Flex ml='50' flexDirection='column'>
<Skeleton height='25px' width='200px' mb='5' borderRadius='md' />
<Skeleton height='25px' width='300px' borderRadius='md' />
</Flex>
</Flex>
<Flex mt='20' justifyContent='center'>
<Spinner size='xl' />
</Flex>
</Box>
);
};
export default LoadingProfilePage;

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Flex, Box } from '@chakra-ui/react'; import { Flex, Box, useColorMode } from '@chakra-ui/react';
import NavList from '../NavList'; import NavList from '../NavList';
import { useRootStore } from 'hooks/useRootStore'; import { useRootStore } from 'hooks/useRootStore';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
@ -14,22 +14,33 @@ const Nav: React.FC = () => {
const { const {
uiStore: { isDesktop }, uiStore: { isDesktop },
} = useRootStore(); } = useRootStore();
const { colorMode } = useColorMode();
if (isDesktop) { if (isDesktop) {
return ( return (
<Box display='block' position='fixed' left={0} right={0} top={0} width='100%' height='100%' maxW='18rem'> <Box
<Box top='4rem' position='relative' overflowY='auto' borderRightWidth={1}> borderRightWidth='1px'
<Flex w='18rem'
as='nav' position='fixed'
flexDirection='column' bottom='0'
alignItems='flex-start' top='0'
textDecoration='none' left='0'
height='calc(100vh - 4rem)' overflowY='auto'
padding='5' zIndex='1'
> backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}
<NavList links={config.links} /> >
</Flex> <Flex
</Box> as='nav'
flexDirection='column'
alignItems='flex-start'
textDecoration='none'
pt='4rem'
pr='5'
pl='5'
pb='5'
>
<NavList links={config.links} />
</Flex>
</Box> </Box>
); );
} }

View File

@ -11,11 +11,13 @@ import { HamburgerIcon } from '@chakra-ui/icons';
import { useDisclosure, IconButton, useColorMode, Skeleton } from '@chakra-ui/react'; import { useDisclosure, IconButton, useColorMode, Skeleton } from '@chakra-ui/react';
import NavList from '../NavList'; import NavList from '../NavList';
import config from 'config'; import config from 'config';
import useLogoutQuery from 'hooks/useLogoutQuery';
const NavMobile: React.FC = () => { const NavMobile: React.FC = () => {
const { userStore } = useRootStore(); const { userStore } = useRootStore();
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { logout } = useLogoutQuery();
return ( return (
<> <>
@ -24,46 +26,50 @@ const NavMobile: React.FC = () => {
<DrawerOverlay /> <DrawerOverlay />
<DrawerContent backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}> <DrawerContent backgroundColor={colorMode === 'dark' ? 'gray.800' : 'white'}>
<DrawerBody> <DrawerBody>
<Flex as='nav' flexDirection='column' alignItems='flex-start' textDecoration='none'> <Flex flexDirection='column' height='100%'>
<NavList links={config.links} onCloseDrawer={onClose} /> <Flex as='nav' flexDirection='column' alignItems='flex-start' textDecoration='none' mb='20px'>
</Flex> <NavList links={config.links} onCloseDrawer={onClose} />
{userStore.me ? ( </Flex>
<Box {userStore.me ? (
position='absolute' <Box
bottom={0} minHeight='60px'
right={0} padding='10px'
left={0} borderRadius='md'
minHeight='60px' marginTop='auto'
padding='10px' backgroundColor={colorMode === 'dark' ? 'gray.700' : 'gray.100'}
margin='5' >
borderRadius='md' <Flex alignItems='center'>
backgroundColor={colorMode === 'dark' ? 'gray.700' : 'gray.100'} <Avatar size='sm' name={`${userStore.me.first_name} ${userStore.me.last_name}`} marginRight={3} />
> <Flex flexDirection='column'>
<Flex alignItems='center'> <Text as='span' mr={2} lineHeight='1.2'>
<Avatar size='sm' name={`${userStore.me.first_name} ${userStore.me.last_name}`} marginRight={3} /> {`${userStore.me.first_name} ${userStore.me.last_name}`}
<Flex flexDirection='column'> </Text>
<Text as='span' mr={2} lineHeight='1.2'> <Link to={`/profile`} style={{ fontSize: '13px', textDecoration: 'underline' }} onClick={onClose}>
{`${userStore.me.first_name} ${userStore.me.last_name}`} View profile
</Text> </Link>
<Link to={`/profile`} style={{ fontSize: '13px', textDecoration: 'underline' }} onClick={onClose}> </Flex>
View profile <IconButton
</Link> onClick={logout}
aria-label='Logout'
backgroundColor={colorMode === 'dark' ? 'gray.600' : 'gray.200'}
marginLeft='auto'
icon={<MdExitToApp />}
/>
</Flex> </Flex>
<IconButton aria-label='Logout' backgroundColor='gray.200' marginLeft='auto' icon={<MdExitToApp />} /> </Box>
</Flex> ) : (
</Box> <Skeleton
) : ( h='60px'
<Skeleton position='absolute'
h='60px' borderRadius='md'
position='absolute' bottom={0}
borderRadius='md' right={0}
bottom={0} left={0}
right={0} padding='10px'
left={0} margin='5'
padding='10px' />
margin='5' )}
/> </Flex>
)}
</DrawerBody> </DrawerBody>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>

View File

@ -1,9 +1,9 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import NavLink from './NavLink'; import NavLink from './NavLink';
// import { useRootStore } from 'hooks/useRootStore'; import { useRootStore } from 'hooks/useRootStore';
// import { userHasRoles } from 'helpers'; import { userHasRoles } from 'helpers';
// import { UserRoleEnum } from 'enums'; import { UserRoleEnum } from 'enums';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { LinkType } from 'interfaces'; import { LinkType } from 'interfaces';
@ -16,7 +16,7 @@ type NavListPropsType = {
* Render a navigation list of links * Render a navigation list of links
*/ */
const NavList: React.FC<NavListPropsType> = ({ links, onCloseDrawer }) => { const NavList: React.FC<NavListPropsType> = ({ links, onCloseDrawer }) => {
// const { userStore } = useRootStore(); const { userStore } = useRootStore();
const history = useHistory(); const history = useHistory();
const onClickLink = useCallback( const onClickLink = useCallback(
(link: LinkType) => { (link: LinkType) => {
@ -31,7 +31,14 @@ const NavList: React.FC<NavListPropsType> = ({ links, onCloseDrawer }) => {
return ( return (
<> <>
{links.map(link => { {links.map(link => {
return <NavLink link={link} key={link.location} onClickLink={onClickLink} />; if (link.public) {
return <NavLink link={link} key={link.location} onClickLink={onClickLink} />;
} else {
if (userStore.me && userHasRoles(userStore.me.roles, [UserRoleEnum.ADMIN])) {
return <NavLink link={link} key={link.location} onClickLink={onClickLink} />;
}
}
return null;
})} })}
</> </>
); );

View File

@ -1,15 +1,39 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Box, Heading, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Grid } from '@chakra-ui/react'; import {
Box,
Heading,
Flex,
Avatar,
useColorMode,
Text,
Stack,
Button,
StackDivider,
FormControl,
Input,
Tabs,
TabList,
Tab,
TabPanels,
TabPanel,
FormHelperText,
} from '@chakra-ui/react';
import { MdSave } from 'react-icons/md';
import { IPageProps } from 'interfaces'; import { IPageProps } from 'interfaces';
import { useRootStore } from 'hooks/useRootStore'; import { useRootStore } from 'hooks/useRootStore';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { observer } from 'mobx-react';
import UserBadge from 'components/User/UserBadge';
import LoadingProfilePage from 'components/Loading/LoadingProfilePage';
/** /**
* Profile page * Profile page
*/ */
const ProfilePage: React.FC<IPageProps> = ({ title }) => { const ProfilePage: React.FC<IPageProps> = ({ title }) => {
const { authStore } = useRootStore(); const { colorMode } = useColorMode();
const { userStore, authStore } = useRootStore();
const history = useHistory(); const history = useHistory();
useEffect(() => { useEffect(() => {
@ -18,23 +42,166 @@ const ProfilePage: React.FC<IPageProps> = ({ title }) => {
} }
}); });
if (userStore.me === null) {
return <LoadingProfilePage />;
}
return ( return (
<> <Box maxW='900px'>
<Breadcrumb fontWeight='medium' fontSize='sm'> <Heading as='h1'>My account</Heading>
<BreadcrumbItem isCurrentPage> <Flex
<BreadcrumbLink href='/'>{title}</BreadcrumbLink> alignItems='center'
</BreadcrumbItem> mt='20'
</Breadcrumb> sx={{
<Box mt='10'> '@media (max-width: 768px)': {
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
mt: '10',
},
}}
>
<Box> <Box>
<Heading as='h1'>Profile page</Heading> <Avatar
<Grid templateColumns='3fr 1fr' mt='25px'> border={`4px solid ${colorMode === 'dark' ? '#2D3748' : '#E2E8F0'}`}
... size='2xl'
</Grid> name={`${userStore.me.first_name} ${userStore.me.last_name}`}
/>
</Box> </Box>
<Flex
ml='50'
flexDirection='column'
sx={{
'@media (max-width: 768px)': {
ml: '0',
mt: '5',
},
}}
>
<Heading as='h2'>{`${userStore.me.first_name} ${userStore.me.last_name}`}</Heading>
<Flex
alignItems='center'
sx={{
'@media (max-width: 768px)': {
flexDirection: 'column',
},
}}
>
<Text
mr='10px'
sx={{
'@media (max-width: 768px)': {
mr: '0',
mb: '5px',
},
}}
>
{userStore.me.email}
</Text>
<UserBadge roles={userStore.me.roles} />
</Flex>
</Flex>
</Flex>
<Box mt='10'>
<Tabs>
<TabList>
<Tab>Account information</Tab>
<Tab>My contacts</Tab>
</TabList>
<TabPanels>
{/* Account info */}
<TabPanel padding='10px 0' mt='20px'>
<Box borderRadius='md' borderWidth='1px' padding='20px'>
<Heading as='h3' size='lg'>
Account information
</Heading>
<Stack spacing='10px' mt='20px' divider={<StackDivider />}>
<Flex>
<Box>
<Text as='small' color='gray.500'>
First Name
</Text>
<Text fontWeight='medium' fontSize='lg'>
{userStore.me.first_name}
</Text>
</Box>
</Flex>
<Flex>
<Box>
<Text as='small' color='gray.500'>
Last Name
</Text>
<Text fontWeight='medium' fontSize='lg'>
{userStore.me.last_name}
</Text>
</Box>
</Flex>
<Flex justifyContent='space-between' alignItems='center'>
<Box>
<Text as='small' color='gray.500'>
Email
</Text>
<Text fontWeight='medium' fontSize='lg'>
{userStore.me.email}
</Text>
</Box>
<Button>Edit</Button>
</Flex>
<Flex justifyContent='space-between' alignItems='center'>
<Box>
<Text as='small' color='gray.500'>
Password
</Text>
<Text fontWeight='medium' fontSize='lg'>
**********
</Text>
</Box>
<Button>Change</Button>
</Flex>
</Stack>
</Box>
</TabPanel>
{/* Contacts */}
<TabPanel padding='10px 0' mt='20px'>
<Box as='form'>
<Box borderRadius='md' borderWidth='1px' padding='20px'>
<Heading as='h3' size='lg'>
My Contacts
</Heading>
<Stack spacing='5px' margin='20px 0'>
<FormControl id='tel'>
<FormHelperText color='gray.500'>Telephone</FormHelperText>
<Input type='number' size='md' variant='flushed' placeholder='123-456-7890' />
</FormControl>
<FormControl id='twitter'>
<FormHelperText color='gray.500'>Twitter</FormHelperText>
<Input type='text' size='md' variant='flushed' placeholder='https://twitter.com/id' />
</FormControl>
<FormControl id='telegram'>
<FormHelperText color='gray.500'>Telegram</FormHelperText>
<Input type='text' size='md' variant='flushed' placeholder='@tag' />
</FormControl>
<FormControl id='vk'>
<FormHelperText color='gray.500'>VKontakte</FormHelperText>
<Input type='text' size='md' variant='flushed' placeholder='https://vk.com/id' />
</FormControl>
</Stack>
</Box>
<Button colorScheme='blue' mt='4' type='submit' size='md' variant='outline' rightIcon={<MdSave />}>
Save
</Button>
</Box>
</TabPanel>
</TabPanels>
</Tabs>
</Box> </Box>
</> </Box>
); );
}; };
export default ProfilePage; export default observer(ProfilePage);

View File

@ -54,11 +54,11 @@ export default class UIStore {
} }
get isDesktop() { get isDesktop() {
return this.windowDimensions.width > 768; return this.windowDimensions.width > 991;
} }
get isTablet() { get isTablet() {
return this.windowDimensions.width < 768; return this.windowDimensions.width < 991;
} }
get isMobile() { get isMobile() {