mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 02:09:08 +03:00
Merge branch 'feature/profile-page' into develop
This commit is contained in:
commit
6f855af3db
@ -1,4 +1,4 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import { Router, Switch } from 'react-router-dom';
|
import { Router, Switch } from 'react-router-dom';
|
||||||
import { routes } from './routes.config';
|
import { routes } from './routes.config';
|
||||||
|
|
||||||
|
|||||||
@ -53,7 +53,7 @@ const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({
|
|||||||
icon={MdGroup}
|
icon={MdGroup}
|
||||||
heading='Add new students'
|
heading='Add new students'
|
||||||
description='Attach new students to current course.'
|
description='Attach new students to current course.'
|
||||||
confirmText='Add students'
|
confirmText='Add'
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onProceed={handleAttachStudents}
|
onProceed={handleAttachStudents}
|
||||||
|
|||||||
@ -1,17 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BoxProps, Box, Flex, Heading, Text, Divider } from '@chakra-ui/react';
|
import { BoxProps, Box, Flex, Heading, Text, Divider } from '@chakra-ui/react';
|
||||||
|
|
||||||
interface IPageHeadingProps extends BoxProps {
|
interface IPageComposition {
|
||||||
|
Heading: React.FC<IPageHeadingProps & BoxProps>;
|
||||||
|
Content: React.FC<BoxProps>;
|
||||||
|
Footer: React.FC<BoxProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Page: React.FC<BoxProps> & IPageComposition = props => {
|
||||||
|
return <Box {...props}>{props.children}</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IPageHeadingProps {
|
||||||
headingPrefix?: JSX.Element;
|
headingPrefix?: JSX.Element;
|
||||||
heading: string;
|
heading: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageWrapper: React.FC<BoxProps> = props => {
|
Page.Heading = props => {
|
||||||
return <Box {...props}>{props.children}</Box>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PageHeading: React.FC<IPageHeadingProps> = props => {
|
|
||||||
const { headingPrefix, heading, description, ...boxProps } = props;
|
const { headingPrefix, heading, description, ...boxProps } = props;
|
||||||
return (
|
return (
|
||||||
<Box {...boxProps}>
|
<Box {...boxProps}>
|
||||||
@ -26,15 +32,17 @@ export const PageHeading: React.FC<IPageHeadingProps> = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageContent: React.FC<BoxProps> = props => (
|
Page.Content = props => (
|
||||||
<Box mt='4' {...props}>
|
<Box mt='4' {...props}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PageFooter: React.FC<BoxProps> = props => (
|
Page.Footer = props => (
|
||||||
<Box mt='4' {...props}>
|
<Box mt='4' {...props}>
|
||||||
<Divider m='20px 0' />
|
<Divider m='20px 0' />
|
||||||
{props.children}
|
{props.children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export { Page };
|
||||||
|
|||||||
20
src/components/UserProfile/Avatar/Avatar.tsx
Normal file
20
src/components/UserProfile/Avatar/Avatar.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Flex, Avatar as ChakraAvatar, AvatarBadge as ChakraAvatarBadge, useColorModeValue } from '@chakra-ui/react';
|
||||||
|
import type { IAvatarProps } from '.';
|
||||||
|
|
||||||
|
const Avatar: React.FC<IAvatarProps> = props => {
|
||||||
|
const { name, isOnline } = props;
|
||||||
|
const onlineColor = isOnline ? 'green.500' : 'red.500';
|
||||||
|
const borderColor = useColorModeValue('#E2E8F0', '#2D3748');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir={'column'} alignItems={'center'}>
|
||||||
|
<ChakraAvatar border={`4px solid ${borderColor}`} borderColor={borderColor} name={name} size='2xl'>
|
||||||
|
{isOnline && <ChakraAvatarBadge boxSize='0.75em' bg={onlineColor} />}
|
||||||
|
</ChakraAvatar>
|
||||||
|
{props.children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Avatar };
|
||||||
19
src/components/UserProfile/Avatar/ChatButton.tsx
Normal file
19
src/components/UserProfile/Avatar/ChatButton.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, ButtonProps } from '@chakra-ui/react';
|
||||||
|
import { MdOutlineMessage } from 'react-icons/md';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import type { IChatButtonProps } from '.';
|
||||||
|
|
||||||
|
const ChatButton: React.FC<IChatButtonProps & ButtonProps> = props => {
|
||||||
|
const { userid } = props;
|
||||||
|
const history = useHistory();
|
||||||
|
const handleChat = () => history.push(`/chat/${userid}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={handleChat} mt='5' variant={'outline'} size={'sm'} leftIcon={<MdOutlineMessage />} {...props}>
|
||||||
|
Chat
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ChatButton };
|
||||||
13
src/components/UserProfile/Avatar/index.ts
Normal file
13
src/components/UserProfile/Avatar/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Avatar } from './Avatar';
|
||||||
|
import { ChatButton } from './ChatButton';
|
||||||
|
|
||||||
|
export interface IAvatarProps {
|
||||||
|
name: string;
|
||||||
|
isOnline?: boolean | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChatButtonProps {
|
||||||
|
userid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Avatar, ChatButton };
|
||||||
12
src/components/UserProfile/Base/Base.tsx
Normal file
12
src/components/UserProfile/Base/Base.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Flex, FlexProps } from '@chakra-ui/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Base: React.FC<FlexProps> = props => {
|
||||||
|
return (
|
||||||
|
<Flex flexDir={{ base: 'column', md: 'row' }} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Base };
|
||||||
1
src/components/UserProfile/Base/index.ts
Normal file
1
src/components/UserProfile/Base/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Base } from './Base';
|
||||||
21
src/components/UserProfile/Info/About.tsx
Normal file
21
src/components/UserProfile/Info/About.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from '@chakra-ui/react';
|
||||||
|
import type { IAboutProps } from '.';
|
||||||
|
|
||||||
|
const About: React.FC<IAboutProps> = ({ about }) => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text fontSize={'sm'}>
|
||||||
|
{about ? (
|
||||||
|
about
|
||||||
|
) : (
|
||||||
|
<Text as='span' color='gray.500' userSelect={'none'}>
|
||||||
|
No description provided
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { About };
|
||||||
31
src/components/UserProfile/Info/Details.tsx
Normal file
31
src/components/UserProfile/Info/Details.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Flex, Text, Icon, Divider } from '@chakra-ui/react';
|
||||||
|
import { CalendarIcon, RepeatClockIcon } from '@chakra-ui/icons';
|
||||||
|
import type { IDetailsProps } from '.';
|
||||||
|
|
||||||
|
const Details: React.FC<IDetailsProps> = ({ registered, lastLogin }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex my={'3'} justifyContent={{ base: 'center', md: 'flex-start' }}>
|
||||||
|
<Flex>
|
||||||
|
<Flex alignItems={'center'} mr='5'>
|
||||||
|
<Icon as={CalendarIcon} color='gray.500' />
|
||||||
|
<Text fontWeight={'medium'} fontSize={'sm'} color='gray.500' ml='3'>
|
||||||
|
Joined: {new Date(registered).toLocaleDateString()}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex alignItems={'center'} mr='5'>
|
||||||
|
<Icon as={RepeatClockIcon} color='gray.500' />
|
||||||
|
<Text fontWeight={'medium'} fontSize={'sm'} color='gray.500' ml='3'>
|
||||||
|
Last login: {lastLogin ? `${moment(lastLogin).fromNow()}` : 'Never'}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Divider my='3' />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Details };
|
||||||
19
src/components/UserProfile/Info/Heading.tsx
Normal file
19
src/components/UserProfile/Info/Heading.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import UserBadge from '@educt/components/UserBadge';
|
||||||
|
import { Flex, Box, Heading as ChakraHeading } from '@chakra-ui/react';
|
||||||
|
import type { IHeadingProps } from '.';
|
||||||
|
|
||||||
|
const Heading: React.FC<IHeadingProps> = ({ fullname, roles }) => {
|
||||||
|
return (
|
||||||
|
<Flex justifyContent={{ base: 'center', md: 'flex-start' }} mt={{ base: '5', md: '0' }}>
|
||||||
|
<ChakraHeading as='h2' fontSize={'xl'} fontWeight={'bold'}>
|
||||||
|
{fullname} •
|
||||||
|
</ChakraHeading>
|
||||||
|
<Box>
|
||||||
|
<UserBadge roles={roles} />
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Heading };
|
||||||
12
src/components/UserProfile/Info/Info.tsx
Normal file
12
src/components/UserProfile/Info/Info.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const Info: React.FC = props => {
|
||||||
|
return (
|
||||||
|
<Box ml={{ base: '0', md: '10' }} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Info };
|
||||||
21
src/components/UserProfile/Info/index.ts
Normal file
21
src/components/UserProfile/Info/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { IUser } from '@educt/interfaces';
|
||||||
|
import { Info } from './Info';
|
||||||
|
import { Heading } from './Heading';
|
||||||
|
import { Details } from './Details';
|
||||||
|
import { About } from './About';
|
||||||
|
|
||||||
|
export interface IHeadingProps {
|
||||||
|
fullname: string;
|
||||||
|
roles: IUser['roles'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDetailsProps {
|
||||||
|
registered: string;
|
||||||
|
lastLogin: IUser['last_login'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAboutProps {
|
||||||
|
about: IUser['about'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Info, Heading, Details, About };
|
||||||
8
src/components/UserProfile/Settings/Entry.tsx
Normal file
8
src/components/UserProfile/Settings/Entry.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const Entry: React.FC = props => {
|
||||||
|
return <Flex flexDir={{ base: 'column', md: 'row' }}>{props.children}</Flex>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Entry };
|
||||||
12
src/components/UserProfile/Settings/EntryContent.tsx
Normal file
12
src/components/UserProfile/Settings/EntryContent.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const EntryContent: React.FC = props => {
|
||||||
|
return (
|
||||||
|
<Flex flexDir={'column'} w='full' mt={{ base: '6', md: '0' }}>
|
||||||
|
{props.children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { EntryContent };
|
||||||
196
src/components/UserProfile/Settings/Fields/index.tsx
Normal file
196
src/components/UserProfile/Settings/Fields/index.tsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputRightElement,
|
||||||
|
InputProps,
|
||||||
|
Button,
|
||||||
|
Textarea,
|
||||||
|
Text,
|
||||||
|
TextareaProps,
|
||||||
|
Select,
|
||||||
|
SelectProps,
|
||||||
|
Switch,
|
||||||
|
SwitchProps,
|
||||||
|
ButtonProps,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { SaveButton } from '@educt/components/Buttons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useColorMode } from '@chakra-ui/react';
|
||||||
|
import { useSettingsContext } from '../context';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
const InputWithButton: React.FC<
|
||||||
|
InputProps & { buttonText?: string | undefined; buttonHandler?: () => void }
|
||||||
|
> = props => {
|
||||||
|
const { buttonText, buttonHandler, ...inputProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputGroup size='md'>
|
||||||
|
<Input {...inputProps} />
|
||||||
|
<InputRightElement width='4.5rem' pr='0.5rem'>
|
||||||
|
<Button onClick={buttonHandler} h='1.75rem' size='sm'>
|
||||||
|
{buttonText || 'Click'}
|
||||||
|
</Button>
|
||||||
|
</InputRightElement>
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullnameInput: React.FC<InputProps> = props => (
|
||||||
|
<FormControl isReadOnly>
|
||||||
|
<FormLabel>Fullname</FormLabel>
|
||||||
|
<Input {...props} />
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const EmailInput: React.FC<InputProps> = props => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const onChangeEmail = () => history.push(`/profile/change-email`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl isReadOnly>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<InputWithButton buttonHandler={onChangeEmail} buttonText='Change' {...props} />
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AboutInput: React.FC<TextareaProps> = props => {
|
||||||
|
const {
|
||||||
|
form: {
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
},
|
||||||
|
} = useSettingsContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>About</FormLabel>
|
||||||
|
<Textarea minH='130px' maxH='500px' isInvalid={!!errors.about} {...register('about')} {...props} />
|
||||||
|
<Text as='small' color='red.500'>
|
||||||
|
{errors.about?.message}
|
||||||
|
</Text>
|
||||||
|
<FormHelperText>Write description for your profile.</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PhoneInput: React.FC<InputProps> = props => {
|
||||||
|
const {
|
||||||
|
form: {
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
},
|
||||||
|
} = useSettingsContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Phone</FormLabel>
|
||||||
|
<Input
|
||||||
|
placeholder='+1 (234) 555-1234'
|
||||||
|
isInvalid={!!errors.phone_number}
|
||||||
|
{...register('phone_number')}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<Text as='small' color='red.500'>
|
||||||
|
{errors.phone_number?.message}
|
||||||
|
</Text>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TwitterInput: React.FC<InputProps> = props => {
|
||||||
|
const {
|
||||||
|
form: {
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
},
|
||||||
|
} = useSettingsContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Twitter</FormLabel>
|
||||||
|
<Input placeholder='@username' isInvalid={!!errors.twitter_id} {...register('twitter_id')} {...props} />
|
||||||
|
<Text as='small' color='red.500'>
|
||||||
|
{errors.twitter_id?.message}
|
||||||
|
</Text>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TelegramInput: React.FC<InputProps> = props => {
|
||||||
|
const {
|
||||||
|
form: {
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
},
|
||||||
|
} = useSettingsContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Telegram</FormLabel>
|
||||||
|
<Input placeholder='@username' isInvalid={!!errors.telegram_id} {...register('telegram_id')} {...props} />
|
||||||
|
<Text as='small' color='red.500'>
|
||||||
|
{errors.telegram_id?.message}
|
||||||
|
</Text>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PasswordInput: React.FC<InputProps> = props => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const onChangePassword = () => history.push(`/profile/change-password`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl isReadOnly>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<InputWithButton buttonHandler={onChangePassword} buttonText='Change' {...props} />
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LanguageSelect: React.FC<SelectProps> = props => {
|
||||||
|
return (
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Display Language</FormLabel>
|
||||||
|
<Select {...props}>
|
||||||
|
<option value='en'>English</option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThemeSwitcher: React.FC<SwitchProps> = props => {
|
||||||
|
const { colorMode, toggleColorMode } = useColorMode();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Switch onChange={toggleColorMode} size='lg' isChecked={colorMode === 'dark'} {...props} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SaveSettingsButton: React.FC<ButtonProps> = props => {
|
||||||
|
const {
|
||||||
|
form: {
|
||||||
|
formState: { isDirty },
|
||||||
|
},
|
||||||
|
isLoading,
|
||||||
|
} = useSettingsContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<SaveButton type='submit' isLoading={isLoading} isDisabled={!isDirty} {...props} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
82
src/components/UserProfile/Settings/Settings.tsx
Normal file
82
src/components/UserProfile/Settings/Settings.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
BoxProps,
|
||||||
|
Heading as ChakraHeading,
|
||||||
|
Divider,
|
||||||
|
InputProps,
|
||||||
|
TextareaProps,
|
||||||
|
Stack,
|
||||||
|
StackDivider,
|
||||||
|
SelectProps,
|
||||||
|
ButtonProps,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Title, ITitleProps } from './Title';
|
||||||
|
import { Entry } from './Entry';
|
||||||
|
import { EntryContent } from './EntryContent';
|
||||||
|
import { SettingsContextProvider } from './SettingsContextProvider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form input fields
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
FullnameInput,
|
||||||
|
EmailInput,
|
||||||
|
AboutInput,
|
||||||
|
PhoneInput,
|
||||||
|
TwitterInput,
|
||||||
|
TelegramInput,
|
||||||
|
PasswordInput,
|
||||||
|
LanguageSelect,
|
||||||
|
ThemeSwitcher,
|
||||||
|
SaveSettingsButton,
|
||||||
|
} from './Fields';
|
||||||
|
import { SwitchProps } from 'react-router-dom';
|
||||||
|
|
||||||
|
export interface ISettingsComposition {
|
||||||
|
Title: React.FC<ITitleProps & BoxProps>;
|
||||||
|
Entry: React.FC;
|
||||||
|
EntryContent: React.FC;
|
||||||
|
FullnameInput: React.FC<InputProps>;
|
||||||
|
EmailInput: React.FC<InputProps>;
|
||||||
|
AboutInput: React.FC<TextareaProps>;
|
||||||
|
PhoneInput: React.FC<InputProps>;
|
||||||
|
TwitterInput: React.FC<InputProps>;
|
||||||
|
TelegramInput: React.FC<InputProps>;
|
||||||
|
PasswordInput: React.FC<InputProps>;
|
||||||
|
LanguageSelect: React.FC<SelectProps>;
|
||||||
|
ThemeSwitcher: React.FC<SwitchProps>;
|
||||||
|
SaveSettingsButton: React.FC<ButtonProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Settings: React.FC<BoxProps> & ISettingsComposition = props => {
|
||||||
|
return (
|
||||||
|
<SettingsContextProvider>
|
||||||
|
<Box maxW={{ base: 'full', lg: '750px' }} {...props}>
|
||||||
|
<ChakraHeading as='h2' mt='12' fontSize={'2xl'} fontWeight={'bold'}>
|
||||||
|
Account Settings
|
||||||
|
</ChakraHeading>
|
||||||
|
<Divider my='5' />
|
||||||
|
<Stack divider={<StackDivider />} spacing={'5'}>
|
||||||
|
{props.children}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</SettingsContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Settings.Title = Title;
|
||||||
|
Settings.Entry = Entry;
|
||||||
|
Settings.EntryContent = EntryContent;
|
||||||
|
Settings.FullnameInput = FullnameInput;
|
||||||
|
Settings.EmailInput = EmailInput;
|
||||||
|
Settings.AboutInput = AboutInput;
|
||||||
|
Settings.PhoneInput = PhoneInput;
|
||||||
|
Settings.TwitterInput = TwitterInput;
|
||||||
|
Settings.TelegramInput = TelegramInput;
|
||||||
|
Settings.PasswordInput = PasswordInput;
|
||||||
|
Settings.LanguageSelect = LanguageSelect;
|
||||||
|
Settings.ThemeSwitcher = ThemeSwitcher;
|
||||||
|
Settings.SaveSettingsButton = SaveSettingsButton;
|
||||||
|
|
||||||
|
export { Settings };
|
||||||
@ -1,6 +1,11 @@
|
|||||||
import yup from '@educt/schema';
|
import yup from '@educt/schema';
|
||||||
|
|
||||||
const UpdateUserContactsSchema = yup.object().shape({
|
const SettingsSchema = yup.object().shape({
|
||||||
|
about: yup
|
||||||
|
.string()
|
||||||
|
.max(250, 'Description must be at most 250 characters')
|
||||||
|
.transform(val => (!val ? null : val))
|
||||||
|
.nullable(true),
|
||||||
phone_number: yup
|
phone_number: yup
|
||||||
.string()
|
.string()
|
||||||
.isMobileValid(['en-US', 'ru-RU'], { strictMode: true })
|
.isMobileValid(['en-US', 'ru-RU'], { strictMode: true })
|
||||||
@ -16,11 +21,6 @@ const UpdateUserContactsSchema = yup.object().shape({
|
|||||||
.matches(/(^|[^@\w])@(\w{1,64})\b/, 'Telegram username is not valid')
|
.matches(/(^|[^@\w])@(\w{1,64})\b/, 'Telegram username is not valid')
|
||||||
.transform(val => (!val ? null : val))
|
.transform(val => (!val ? null : val))
|
||||||
.nullable(true),
|
.nullable(true),
|
||||||
vk_id: yup
|
|
||||||
.string()
|
|
||||||
.matches(/^([a-zA-Z0-9_]){1,64}$/, 'VKontakte username is not valid')
|
|
||||||
.transform(val => (!val ? null : val))
|
|
||||||
.nullable(true),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default UpdateUserContactsSchema;
|
export { SettingsSchema };
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { runInAction } from 'mobx';
|
||||||
|
import * as helpers from '@educt/helpers';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { FormTypes, SettingsContext } from './context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
|
import { useUpdateUserInfo } from '@educt/hooks/queries';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator
|
||||||
|
*/
|
||||||
|
import { SettingsSchema } from './Settings.validator';
|
||||||
|
|
||||||
|
const SettingsContextProvider: React.FC = ({ children }) => {
|
||||||
|
const {
|
||||||
|
userStore: { me },
|
||||||
|
} = useRootStore();
|
||||||
|
const form = useForm<FormTypes>({
|
||||||
|
resolver: yupResolver(SettingsSchema),
|
||||||
|
defaultValues: {
|
||||||
|
about: me?.about || '',
|
||||||
|
phone_number: me?.contacts?.phone_number || '',
|
||||||
|
twitter_id: me?.contacts?.twitter_id || '',
|
||||||
|
telegram_id: me?.contacts?.telegram_id || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { updateInfo, isLoading } = useUpdateUserInfo();
|
||||||
|
const {
|
||||||
|
formState: { dirtyFields },
|
||||||
|
reset,
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
const onSubmit = async (data: FormTypes) => {
|
||||||
|
try {
|
||||||
|
const updatedFields = helpers.getDirtyFields(dirtyFields, data);
|
||||||
|
|
||||||
|
const { about, phone_number, telegram_id, twitter_id } = await updateInfo(updatedFields);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update info in store
|
||||||
|
*/
|
||||||
|
runInAction(() => {
|
||||||
|
if (me !== null) {
|
||||||
|
me.about = about === undefined ? me.about : about;
|
||||||
|
if (me.contacts) {
|
||||||
|
me.contacts.phone_number = phone_number === undefined ? me.contacts.phone_number : phone_number;
|
||||||
|
me.contacts.twitter_id = twitter_id === undefined ? me.contacts.twitter_id : twitter_id;
|
||||||
|
me.contacts.telegram_id = telegram_id === undefined ? me.contacts.telegram_id : telegram_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reset(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContext.Provider value={{ form, isLoading }}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)}>{children}</form>
|
||||||
|
</SettingsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SettingsContextProvider };
|
||||||
18
src/components/UserProfile/Settings/Title.tsx
Normal file
18
src/components/UserProfile/Settings/Title.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, BoxProps, Heading } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export interface ITitleProps {
|
||||||
|
title?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Title: React.FC<ITitleProps & BoxProps> = props => {
|
||||||
|
return (
|
||||||
|
<Box minW='3xs' {...props}>
|
||||||
|
<Heading as='h2' fontWeight={'semibold'} fontSize={'lg'}>
|
||||||
|
{props.title}
|
||||||
|
</Heading>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Title };
|
||||||
26
src/components/UserProfile/Settings/context.ts
Normal file
26
src/components/UserProfile/Settings/context.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { UseFormReturn } from 'react-hook-form';
|
||||||
|
|
||||||
|
interface ISettingsContext {
|
||||||
|
form: UseFormReturn<FormTypes, object>;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormTypes = {
|
||||||
|
about: string;
|
||||||
|
phone_number: string;
|
||||||
|
twitter_id: string;
|
||||||
|
telegram_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsContext = createContext<ISettingsContext | undefined>(undefined);
|
||||||
|
|
||||||
|
export const useSettingsContext = () => {
|
||||||
|
const context = useContext(SettingsContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('SettingsContext was outside of its provider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
1
src/components/UserProfile/Settings/index.ts
Normal file
1
src/components/UserProfile/Settings/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Settings } from './Settings';
|
||||||
37
src/components/UserProfile/index.tsx
Normal file
37
src/components/UserProfile/index.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BoxProps, Flex, FlexProps } from '@chakra-ui/react';
|
||||||
|
import { Base } from './Base';
|
||||||
|
import { Avatar, ChatButton, IAvatarProps, IChatButtonProps } from './Avatar';
|
||||||
|
import { About, Details, Heading, IAboutProps, IDetailsProps, IHeadingProps, Info } from './Info';
|
||||||
|
import { Settings } from './Settings';
|
||||||
|
import { ISettingsComposition } from './Settings/Settings';
|
||||||
|
|
||||||
|
interface IUserProfileComposition {
|
||||||
|
Base: React.FC<FlexProps>;
|
||||||
|
Avatar: React.FC<IAvatarProps>;
|
||||||
|
ChatButton: React.FC<IChatButtonProps>;
|
||||||
|
Info: React.FC<BoxProps>;
|
||||||
|
Heading: React.FC<IHeadingProps>;
|
||||||
|
Details: React.FC<IDetailsProps>;
|
||||||
|
About: React.FC<IAboutProps>;
|
||||||
|
Settings: React.FC<BoxProps> & ISettingsComposition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserProfile: React.FC<FlexProps> & IUserProfileComposition = props => {
|
||||||
|
return (
|
||||||
|
<Flex flexDir={{ base: 'column' }} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UserProfile.Base = Base;
|
||||||
|
UserProfile.Avatar = Avatar;
|
||||||
|
UserProfile.ChatButton = ChatButton;
|
||||||
|
UserProfile.Info = Info;
|
||||||
|
UserProfile.Heading = Heading;
|
||||||
|
UserProfile.Details = Details;
|
||||||
|
UserProfile.About = About;
|
||||||
|
UserProfile.Settings = Settings;
|
||||||
|
|
||||||
|
export { UserProfile };
|
||||||
@ -29,6 +29,7 @@ import { useCreateUser } from './user/useCreateUser';
|
|||||||
import { useDeleteUser } from './user/useDeleteUser';
|
import { useDeleteUser } from './user/useDeleteUser';
|
||||||
import { useUpdateUser } from './user/useUpdateUser';
|
import { useUpdateUser } from './user/useUpdateUser';
|
||||||
import { useUpdateUserEmail } from './user/useUpdateUserEmail';
|
import { useUpdateUserEmail } from './user/useUpdateUserEmail';
|
||||||
|
import { useUpdateUserInfo } from './user/useUpdateUserInfo';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Category
|
* Category
|
||||||
@ -59,6 +60,7 @@ export {
|
|||||||
useCreateUser,
|
useCreateUser,
|
||||||
useDeleteUser,
|
useDeleteUser,
|
||||||
useUpdateUser,
|
useUpdateUser,
|
||||||
|
useUpdateUserInfo,
|
||||||
useUpdateUserEmail,
|
useUpdateUserEmail,
|
||||||
useCreateCategory,
|
useCreateCategory,
|
||||||
useDeleteCategory,
|
useDeleteCategory,
|
||||||
|
|||||||
25
src/hooks/queries/user/useFetchUser.ts
Normal file
25
src/hooks/queries/user/useFetchUser.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import useAsync from '@educt/hooks/useAsync';
|
||||||
|
import { IUser } from '@educt/interfaces';
|
||||||
|
import { UserServiceInstance } from '@educt/services';
|
||||||
|
|
||||||
|
const useFetchUser = (id: string) => {
|
||||||
|
const fetch = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const { data } = await UserServiceInstance.fetchUserById(id);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { execute: fetchUserById, ...state } = useAsync<IUser, Parameters<typeof fetch>>(fetch);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUserById(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { ...state };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useFetchUser };
|
||||||
42
src/hooks/queries/user/useUpdateUserInfo.ts
Normal file
42
src/hooks/queries/user/useUpdateUserInfo.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useToast } from '@chakra-ui/react';
|
||||||
|
import useAsync from '@educt/hooks/useAsync';
|
||||||
|
import { IUserInfo } from '@educt/interfaces';
|
||||||
|
import { UserServiceInstance } from '@educt/services';
|
||||||
|
import { UpdateUserInfoParamsType } from '@educt/types';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
|
|
||||||
|
const useUpdateUserInfo = () => {
|
||||||
|
const toast = useToast();
|
||||||
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
|
const fetch = async (data: UpdateUserInfoParamsType) => {
|
||||||
|
try {
|
||||||
|
const result = await UserServiceInstance.updateInfo(data);
|
||||||
|
toast({ title: 'Info updated.', status: 'info' });
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response) {
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 422:
|
||||||
|
toast({ title: error.response.data.errors[0].message, status: 'error', duration: 2000 });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { execute, ...state } = useAsync<Partial<IUserInfo>, Parameters<typeof fetch>>(fetch);
|
||||||
|
|
||||||
|
return { updateInfo: execute, ...state };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useUpdateUserInfo };
|
||||||
@ -116,6 +116,8 @@ export interface IUser {
|
|||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
fullname: string;
|
fullname: string;
|
||||||
|
about: string | null;
|
||||||
|
last_login: string | null;
|
||||||
email: string;
|
email: string;
|
||||||
roles: IUserRole[];
|
roles: IUserRole[];
|
||||||
contacts: IUserContacts | null;
|
contacts: IUserContacts | null;
|
||||||
@ -145,6 +147,10 @@ export interface IUserContacts {
|
|||||||
vk_id: string | null;
|
vk_id: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUserInfo extends IUserContacts {
|
||||||
|
about: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IUserRole {
|
export interface IUserRole {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { MdError } from 'react-icons/md';
|
|||||||
* Types
|
* Types
|
||||||
*/
|
*/
|
||||||
import { IPageProps } from '@educt/interfaces';
|
import { IPageProps } from '@educt/interfaces';
|
||||||
import { PageContent, PageWrapper } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks
|
* Hooks
|
||||||
@ -28,8 +28,8 @@ const NotFoundPage: React.FC<IPageProps> = ({ title }) => {
|
|||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<PageWrapper textAlign='center' mt={40}>
|
<Page textAlign='center' mt={40}>
|
||||||
<PageContent>
|
<Page.Content>
|
||||||
<Icon as={MdError} w='16' h='16' />
|
<Icon as={MdError} w='16' h='16' />
|
||||||
<Heading display='block' as='h1' fontSize='6xl'>
|
<Heading display='block' as='h1' fontSize='6xl'>
|
||||||
Page not found
|
Page not found
|
||||||
@ -38,8 +38,8 @@ const NotFoundPage: React.FC<IPageProps> = ({ title }) => {
|
|||||||
<Link as={ReactRouterLink} to='/' color='blue.500'>
|
<Link as={ReactRouterLink} to='/' color='blue.500'>
|
||||||
Return to main page
|
Return to main page
|
||||||
</Link>
|
</Link>
|
||||||
</PageContent>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IPageProps } from '@educt/interfaces';
|
import { IPageProps } from '@educt/interfaces';
|
||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
import { CategoryTableList, CategoryTableRow } from '@educt/components/Tables/CategoryTableList';
|
import { CategoryTableList, CategoryTableRow } from '@educt/components/Tables/CategoryTableList';
|
||||||
import { useFetchCategories } from '@educt/hooks/queries';
|
|
||||||
import LoadingPage from '@educt/components/LoadingPage';
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useFetchCategories } from '@educt/hooks/queries';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Categories page
|
* Categories page
|
||||||
*/
|
*/
|
||||||
@ -14,12 +18,12 @@ const CategoriesPage: React.FC<IPageProps> = () => {
|
|||||||
if (isLoading || categories === null) return <LoadingPage />;
|
if (isLoading || categories === null) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<Page>
|
||||||
<PageHeading heading='Categories' description='Edit categories in the system.' />
|
<Page.Heading heading='Categories' description='Edit categories in the system.' />
|
||||||
<PageContent>
|
<Page.Content>
|
||||||
<CategoryTableList categories={categories} render={CategoryTableRow} />
|
<CategoryTableList categories={categories} render={CategoryTableRow} />
|
||||||
</PageContent>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import type { IPageProps } from '@educt/interfaces';
|
|||||||
* Components
|
* Components
|
||||||
*/
|
*/
|
||||||
import { CourseBackgroundImage, CourseHeader, CourseHeading, CourseInfo, LessonItem, LessonList } from '.';
|
import { CourseBackgroundImage, CourseHeader, CourseHeading, CourseInfo, LessonItem, LessonList } from '.';
|
||||||
import { PageWrapper, PageContent } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
import LoadingPage from '@educt/components/LoadingPage';
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,7 +50,7 @@ const CoursePage: React.FC<IPageProps> = () => {
|
|||||||
const handleEditCourse = () => history.push(`/courses/edit/${course.id}`);
|
const handleEditCourse = () => history.push(`/courses/edit/${course.id}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper maxW='1200px'>
|
<Page maxW='1200px'>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{course.title}</title>
|
<title>{course.title}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
@ -64,7 +64,7 @@ const CoursePage: React.FC<IPageProps> = () => {
|
|||||||
/>
|
/>
|
||||||
</CourseHeader>
|
</CourseHeader>
|
||||||
|
|
||||||
<PageContent mt={{ base: '5', sm: '2' }}>
|
<Page.Content mt={{ base: '5', sm: '2' }}>
|
||||||
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }} gap={'7'}>
|
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }} gap={'7'}>
|
||||||
<GridItem>
|
<GridItem>
|
||||||
<Box>
|
<Box>
|
||||||
@ -158,8 +158,8 @@ const CoursePage: React.FC<IPageProps> = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
</PageContent>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { IPageProps } from '@educt/interfaces';
|
|||||||
/**
|
/**
|
||||||
* Components
|
* Components
|
||||||
*/
|
*/
|
||||||
import { PageWrapper, PageHeading, PageContent } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
import { CreateButton } from '@educt/components/Buttons';
|
import { CreateButton } from '@educt/components/Buttons';
|
||||||
import { Box } from '@chakra-ui/layout';
|
import { Box } from '@chakra-ui/layout';
|
||||||
import LoadingPage from '@educt/components/LoadingPage';
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
@ -38,18 +38,18 @@ const CoursesPage: React.FC<IPageProps> = () => {
|
|||||||
if (me === null) return <LoadingPage />;
|
if (me === null) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<Page>
|
||||||
<PageHeading heading='Courses' description='List of all courses'>
|
<Page.Heading heading='Courses' description='List of all courses'>
|
||||||
<Box mt='5'>
|
<Box mt='5'>
|
||||||
{(me.isAdmin || me.isTeacher) && <CreateButton onClick={handleCreateCourse} ml='auto' display='block' />}
|
{(me.isAdmin || me.isTeacher) && <CreateButton onClick={handleCreateCourse} ml='auto' display='block' />}
|
||||||
</Box>
|
</Box>
|
||||||
</PageHeading>
|
</Page.Heading>
|
||||||
<PageContent mt='10'>
|
<Page.Content mt='10'>
|
||||||
<CoursesTabs mb='7' />
|
<CoursesTabs mb='7' />
|
||||||
<CategoryList render={CategoryItem} />
|
<CategoryList render={CategoryItem} />
|
||||||
<CourseList render={CourseItem} />
|
<CourseList render={CourseItem} />
|
||||||
</PageContent>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { IPageProps } from '@educt/interfaces';
|
import { IPageProps } from '@educt/interfaces';
|
||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
import CreateLessonForm from '@educt/components/Forms/LessonForm/CreateLessonForm';
|
import CreateLessonForm from '@educt/components/Forms/LessonForm/CreateLessonForm';
|
||||||
import { Redirect, useParams } from 'react-router-dom';
|
import { Redirect, useParams } from 'react-router-dom';
|
||||||
import { useFetchCourse } from '@educt/hooks/queries';
|
import { useFetchCourse } from '@educt/hooks/queries';
|
||||||
@ -21,12 +21,12 @@ const CreateLessonPage: React.FC<IPageProps> = () => {
|
|||||||
if (!course || isLoading) return <LoadingPage />;
|
if (!course || isLoading) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<Page>
|
||||||
<PageHeading heading='Lesson Creation' description='Create new lesson' />
|
<Page.Heading heading='Lesson Creation' description='Create new lesson' />
|
||||||
<PageContent>
|
<Page.Content>
|
||||||
<CreateLessonForm />
|
<CreateLessonForm />
|
||||||
</PageContent>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex, Box, Heading, Text } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
@ -11,23 +10,23 @@ import { IPageProps } from '@educt/interfaces';
|
|||||||
*/
|
*/
|
||||||
import PrevPageButton from '@educt/components/PrevPageButton';
|
import PrevPageButton from '@educt/components/PrevPageButton';
|
||||||
import { CreateCourseForm } from '@educt/components/Forms/CourseForm';
|
import { CreateCourseForm } from '@educt/components/Forms/CourseForm';
|
||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create course page
|
* Create course page
|
||||||
*/
|
*/
|
||||||
const CreateCoursePage: React.FC<IPageProps> = () => {
|
const CreateCoursePage: React.FC<IPageProps> = () => {
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<Page>
|
||||||
<PageHeading
|
<Page.Heading
|
||||||
headingPrefix={<PrevPageButton prevPage='/courses' />}
|
headingPrefix={<PrevPageButton prevPage='/courses' />}
|
||||||
heading='Course Creation'
|
heading='Course Creation'
|
||||||
description='Fill in the required fields to create a new course'
|
description='Fill in the required fields to create a new course'
|
||||||
/>
|
/>
|
||||||
<PageContent>
|
<Page.Content>
|
||||||
<CreateCourseForm />
|
<CreateCourseForm />
|
||||||
</PageContent>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import DeleteCourseDialog from '@educt/components/Dialogs/DeleteCourseDialog';
|
|||||||
import PrevPageButton from '@educt/components/PrevPageButton';
|
import PrevPageButton from '@educt/components/PrevPageButton';
|
||||||
import { DeleteButton } from '@educt/components/Buttons';
|
import { DeleteButton } from '@educt/components/Buttons';
|
||||||
import { StatusButton } from './components';
|
import { StatusButton } from './components';
|
||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
import { StudentTableList, StudentTableRow } from './components/StudentTableList';
|
import { StudentTableList, StudentTableRow } from './components/StudentTableList';
|
||||||
import LessonList from './components/LessonList';
|
import LessonList from './components/LessonList';
|
||||||
|
|
||||||
@ -43,13 +43,13 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
|||||||
if (!course || isLoading) return <LoadingPage />;
|
if (!course || isLoading) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<Page>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Editing {course.title}</title>
|
<title>Editing {course.title}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<DeleteCourseDialog course={course} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />
|
<DeleteCourseDialog course={course} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />
|
||||||
<PageHeading
|
<Page.Heading
|
||||||
headingPrefix={<PrevPageButton prevPage='/courses' />}
|
headingPrefix={<PrevPageButton prevPage='/courses' />}
|
||||||
heading='Course editor'
|
heading='Course editor'
|
||||||
description={course.title}
|
description={course.title}
|
||||||
@ -58,8 +58,8 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
|||||||
<StatusButton courseId={course.id} currentStatus={course.status} />
|
<StatusButton courseId={course.id} currentStatus={course.status} />
|
||||||
<DeleteButton onClick={onOpenDeleteDialog} ml='2' />
|
<DeleteButton onClick={onOpenDeleteDialog} ml='2' />
|
||||||
</Flex>
|
</Flex>
|
||||||
</PageHeading>
|
</Page.Heading>
|
||||||
<PageContent>
|
<Page.Content>
|
||||||
<Box>
|
<Box>
|
||||||
<Tabs mt='8'>
|
<Tabs mt='8'>
|
||||||
<TabList>
|
<TabList>
|
||||||
@ -91,8 +91,8 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
|||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
</PageContent>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
import ReactPlayer from 'react-player';
|
import ReactPlayer from 'react-player';
|
||||||
import { MdOutlineCircle, MdCheckCircle, MdOutlineFilePresent, MdSkipNext, MdSkipPrevious } from 'react-icons/md';
|
import { MdOutlineCircle, MdCheckCircle, MdOutlineFilePresent, MdSkipNext, MdSkipPrevious } from 'react-icons/md';
|
||||||
import LoadingPage from '@educt/components/LoadingPage';
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
@ -138,17 +138,17 @@ const LessonPage: React.FC<IPageProps> = () => {
|
|||||||
if (me === null) return <LoadingPage />;
|
if (me === null) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<Page>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{lesson.title}</title>
|
<title>{lesson.title}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<PageHeading heading={lesson.title}>
|
<Page.Heading heading={lesson.title}>
|
||||||
<Flex mt='3' alignItems={'center'} justifyContent={'space-between'}>
|
<Flex mt='3' alignItems={'center'} justifyContent={'space-between'}>
|
||||||
<Text fontSize={'sm'}>~{moment.duration(lesson.duration, 'minutes').humanize()}</Text>
|
<Text fontSize={'sm'}>~{moment.duration(lesson.duration, 'minutes').humanize()}</Text>
|
||||||
{(me.isAdmin || me.isTeacher) && <EditButton onClick={handleEditLesson} />}
|
{(me.isAdmin || me.isTeacher) && <EditButton onClick={handleEditLesson} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</PageHeading>
|
</Page.Heading>
|
||||||
<PageContent>
|
<Page.Content>
|
||||||
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }}>
|
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }}>
|
||||||
<GridItem>
|
<GridItem>
|
||||||
<Box>
|
<Box>
|
||||||
@ -270,8 +270,8 @@ const LessonPage: React.FC<IPageProps> = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
</PageContent>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IPageProps } from '@educt/interfaces';
|
import { IPageProps } from '@educt/interfaces';
|
||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
import { Redirect, useParams } from 'react-router-dom';
|
import { Redirect, useParams } from 'react-router-dom';
|
||||||
import LoadingPage from '@educt/components/LoadingPage';
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
import { useFetchLesson } from '@educt/hooks/queries/lesson/useFetchLesson';
|
|
||||||
import EditLessonForm from '@educt/components/Forms/LessonForm/EditLessonForm';
|
import EditLessonForm from '@educt/components/Forms/LessonForm/EditLessonForm';
|
||||||
|
import { useFetchLesson } from '@educt/hooks/queries/lesson/useFetchLesson';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create Lesson page
|
* Create Lesson page
|
||||||
@ -20,12 +20,12 @@ const EditLessonPage: React.FC<IPageProps> = () => {
|
|||||||
if (!lesson || isLoading) return <LoadingPage />;
|
if (!lesson || isLoading) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<Page>
|
||||||
<PageHeading heading='Lesson Editor' description={lesson.description} />
|
<Page.Heading heading='Lesson Editor' description={lesson.description} />
|
||||||
<PageContent>
|
<Page.Content>
|
||||||
<EditLessonForm lesson={lesson} />
|
<EditLessonForm lesson={lesson} />
|
||||||
</PageContent>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export const CourseCard: React.FC<BoxProps> = props => <Card py='5' px='6' {...p
|
|||||||
export const CourseCardHeading: React.FC<BoxProps> = props => (
|
export const CourseCardHeading: React.FC<BoxProps> = props => (
|
||||||
<Box borderLeftWidth={'5px'} borderColor={'blue.500'} pl='2' {...props} />
|
<Box borderLeftWidth={'5px'} borderColor={'blue.500'} pl='2' {...props} />
|
||||||
);
|
);
|
||||||
export const CourseCardTitle: React.FC<TextProps> = props => <Text fontSize={'2xl'} fontWeight={'bold'} {...props} />;
|
export const CourseCardTitle: React.FC<TextProps> = props => <Text fontSize={'2xl'} fontWeight={'medium'} {...props} />;
|
||||||
export const CourseCardCategory: React.FC = props => (
|
export const CourseCardCategory: React.FC = props => (
|
||||||
<Flex mt='2'>
|
<Flex mt='2'>
|
||||||
<Tag size='sm' borderRadius='full' variant={'solid'}>
|
<Tag size='sm' borderRadius='full' variant={'solid'}>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { Flex, Box, Text, Link, SimpleGrid, Divider } from '@chakra-ui/react';
|
import { Flex, Box, Text, Link, SimpleGrid, Divider } from '@chakra-ui/react';
|
||||||
import { Link as ReactRouterLink } from 'react-router-dom';
|
import { Link as ReactRouterLink } from 'react-router-dom';
|
||||||
import { IPageProps } from '@educt/interfaces';
|
import { IPageProps } from '@educt/interfaces';
|
||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
import { Stat, StatContent, StatIcon, StatLabel, StatNumber } from '@educt/components/Stat';
|
import { Stat, StatContent, StatIcon, StatLabel, StatNumber } from '@educt/components/Stat';
|
||||||
import { MdOutlineCollectionsBookmark, MdOutlineGroup, MdOutlineMessage, MdOutlineVideoLibrary } from 'react-icons/md';
|
import { MdOutlineCollectionsBookmark, MdOutlineGroup, MdOutlineMessage, MdOutlineVideoLibrary } from 'react-icons/md';
|
||||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
@ -21,9 +21,13 @@ import {
|
|||||||
CourseCardList,
|
CourseCardList,
|
||||||
} from './components';
|
} from './components';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { useFetchCourses, useFetchStat } from '@educt/hooks/queries';
|
import { useFetchCourses, useFetchStat } from '@educt/hooks/queries';
|
||||||
import { useSocketEvent } from '@educt/hooks/useSocketEvent';
|
import { useSocketEvent } from '@educt/hooks/useSocketEvent';
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main page
|
* Main page
|
||||||
@ -52,9 +56,9 @@ const MainPage: React.FC<IPageProps> = () => {
|
|||||||
if (me === null || courses === null || stat === null) return <LoadingPage />;
|
if (me === null || courses === null || stat === null) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<Page>
|
||||||
<PageHeading heading={`👋 Hello ${me.first_name},`} description="This what we've got today for you." />
|
<Page.Heading heading={`👋 Hello ${me.first_name},`} description="This what we've got today for you." />
|
||||||
<PageContent>
|
<Page.Content>
|
||||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 4 }} spacing='6'>
|
<SimpleGrid columns={{ base: 1, md: 2, lg: 4 }} spacing='6'>
|
||||||
<Stat>
|
<Stat>
|
||||||
<StatLabel>Online</StatLabel>
|
<StatLabel>Online</StatLabel>
|
||||||
@ -121,8 +125,8 @@ const MainPage: React.FC<IPageProps> = () => {
|
|||||||
</CourseCardList>
|
</CourseCardList>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider />
|
<Divider />
|
||||||
</PageContent>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Heading, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Grid } from '@chakra-ui/react';
|
import { Box, Heading } from '@chakra-ui/react';
|
||||||
|
import { Page } from '@educt/components/PageElements';
|
||||||
|
|
||||||
import { IPageProps } from '@educt/interfaces';
|
import { IPageProps } from '@educt/interfaces';
|
||||||
|
|
||||||
@ -8,9 +9,9 @@ import { IPageProps } from '@educt/interfaces';
|
|||||||
*/
|
*/
|
||||||
const MessagesPage: React.FC<IPageProps> = () => {
|
const MessagesPage: React.FC<IPageProps> = () => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Page>
|
||||||
<Heading as='h1'>Messages page</Heading>
|
<Page.Heading heading='Messages' />
|
||||||
</Box>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,165 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import * as helpers from '@educt/helpers';
|
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
|
||||||
import { MdSave } from 'react-icons/md';
|
|
||||||
import { Text, Input } from '@chakra-ui/react';
|
|
||||||
import { Box, Stack } from '@chakra-ui/layout';
|
|
||||||
import { FormControl, FormHelperText } from '@chakra-ui/form-control';
|
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Types
|
|
||||||
*/
|
|
||||||
import { IUserContacts } from '@educt/interfaces';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hooks
|
|
||||||
*/
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useToast } from '@chakra-ui/toast';
|
|
||||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schema
|
|
||||||
*/
|
|
||||||
import UpdateUserContactsSchema from './UpdateUserContactsForm.validator';
|
|
||||||
import { SaveButton } from '@educt/components/Buttons';
|
|
||||||
|
|
||||||
type UpdateUserContactsInputType = {
|
|
||||||
phone_number: string | null;
|
|
||||||
twitter_id: string | null;
|
|
||||||
telegram_id: string | null;
|
|
||||||
vk_id: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UpdateUserContactsFormPropsType = {
|
|
||||||
contacts: IUserContacts | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UpdateUserContactsForm: React.FC<UpdateUserContactsFormPropsType> = ({ contacts }) => {
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
formState: { errors, dirtyFields, isDirty },
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
} = useForm<UpdateUserContactsInputType>({
|
|
||||||
resolver: yupResolver(UpdateUserContactsSchema),
|
|
||||||
defaultValues: {
|
|
||||||
phone_number: contacts && (contacts.phone_number ?? ''),
|
|
||||||
twitter_id: contacts && (contacts.twitter_id?.replace(/^/, '@') ?? ''),
|
|
||||||
telegram_id: contacts && (contacts.telegram_id?.replace(/^/, '@') ?? ''),
|
|
||||||
vk_id: contacts && (contacts.vk_id ?? ''),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
const { userStore } = useRootStore();
|
|
||||||
const toast = useToast();
|
|
||||||
const handleError = useErrorHandler();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sumbit form handler
|
|
||||||
*/
|
|
||||||
const onSubmit: SubmitHandler<UpdateUserContactsInputType> = async data => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const updatedFields = helpers.getDirtyFields(dirtyFields, data);
|
|
||||||
await userStore.updateCurrentUserContacts(updatedFields);
|
|
||||||
toast({ title: 'Contacts saved!', status: 'info' });
|
|
||||||
reset({
|
|
||||||
phone_number: data.phone_number ?? '',
|
|
||||||
twitter_id: data.twitter_id ?? '',
|
|
||||||
telegram_id: data.telegram_id ?? '',
|
|
||||||
vk_id: data.vk_id ?? '',
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.response) {
|
|
||||||
switch (error.response.status) {
|
|
||||||
case 422:
|
|
||||||
toast({ title: error.response.data.errors[0].message, status: 'error', duration: 2000 });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
handleError(error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handleError(error);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<Box borderRadius='lg' borderWidth='1px' padding='5'>
|
|
||||||
<Stack spacing='5px'>
|
|
||||||
<FormControl id='phone_number'>
|
|
||||||
<FormHelperText color='gray.500'>Phone number with country code</FormHelperText>
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
size='md'
|
|
||||||
variant='flushed'
|
|
||||||
placeholder='+1 (234) 555-1234'
|
|
||||||
isInvalid={errors.phone_number ? true : false}
|
|
||||||
{...register('phone_number')}
|
|
||||||
/>
|
|
||||||
<Text as='small' color='red.500'>
|
|
||||||
{errors.phone_number?.message}
|
|
||||||
</Text>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl id='twitter_id'>
|
|
||||||
<FormHelperText color='gray.500'>Twitter</FormHelperText>
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
size='md'
|
|
||||||
variant='flushed'
|
|
||||||
placeholder='@username'
|
|
||||||
isInvalid={errors.twitter_id ? true : false}
|
|
||||||
{...register('twitter_id')}
|
|
||||||
/>
|
|
||||||
<Text as='small' color='red.500'>
|
|
||||||
{errors.twitter_id?.message}
|
|
||||||
</Text>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl id='telegram'>
|
|
||||||
<FormHelperText color='gray.500'>Telegram</FormHelperText>
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
size='md'
|
|
||||||
variant='flushed'
|
|
||||||
placeholder='@username'
|
|
||||||
isInvalid={errors.telegram_id ? true : false}
|
|
||||||
{...register('telegram_id')}
|
|
||||||
/>
|
|
||||||
<Text as='small' color='red.500'>
|
|
||||||
{errors.telegram_id?.message}
|
|
||||||
</Text>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl id='vk'>
|
|
||||||
<FormHelperText color='gray.500'>VKontakte</FormHelperText>
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
size='md'
|
|
||||||
variant='flushed'
|
|
||||||
placeholder='username'
|
|
||||||
isInvalid={errors.vk_id ? true : false}
|
|
||||||
{...register('vk_id')}
|
|
||||||
/>
|
|
||||||
<Text as='small' color='red.500'>
|
|
||||||
{errors.vk_id?.message}
|
|
||||||
</Text>
|
|
||||||
</FormControl>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
<SaveButton type='submit' mt='4' isLoading={isLoading} rightIcon={<MdSave />} isDisabled={!isDirty} />
|
|
||||||
</form>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default observer(UpdateUserContactsForm);
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import UpdateUserContactsForm from './UpdateUserContactsForm';
|
|
||||||
|
|
||||||
export default UpdateUserContactsForm;
|
|
||||||
@ -1,100 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { FlexProps, Flex, BoxProps, Box, Heading, Text } from '@chakra-ui/layout';
|
|
||||||
import { Avatar } from '@chakra-ui/avatar';
|
|
||||||
import { Button, ButtonProps } from '@chakra-ui/button';
|
|
||||||
import { Skeleton, SkeletonCircle, Spinner } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Types
|
|
||||||
*/
|
|
||||||
import { IUserRole } from '@educt/interfaces';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Components
|
|
||||||
*/
|
|
||||||
import UserBadge from '@educt/components/UserBadge';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hooks
|
|
||||||
*/
|
|
||||||
import { useColorMode } from '@chakra-ui/react';
|
|
||||||
import { useLogout } from '@educt/hooks/queries';
|
|
||||||
|
|
||||||
export const ProfileBaseInfo: React.FC<FlexProps> = props => {
|
|
||||||
return (
|
|
||||||
<Flex flexDir={{ base: 'column', md: 'row' }} textAlign={{ base: 'center', md: 'left' }} {...props}>
|
|
||||||
{props.children}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IProfileAvatarProps extends BoxProps {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
export const ProfileAvatar: React.FC<IProfileAvatarProps> = props => {
|
|
||||||
const { colorMode } = useColorMode();
|
|
||||||
const isDark = colorMode === 'dark';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Avatar border={`4px solid ${isDark ? '#2D3748' : '#E2E8F0'}`} size='2xl' name={props.name} />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IProfileDescriptionProps extends BoxProps {
|
|
||||||
fullname: string;
|
|
||||||
email: string;
|
|
||||||
roles: IUserRole[];
|
|
||||||
}
|
|
||||||
export const ProfileDescription: React.FC<IProfileDescriptionProps> = props => {
|
|
||||||
const { fullname, email, roles, ...boxProps } = props;
|
|
||||||
return (
|
|
||||||
<Box w='full' ml={{ md: '50' }} mt={{ base: '2', md: '3' }} {...boxProps}>
|
|
||||||
<Flex alignItems='center' justifyContent={{ base: 'center', md: 'space-between' }}>
|
|
||||||
<Box>
|
|
||||||
<Heading lineHeight='1.5rem' fontWeight='bold' fontSize='24px'>
|
|
||||||
{fullname}
|
|
||||||
<br />
|
|
||||||
<Text as='span' fontSize='sm' fontWeight='normal'>
|
|
||||||
{email}
|
|
||||||
</Text>
|
|
||||||
</Heading>
|
|
||||||
<Box>
|
|
||||||
<UserBadge mt='2' roles={roles} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProfileSignOutButton: React.FC<ButtonProps> = () => {
|
|
||||||
const { logout } = useLogout();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button onClick={logout} size='sm' w={{ base: 'full', sm: '150px' }}>
|
|
||||||
Sign Out
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProfileLoading: React.FC<BoxProps> = props => {
|
|
||||||
return (
|
|
||||||
<Box maxW='900px' {...props}>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,37 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { observer } from 'mobx-react';
|
||||||
Avatar,
|
import { Stack } from '@chakra-ui/layout';
|
||||||
Tabs,
|
|
||||||
TabList,
|
|
||||||
Tab,
|
|
||||||
TabPanels,
|
|
||||||
TabPanel,
|
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
FormHelperText,
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputRightElement,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { Box, Stack } from '@chakra-ui/layout';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
*/
|
*/
|
||||||
import { IPageProps } from '@educt/interfaces';
|
import type { IPageProps } from '@educt/interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Components
|
* Components
|
||||||
*/
|
*/
|
||||||
import { PageHeading, PageContent, PageWrapper, PageFooter } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
import { ProfileAvatar, ProfileBaseInfo, ProfileDescription, ProfileLoading, ProfileSignOutButton } from './components';
|
import { UserProfile } from '@educt/components/UserProfile';
|
||||||
import { CourseList } from './components/CourseList';
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
import UpdateUserContactsForm from '@educt/pages/profile/components/UpdateUserContactsForm';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks
|
* Hooks
|
||||||
*/
|
*/
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,87 +26,84 @@ const ProfilePage: React.FC<IPageProps> = () => {
|
|||||||
const {
|
const {
|
||||||
userStore: { me },
|
userStore: { me },
|
||||||
} = useRootStore();
|
} = useRootStore();
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
if (me === null) return <ProfileLoading />;
|
if (me === null) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper maxW='900px'>
|
<Page maxW={'900px'}>
|
||||||
<PageHeading heading='My account' description='Update you contacts information or password here.' />
|
<Page.Heading heading='My account' description='Update your profile information or password here.' />
|
||||||
<ProfileBaseInfo mt='20'>
|
<Page.Content>
|
||||||
<ProfileAvatar name={me.fullname} />
|
<UserProfile mt='14'>
|
||||||
<ProfileDescription fullname={me.fullname} email={me.email} roles={me.roles} />
|
<UserProfile.Base>
|
||||||
</ProfileBaseInfo>
|
<UserProfile.Avatar name={me.fullname} />
|
||||||
<PageContent>
|
<UserProfile.Info>
|
||||||
<Tabs mt='7'>
|
<UserProfile.Heading fullname={me.fullname} roles={me.roles} />
|
||||||
<TabList>
|
<UserProfile.Details registered={me.created_at} lastLogin={me.last_login} />
|
||||||
<Tab>Account information</Tab>
|
<UserProfile.About about={me.about} />
|
||||||
<Tab>My contacts</Tab>
|
</UserProfile.Info>
|
||||||
{me.isStudent && <Tab>Available courses</Tab>}
|
</UserProfile.Base>
|
||||||
</TabList>
|
|
||||||
<TabPanels mt='2'>
|
|
||||||
<TabPanel>
|
|
||||||
<form>
|
|
||||||
<Box borderRadius='md' borderWidth='1px' padding='5'>
|
|
||||||
<Stack spacing='5px'>
|
|
||||||
<FormControl isDisabled>
|
|
||||||
<FormHelperText color='gray.500'>First name</FormHelperText>
|
|
||||||
<Input type='text' value={me.first_name} size='md' variant='flushed' />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl isDisabled>
|
<UserProfile.Settings>
|
||||||
<FormHelperText color='gray.500'>Last name</FormHelperText>
|
{/* Personal Info */}
|
||||||
<Input type='text' value={me.last_name} size='md' variant='flushed' />
|
<UserProfile.Settings.Entry>
|
||||||
</FormControl>
|
<UserProfile.Settings.Title title={'Personal Info'} />
|
||||||
|
<UserProfile.Settings.EntryContent>
|
||||||
|
<Stack spacing={'6'}>
|
||||||
|
<UserProfile.Settings.FullnameInput defaultValue={me.fullname} />
|
||||||
|
<UserProfile.Settings.EmailInput defaultValue={me.email} />
|
||||||
|
<UserProfile.Settings.AboutInput />
|
||||||
|
</Stack>
|
||||||
|
</UserProfile.Settings.EntryContent>
|
||||||
|
</UserProfile.Settings.Entry>
|
||||||
|
|
||||||
<FormControl isDisabled>
|
{/* Contacts */}
|
||||||
<FormHelperText color='gray.500'>Email</FormHelperText>
|
<UserProfile.Settings.Entry>
|
||||||
<InputGroup>
|
<UserProfile.Settings.Title title={'Contacts'} />
|
||||||
<Input type='text' value={me.email} size='md' variant='flushed' />
|
<UserProfile.Settings.EntryContent>
|
||||||
<InputRightElement width='4rem'>
|
<Stack spacing={'6'}>
|
||||||
<Button size='sm' h='1.90rem' w='full' onClick={() => history.push('/profile/change-email')}>
|
<UserProfile.Settings.PhoneInput />
|
||||||
Edit
|
<UserProfile.Settings.TwitterInput />
|
||||||
</Button>
|
<UserProfile.Settings.TelegramInput />
|
||||||
</InputRightElement>
|
</Stack>
|
||||||
</InputGroup>
|
</UserProfile.Settings.EntryContent>
|
||||||
</FormControl>
|
</UserProfile.Settings.Entry>
|
||||||
|
|
||||||
<FormControl isDisabled>
|
{/* Security */}
|
||||||
<FormHelperText color='gray.500'>Password</FormHelperText>
|
<UserProfile.Settings.Entry>
|
||||||
<InputGroup>
|
<UserProfile.Settings.Title title='Security' />
|
||||||
<Input type='text' value='**********' size='md' variant='flushed' />
|
<UserProfile.Settings.EntryContent>
|
||||||
<InputRightElement width='5rem'>
|
<UserProfile.Settings.PasswordInput defaultValue={'**********'} />
|
||||||
<Button
|
</UserProfile.Settings.EntryContent>
|
||||||
size='sm'
|
</UserProfile.Settings.Entry>
|
||||||
h='1.90rem'
|
|
||||||
w='full'
|
{/* Language */}
|
||||||
onClick={() => history.push('/profile/change-password')}
|
<UserProfile.Settings.Entry>
|
||||||
>
|
<UserProfile.Settings.Title title='Language' />
|
||||||
Change
|
<UserProfile.Settings.EntryContent>
|
||||||
</Button>
|
<UserProfile.Settings.LanguageSelect />
|
||||||
</InputRightElement>
|
</UserProfile.Settings.EntryContent>
|
||||||
</InputGroup>
|
</UserProfile.Settings.Entry>
|
||||||
</FormControl>
|
|
||||||
</Stack>
|
{/* Night Theme */}
|
||||||
</Box>
|
<UserProfile.Settings.Entry>
|
||||||
</form>
|
<UserProfile.Settings.Title title='Night Theme' />
|
||||||
</TabPanel>
|
<UserProfile.Settings.EntryContent>
|
||||||
<TabPanel>
|
<UserProfile.Settings.ThemeSwitcher />
|
||||||
<UpdateUserContactsForm contacts={me.contacts} />
|
</UserProfile.Settings.EntryContent>
|
||||||
</TabPanel>
|
</UserProfile.Settings.Entry>
|
||||||
{me.isStudent && (
|
|
||||||
<TabPanel>
|
{/* Save Button */}
|
||||||
<CourseList courses={me.courses} />
|
<UserProfile.Settings.Entry>
|
||||||
</TabPanel>
|
<UserProfile.Settings.Title />
|
||||||
)}
|
<UserProfile.Settings.EntryContent>
|
||||||
</TabPanels>
|
<UserProfile.Settings.SaveSettingsButton />
|
||||||
</Tabs>
|
</UserProfile.Settings.EntryContent>
|
||||||
</PageContent>
|
</UserProfile.Settings.Entry>
|
||||||
<PageFooter textAlign='center'>
|
</UserProfile.Settings>
|
||||||
<ProfileSignOutButton />
|
</UserProfile>
|
||||||
</PageFooter>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProfilePage;
|
export default observer(ProfilePage);
|
||||||
|
|||||||
52
src/pages/user/user.tsx
Normal file
52
src/pages/user/user.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Redirect } from 'react-router-dom';
|
||||||
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
|
import { Page } from '@educt/components/PageElements';
|
||||||
|
import { UserProfile } from '@educt/components/UserProfile';
|
||||||
|
import type { IPageProps } from '@educt/interfaces';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useFetchUser } from '@educt/hooks/queries/user/useFetchUser';
|
||||||
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
|
|
||||||
|
const UserPage: React.FC<IPageProps> = () => {
|
||||||
|
const params = useParams<{ id: string }>();
|
||||||
|
const {
|
||||||
|
userStore: { me },
|
||||||
|
} = useRootStore();
|
||||||
|
const { data: user, error, isLoading } = useFetchUser(params.id);
|
||||||
|
|
||||||
|
if (error?.response?.status === 404) return <Redirect to='/404' />;
|
||||||
|
|
||||||
|
if (user === null || me === null || isLoading) return <LoadingPage />;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to profile page
|
||||||
|
*/
|
||||||
|
if (me.id === params.id) return <Redirect to='/profile' />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<Page.Content maxW={'900px'}>
|
||||||
|
<UserProfile>
|
||||||
|
<UserProfile.Base>
|
||||||
|
<UserProfile.Avatar name={user.fullname} isOnline>
|
||||||
|
<UserProfile.ChatButton userid={user.id} />
|
||||||
|
</UserProfile.Avatar>
|
||||||
|
<UserProfile.Info>
|
||||||
|
<UserProfile.Heading fullname={user.fullname} roles={user.roles} />
|
||||||
|
<UserProfile.Details registered={user.created_at} lastLogin={user.last_login} />
|
||||||
|
<UserProfile.About about={user.about} />
|
||||||
|
</UserProfile.Info>
|
||||||
|
</UserProfile.Base>
|
||||||
|
</UserProfile>
|
||||||
|
</Page.Content>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(UserPage);
|
||||||
@ -8,7 +8,7 @@ import type { IPageProps } from '@educt/interfaces';
|
|||||||
/**
|
/**
|
||||||
* Components
|
* Components
|
||||||
*/
|
*/
|
||||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
import { UserList, UserItem } from './components/UserList';
|
import { UserList, UserItem } from './components/UserList';
|
||||||
import LoadingPage from '@educt/components/LoadingPage';
|
import LoadingPage from '@educt/components/LoadingPage';
|
||||||
|
|
||||||
@ -35,12 +35,12 @@ const UsersPage: React.FC<IPageProps> = () => {
|
|||||||
return (
|
return (
|
||||||
// TODO: delete page context for better performance
|
// TODO: delete page context for better performance
|
||||||
<UsersPageContextProvider>
|
<UsersPageContextProvider>
|
||||||
<PageWrapper>
|
<Page>
|
||||||
<PageHeading heading='User management' description='You can add or delete users on this page.' />
|
<Page.Heading heading='User management' description='You can add or delete users on this page.' />
|
||||||
<PageContent mt='5'>
|
<Page.Content mt='5'>
|
||||||
<UserList render={UserItem} limit={6} />
|
<UserList render={UserItem} limit={6} />
|
||||||
</PageContent>
|
</Page.Content>
|
||||||
</PageWrapper>
|
</Page>
|
||||||
</UsersPageContextProvider>
|
</UsersPageContextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import CoursePage from '@educt/pages/course/course';
|
|||||||
import EditCoursePage from '@educt/pages/courses/edit/edit';
|
import EditCoursePage from '@educt/pages/courses/edit/edit';
|
||||||
import ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
|
import ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
|
||||||
import ChangeEmailPage from '@educt/pages/profile/change-email/change-email';
|
import ChangeEmailPage from '@educt/pages/profile/change-email/change-email';
|
||||||
|
import UserPage from './pages/user/user';
|
||||||
import { UserRoleEnum } from './enums';
|
import { UserRoleEnum } from './enums';
|
||||||
const CoursesPage = lazy(() => import('@educt/pages/courses/courses'));
|
const CoursesPage = lazy(() => import('@educt/pages/courses/courses'));
|
||||||
const LessonPage = lazy(() => import('@educt/pages/lesson/lesson'));
|
const LessonPage = lazy(() => import('@educt/pages/lesson/lesson'));
|
||||||
@ -126,6 +127,13 @@ export const routes: IRouteOption[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
|
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/user/:id',
|
||||||
|
title: 'User',
|
||||||
|
component: UserPage,
|
||||||
|
isPrivate: true,
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/auth',
|
path: '/auth',
|
||||||
title: 'Auth',
|
title: 'Auth',
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { AxiosInstance } from 'axios';
|
import { AxiosInstance } from 'axios';
|
||||||
import { ApiServiceInstance } from '.';
|
import { ApiServiceInstance } from '.';
|
||||||
import * as helpers from '@educt/helpers';
|
import * as helpers from '@educt/helpers';
|
||||||
import { IApiRespose, IMe, IUser, IUserContacts } from '@educt/interfaces';
|
import { IApiRespose, IMe, IUser, IUserContacts, IUserInfo } from '@educt/interfaces';
|
||||||
import {
|
import {
|
||||||
CreateUserParamsType,
|
CreateUserParamsType,
|
||||||
FetchUsersParamsType,
|
FetchUsersParamsType,
|
||||||
UpdateUserParamsType,
|
UpdateUserParamsType,
|
||||||
UpdateUserContactsParamsType,
|
UpdateUserContactsParamsType,
|
||||||
|
UpdateUserInfoParamsType,
|
||||||
} from '@educt/types';
|
} from '@educt/types';
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
@ -45,6 +46,17 @@ class UserService {
|
|||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch user by id
|
||||||
|
*
|
||||||
|
* @param id User id
|
||||||
|
* @returns Users
|
||||||
|
*/
|
||||||
|
public async fetchUserById(id: string): Promise<IApiRespose<IUser>> {
|
||||||
|
const result = await this.api.get(`/v1/users/${id}`);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new user
|
* Create new user
|
||||||
*
|
*
|
||||||
@ -97,6 +109,17 @@ class UserService {
|
|||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user info
|
||||||
|
*
|
||||||
|
* @param data Fields to update
|
||||||
|
* @returns Updated fields
|
||||||
|
*/
|
||||||
|
public async updateInfo(data: UpdateUserInfoParamsType): Promise<IApiRespose<Partial<IUserInfo>>> {
|
||||||
|
const result = await this.api.patch('v1/me/info', data);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update password of authorized user
|
* Update password of authorized user
|
||||||
*
|
*
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
CreateUserParamsType,
|
CreateUserParamsType,
|
||||||
FetchUsersParamsType,
|
FetchUsersParamsType,
|
||||||
UpdateUserContactsParamsType,
|
UpdateUserContactsParamsType,
|
||||||
|
UpdateUserInfoParamsType,
|
||||||
UpdateUserParamsType,
|
UpdateUserParamsType,
|
||||||
} from '@educt/types';
|
} from '@educt/types';
|
||||||
import { IUser, IMe, IPaginationMeta } from '@educt/interfaces';
|
import { IUser, IMe, IPaginationMeta } from '@educt/interfaces';
|
||||||
@ -62,10 +63,25 @@ export default class UserStore {
|
|||||||
public async loadCurrentUserData() {
|
public async loadCurrentUserData() {
|
||||||
const result = await UserServiceInstance.fetchMe();
|
const result = await UserServiceInstance.fetchMe();
|
||||||
const {
|
const {
|
||||||
data: { id, first_name, last_name, fullname, email, roles, likes, contacts, courses, created_at, updated_at },
|
data: {
|
||||||
|
id,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
fullname,
|
||||||
|
about,
|
||||||
|
last_login,
|
||||||
|
email,
|
||||||
|
roles,
|
||||||
|
likes,
|
||||||
|
contacts,
|
||||||
|
courses,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
},
|
||||||
} = result;
|
} = result;
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
// TODO Fix this
|
||||||
this.me = {
|
this.me = {
|
||||||
id,
|
id,
|
||||||
first_name,
|
first_name,
|
||||||
@ -73,6 +89,8 @@ export default class UserStore {
|
|||||||
email,
|
email,
|
||||||
roles,
|
roles,
|
||||||
fullname,
|
fullname,
|
||||||
|
about,
|
||||||
|
last_login,
|
||||||
likes,
|
likes,
|
||||||
contacts,
|
contacts,
|
||||||
courses,
|
courses,
|
||||||
|
|||||||
5
src/types/index.d.ts
vendored
5
src/types/index.d.ts
vendored
@ -134,6 +134,7 @@ export type CreateUserParamsType = {
|
|||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
export type UpdateUserParamsType = { first_name?: string; last_name?: string; email?: string };
|
export type UpdateUserParamsType = { first_name?: string; last_name?: string; email?: string };
|
||||||
|
|
||||||
export type UpdateUserContactsParamsType = {
|
export type UpdateUserContactsParamsType = {
|
||||||
phone_number?: string | null | undefined;
|
phone_number?: string | null | undefined;
|
||||||
twitter_id?: string | null | undefined;
|
twitter_id?: string | null | undefined;
|
||||||
@ -141,6 +142,10 @@ export type UpdateUserContactsParamsType = {
|
|||||||
vk_id?: string | null | undefined;
|
vk_id?: string | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateUserInfoParamsType = {
|
||||||
|
about?: string | null | undefined;
|
||||||
|
} & UpdateUserContactsParamsType;
|
||||||
|
|
||||||
export type CreateCourseParamsType = {
|
export type CreateCourseParamsType = {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user