mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +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 { routes } from './routes.config';
|
||||
|
||||
|
||||
@ -53,7 +53,7 @@ const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({
|
||||
icon={MdGroup}
|
||||
heading='Add new students'
|
||||
description='Attach new students to current course.'
|
||||
confirmText='Add students'
|
||||
confirmText='Add'
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onProceed={handleAttachStudents}
|
||||
|
||||
@ -1,17 +1,23 @@
|
||||
import React from '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;
|
||||
heading: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const PageWrapper: React.FC<BoxProps> = props => {
|
||||
return <Box {...props}>{props.children}</Box>;
|
||||
};
|
||||
|
||||
export const PageHeading: React.FC<IPageHeadingProps> = props => {
|
||||
Page.Heading = props => {
|
||||
const { headingPrefix, heading, description, ...boxProps } = props;
|
||||
return (
|
||||
<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}>
|
||||
{props.children}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const PageFooter: React.FC<BoxProps> = props => (
|
||||
Page.Footer = props => (
|
||||
<Box mt='4' {...props}>
|
||||
<Divider m='20px 0' />
|
||||
{props.children}
|
||||
</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';
|
||||
|
||||
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
|
||||
.string()
|
||||
.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')
|
||||
.transform(val => (!val ? null : val))
|
||||
.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 { useUpdateUser } from './user/useUpdateUser';
|
||||
import { useUpdateUserEmail } from './user/useUpdateUserEmail';
|
||||
import { useUpdateUserInfo } from './user/useUpdateUserInfo';
|
||||
|
||||
/**
|
||||
* Category
|
||||
@ -59,6 +60,7 @@ export {
|
||||
useCreateUser,
|
||||
useDeleteUser,
|
||||
useUpdateUser,
|
||||
useUpdateUserInfo,
|
||||
useUpdateUserEmail,
|
||||
useCreateCategory,
|
||||
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;
|
||||
last_name: string;
|
||||
fullname: string;
|
||||
about: string | null;
|
||||
last_login: string | null;
|
||||
email: string;
|
||||
roles: IUserRole[];
|
||||
contacts: IUserContacts | null;
|
||||
@ -145,6 +147,10 @@ export interface IUserContacts {
|
||||
vk_id: string | null;
|
||||
}
|
||||
|
||||
export interface IUserInfo extends IUserContacts {
|
||||
about: string | null;
|
||||
}
|
||||
|
||||
export interface IUserRole {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@ -8,7 +8,7 @@ import { MdError } from 'react-icons/md';
|
||||
* Types
|
||||
*/
|
||||
import { IPageProps } from '@educt/interfaces';
|
||||
import { PageContent, PageWrapper } from '@educt/components/PageElements';
|
||||
import { Page } from '@educt/components/PageElements';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
@ -28,8 +28,8 @@ const NotFoundPage: React.FC<IPageProps> = ({ title }) => {
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
</Helmet>
|
||||
<PageWrapper textAlign='center' mt={40}>
|
||||
<PageContent>
|
||||
<Page textAlign='center' mt={40}>
|
||||
<Page.Content>
|
||||
<Icon as={MdError} w='16' h='16' />
|
||||
<Heading display='block' as='h1' fontSize='6xl'>
|
||||
Page not found
|
||||
@ -38,8 +38,8 @@ const NotFoundPage: React.FC<IPageProps> = ({ title }) => {
|
||||
<Link as={ReactRouterLink} to='/' color='blue.500'>
|
||||
Return to main page
|
||||
</Link>
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import React from 'react';
|
||||
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 { useFetchCategories } from '@educt/hooks/queries';
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useFetchCategories } from '@educt/hooks/queries';
|
||||
|
||||
/**
|
||||
* Categories page
|
||||
*/
|
||||
@ -14,12 +18,12 @@ const CategoriesPage: React.FC<IPageProps> = () => {
|
||||
if (isLoading || categories === null) return <LoadingPage />;
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageHeading heading='Categories' description='Edit categories in the system.' />
|
||||
<PageContent>
|
||||
<Page>
|
||||
<Page.Heading heading='Categories' description='Edit categories in the system.' />
|
||||
<Page.Content>
|
||||
<CategoryTableList categories={categories} render={CategoryTableRow} />
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ import type { IPageProps } from '@educt/interfaces';
|
||||
* Components
|
||||
*/
|
||||
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';
|
||||
|
||||
/**
|
||||
@ -50,7 +50,7 @@ const CoursePage: React.FC<IPageProps> = () => {
|
||||
const handleEditCourse = () => history.push(`/courses/edit/${course.id}`);
|
||||
|
||||
return (
|
||||
<PageWrapper maxW='1200px'>
|
||||
<Page maxW='1200px'>
|
||||
<Helmet>
|
||||
<title>{course.title}</title>
|
||||
</Helmet>
|
||||
@ -64,7 +64,7 @@ const CoursePage: React.FC<IPageProps> = () => {
|
||||
/>
|
||||
</CourseHeader>
|
||||
|
||||
<PageContent mt={{ base: '5', sm: '2' }}>
|
||||
<Page.Content mt={{ base: '5', sm: '2' }}>
|
||||
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }} gap={'7'}>
|
||||
<GridItem>
|
||||
<Box>
|
||||
@ -158,8 +158,8 @@ const CoursePage: React.FC<IPageProps> = () => {
|
||||
</Box>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import { IPageProps } from '@educt/interfaces';
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
import { PageWrapper, PageHeading, PageContent } from '@educt/components/PageElements';
|
||||
import { Page } from '@educt/components/PageElements';
|
||||
import { CreateButton } from '@educt/components/Buttons';
|
||||
import { Box } from '@chakra-ui/layout';
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
@ -38,18 +38,18 @@ const CoursesPage: React.FC<IPageProps> = () => {
|
||||
if (me === null) return <LoadingPage />;
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageHeading heading='Courses' description='List of all courses'>
|
||||
<Page>
|
||||
<Page.Heading heading='Courses' description='List of all courses'>
|
||||
<Box mt='5'>
|
||||
{(me.isAdmin || me.isTeacher) && <CreateButton onClick={handleCreateCourse} ml='auto' display='block' />}
|
||||
</Box>
|
||||
</PageHeading>
|
||||
<PageContent mt='10'>
|
||||
</Page.Heading>
|
||||
<Page.Content mt='10'>
|
||||
<CoursesTabs mb='7' />
|
||||
<CategoryList render={CategoryItem} />
|
||||
<CourseList render={CourseItem} />
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
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 { Redirect, useParams } from 'react-router-dom';
|
||||
import { useFetchCourse } from '@educt/hooks/queries';
|
||||
@ -21,12 +21,12 @@ const CreateLessonPage: React.FC<IPageProps> = () => {
|
||||
if (!course || isLoading) return <LoadingPage />;
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageHeading heading='Lesson Creation' description='Create new lesson' />
|
||||
<PageContent>
|
||||
<Page>
|
||||
<Page.Heading heading='Lesson Creation' description='Create new lesson' />
|
||||
<Page.Content>
|
||||
<CreateLessonForm />
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Flex, Box, Heading, Text } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* Types
|
||||
@ -11,23 +10,23 @@ import { IPageProps } from '@educt/interfaces';
|
||||
*/
|
||||
import PrevPageButton from '@educt/components/PrevPageButton';
|
||||
import { CreateCourseForm } from '@educt/components/Forms/CourseForm';
|
||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
||||
import { Page } from '@educt/components/PageElements';
|
||||
|
||||
/**
|
||||
* Create course page
|
||||
*/
|
||||
const CreateCoursePage: React.FC<IPageProps> = () => {
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageHeading
|
||||
<Page>
|
||||
<Page.Heading
|
||||
headingPrefix={<PrevPageButton prevPage='/courses' />}
|
||||
heading='Course Creation'
|
||||
description='Fill in the required fields to create a new course'
|
||||
/>
|
||||
<PageContent>
|
||||
<Page.Content>
|
||||
<CreateCourseForm />
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ import DeleteCourseDialog from '@educt/components/Dialogs/DeleteCourseDialog';
|
||||
import PrevPageButton from '@educt/components/PrevPageButton';
|
||||
import { DeleteButton } from '@educt/components/Buttons';
|
||||
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 LessonList from './components/LessonList';
|
||||
|
||||
@ -43,13 +43,13 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
||||
if (!course || isLoading) return <LoadingPage />;
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Page>
|
||||
<Helmet>
|
||||
<title>Editing {course.title}</title>
|
||||
</Helmet>
|
||||
|
||||
<DeleteCourseDialog course={course} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />
|
||||
<PageHeading
|
||||
<Page.Heading
|
||||
headingPrefix={<PrevPageButton prevPage='/courses' />}
|
||||
heading='Course editor'
|
||||
description={course.title}
|
||||
@ -58,8 +58,8 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
||||
<StatusButton courseId={course.id} currentStatus={course.status} />
|
||||
<DeleteButton onClick={onOpenDeleteDialog} ml='2' />
|
||||
</Flex>
|
||||
</PageHeading>
|
||||
<PageContent>
|
||||
</Page.Heading>
|
||||
<Page.Content>
|
||||
<Box>
|
||||
<Tabs mt='8'>
|
||||
<TabList>
|
||||
@ -91,8 +91,8 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
import ReactPlayer from 'react-player';
|
||||
import { MdOutlineCircle, MdCheckCircle, MdOutlineFilePresent, MdSkipNext, MdSkipPrevious } from 'react-icons/md';
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
||||
import { Page } from '@educt/components/PageElements';
|
||||
|
||||
/**
|
||||
* Types
|
||||
@ -138,17 +138,17 @@ const LessonPage: React.FC<IPageProps> = () => {
|
||||
if (me === null) return <LoadingPage />;
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Page>
|
||||
<Helmet>
|
||||
<title>{lesson.title}</title>
|
||||
</Helmet>
|
||||
<PageHeading heading={lesson.title}>
|
||||
<Page.Heading heading={lesson.title}>
|
||||
<Flex mt='3' alignItems={'center'} justifyContent={'space-between'}>
|
||||
<Text fontSize={'sm'}>~{moment.duration(lesson.duration, 'minutes').humanize()}</Text>
|
||||
{(me.isAdmin || me.isTeacher) && <EditButton onClick={handleEditLesson} />}
|
||||
</Flex>
|
||||
</PageHeading>
|
||||
<PageContent>
|
||||
</Page.Heading>
|
||||
<Page.Content>
|
||||
<Grid templateColumns={{ base: '1fr', xl: '3fr 1fr' }}>
|
||||
<GridItem>
|
||||
<Box>
|
||||
@ -270,8 +270,8 @@ const LessonPage: React.FC<IPageProps> = () => {
|
||||
</Box>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
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 LoadingPage from '@educt/components/LoadingPage';
|
||||
import { useFetchLesson } from '@educt/hooks/queries/lesson/useFetchLesson';
|
||||
import EditLessonForm from '@educt/components/Forms/LessonForm/EditLessonForm';
|
||||
import { useFetchLesson } from '@educt/hooks/queries/lesson/useFetchLesson';
|
||||
|
||||
/**
|
||||
* Create Lesson page
|
||||
@ -20,12 +20,12 @@ const EditLessonPage: React.FC<IPageProps> = () => {
|
||||
if (!lesson || isLoading) return <LoadingPage />;
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageHeading heading='Lesson Editor' description={lesson.description} />
|
||||
<PageContent>
|
||||
<Page>
|
||||
<Page.Heading heading='Lesson Editor' description={lesson.description} />
|
||||
<Page.Content>
|
||||
<EditLessonForm lesson={lesson} />
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</Page.Content>
|
||||
</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 => (
|
||||
<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 => (
|
||||
<Flex mt='2'>
|
||||
<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 { Link as ReactRouterLink } from 'react-router-dom';
|
||||
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 { MdOutlineCollectionsBookmark, MdOutlineGroup, MdOutlineMessage, MdOutlineVideoLibrary } from 'react-icons/md';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
@ -21,9 +21,13 @@ import {
|
||||
CourseCardList,
|
||||
} from './components';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useFetchCourses, useFetchStat } from '@educt/hooks/queries';
|
||||
import { useSocketEvent } from '@educt/hooks/useSocketEvent';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Main page
|
||||
@ -52,9 +56,9 @@ const MainPage: React.FC<IPageProps> = () => {
|
||||
if (me === null || courses === null || stat === null) return <LoadingPage />;
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageHeading heading={`👋 Hello ${me.first_name},`} description="This what we've got today for you." />
|
||||
<PageContent>
|
||||
<Page>
|
||||
<Page.Heading heading={`👋 Hello ${me.first_name},`} description="This what we've got today for you." />
|
||||
<Page.Content>
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 4 }} spacing='6'>
|
||||
<Stat>
|
||||
<StatLabel>Online</StatLabel>
|
||||
@ -121,8 +125,8 @@ const MainPage: React.FC<IPageProps> = () => {
|
||||
</CourseCardList>
|
||||
</Box>
|
||||
<Divider />
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
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';
|
||||
|
||||
@ -8,9 +9,9 @@ import { IPageProps } from '@educt/interfaces';
|
||||
*/
|
||||
const MessagesPage: React.FC<IPageProps> = () => {
|
||||
return (
|
||||
<Box>
|
||||
<Heading as='h1'>Messages page</Heading>
|
||||
</Box>
|
||||
<Page>
|
||||
<Page.Heading heading='Messages' />
|
||||
</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 {
|
||||
Avatar,
|
||||
Tabs,
|
||||
TabList,
|
||||
Tab,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
Button,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
} from '@chakra-ui/react';
|
||||
import { Box, Stack } from '@chakra-ui/layout';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Stack } from '@chakra-ui/layout';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import { IPageProps } from '@educt/interfaces';
|
||||
import type { IPageProps } from '@educt/interfaces';
|
||||
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
import { PageHeading, PageContent, PageWrapper, PageFooter } from '@educt/components/PageElements';
|
||||
import { ProfileAvatar, ProfileBaseInfo, ProfileDescription, ProfileLoading, ProfileSignOutButton } from './components';
|
||||
import { CourseList } from './components/CourseList';
|
||||
import UpdateUserContactsForm from '@educt/pages/profile/components/UpdateUserContactsForm';
|
||||
import { Page } from '@educt/components/PageElements';
|
||||
import { UserProfile } from '@educt/components/UserProfile';
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
|
||||
/**
|
||||
@ -41,87 +26,84 @@ const ProfilePage: React.FC<IPageProps> = () => {
|
||||
const {
|
||||
userStore: { me },
|
||||
} = useRootStore();
|
||||
const history = useHistory();
|
||||
|
||||
if (me === null) return <ProfileLoading />;
|
||||
if (me === null) return <LoadingPage />;
|
||||
|
||||
return (
|
||||
<PageWrapper maxW='900px'>
|
||||
<PageHeading heading='My account' description='Update you contacts information or password here.' />
|
||||
<ProfileBaseInfo mt='20'>
|
||||
<ProfileAvatar name={me.fullname} />
|
||||
<ProfileDescription fullname={me.fullname} email={me.email} roles={me.roles} />
|
||||
</ProfileBaseInfo>
|
||||
<PageContent>
|
||||
<Tabs mt='7'>
|
||||
<TabList>
|
||||
<Tab>Account information</Tab>
|
||||
<Tab>My contacts</Tab>
|
||||
{me.isStudent && <Tab>Available courses</Tab>}
|
||||
</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>
|
||||
<Page maxW={'900px'}>
|
||||
<Page.Heading heading='My account' description='Update your profile information or password here.' />
|
||||
<Page.Content>
|
||||
<UserProfile mt='14'>
|
||||
<UserProfile.Base>
|
||||
<UserProfile.Avatar name={me.fullname} />
|
||||
<UserProfile.Info>
|
||||
<UserProfile.Heading fullname={me.fullname} roles={me.roles} />
|
||||
<UserProfile.Details registered={me.created_at} lastLogin={me.last_login} />
|
||||
<UserProfile.About about={me.about} />
|
||||
</UserProfile.Info>
|
||||
</UserProfile.Base>
|
||||
|
||||
<FormControl isDisabled>
|
||||
<FormHelperText color='gray.500'>Last name</FormHelperText>
|
||||
<Input type='text' value={me.last_name} size='md' variant='flushed' />
|
||||
</FormControl>
|
||||
|
||||
<FormControl isDisabled>
|
||||
<FormHelperText color='gray.500'>Email</FormHelperText>
|
||||
<InputGroup>
|
||||
<Input type='text' value={me.email} size='md' variant='flushed' />
|
||||
<InputRightElement width='4rem'>
|
||||
<Button size='sm' h='1.90rem' w='full' onClick={() => history.push('/profile/change-email')}>
|
||||
Edit
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
|
||||
<FormControl isDisabled>
|
||||
<FormHelperText color='gray.500'>Password</FormHelperText>
|
||||
<InputGroup>
|
||||
<Input type='text' value='**********' size='md' variant='flushed' />
|
||||
<InputRightElement width='5rem'>
|
||||
<Button
|
||||
size='sm'
|
||||
h='1.90rem'
|
||||
w='full'
|
||||
onClick={() => history.push('/profile/change-password')}
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
<UserProfile.Settings>
|
||||
{/* Personal Info */}
|
||||
<UserProfile.Settings.Entry>
|
||||
<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>
|
||||
</Box>
|
||||
</form>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<UpdateUserContactsForm contacts={me.contacts} />
|
||||
</TabPanel>
|
||||
{me.isStudent && (
|
||||
<TabPanel>
|
||||
<CourseList courses={me.courses} />
|
||||
</TabPanel>
|
||||
)}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</PageContent>
|
||||
<PageFooter textAlign='center'>
|
||||
<ProfileSignOutButton />
|
||||
</PageFooter>
|
||||
</PageWrapper>
|
||||
</UserProfile.Settings.EntryContent>
|
||||
</UserProfile.Settings.Entry>
|
||||
|
||||
{/* Contacts */}
|
||||
<UserProfile.Settings.Entry>
|
||||
<UserProfile.Settings.Title title={'Contacts'} />
|
||||
<UserProfile.Settings.EntryContent>
|
||||
<Stack spacing={'6'}>
|
||||
<UserProfile.Settings.PhoneInput />
|
||||
<UserProfile.Settings.TwitterInput />
|
||||
<UserProfile.Settings.TelegramInput />
|
||||
</Stack>
|
||||
</UserProfile.Settings.EntryContent>
|
||||
</UserProfile.Settings.Entry>
|
||||
|
||||
{/* Security */}
|
||||
<UserProfile.Settings.Entry>
|
||||
<UserProfile.Settings.Title title='Security' />
|
||||
<UserProfile.Settings.EntryContent>
|
||||
<UserProfile.Settings.PasswordInput defaultValue={'**********'} />
|
||||
</UserProfile.Settings.EntryContent>
|
||||
</UserProfile.Settings.Entry>
|
||||
|
||||
{/* Language */}
|
||||
<UserProfile.Settings.Entry>
|
||||
<UserProfile.Settings.Title title='Language' />
|
||||
<UserProfile.Settings.EntryContent>
|
||||
<UserProfile.Settings.LanguageSelect />
|
||||
</UserProfile.Settings.EntryContent>
|
||||
</UserProfile.Settings.Entry>
|
||||
|
||||
{/* Night Theme */}
|
||||
<UserProfile.Settings.Entry>
|
||||
<UserProfile.Settings.Title title='Night Theme' />
|
||||
<UserProfile.Settings.EntryContent>
|
||||
<UserProfile.Settings.ThemeSwitcher />
|
||||
</UserProfile.Settings.EntryContent>
|
||||
</UserProfile.Settings.Entry>
|
||||
|
||||
{/* Save Button */}
|
||||
<UserProfile.Settings.Entry>
|
||||
<UserProfile.Settings.Title />
|
||||
<UserProfile.Settings.EntryContent>
|
||||
<UserProfile.Settings.SaveSettingsButton />
|
||||
</UserProfile.Settings.EntryContent>
|
||||
</UserProfile.Settings.Entry>
|
||||
</UserProfile.Settings>
|
||||
</UserProfile>
|
||||
</Page.Content>
|
||||
</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
|
||||
*/
|
||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
||||
import { Page } from '@educt/components/PageElements';
|
||||
import { UserList, UserItem } from './components/UserList';
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
|
||||
@ -35,12 +35,12 @@ const UsersPage: React.FC<IPageProps> = () => {
|
||||
return (
|
||||
// TODO: delete page context for better performance
|
||||
<UsersPageContextProvider>
|
||||
<PageWrapper>
|
||||
<PageHeading heading='User management' description='You can add or delete users on this page.' />
|
||||
<PageContent mt='5'>
|
||||
<Page>
|
||||
<Page.Heading heading='User management' description='You can add or delete users on this page.' />
|
||||
<Page.Content mt='5'>
|
||||
<UserList render={UserItem} limit={6} />
|
||||
</PageContent>
|
||||
</PageWrapper>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
</UsersPageContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -14,6 +14,7 @@ import CoursePage from '@educt/pages/course/course';
|
||||
import EditCoursePage from '@educt/pages/courses/edit/edit';
|
||||
import ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
|
||||
import ChangeEmailPage from '@educt/pages/profile/change-email/change-email';
|
||||
import UserPage from './pages/user/user';
|
||||
import { UserRoleEnum } from './enums';
|
||||
const CoursesPage = lazy(() => import('@educt/pages/courses/courses'));
|
||||
const LessonPage = lazy(() => import('@educt/pages/lesson/lesson'));
|
||||
@ -126,6 +127,13 @@ export const routes: IRouteOption[] = [
|
||||
isPrivate: true,
|
||||
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
|
||||
},
|
||||
{
|
||||
path: '/user/:id',
|
||||
title: 'User',
|
||||
component: UserPage,
|
||||
isPrivate: true,
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: '/auth',
|
||||
title: 'Auth',
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { ApiServiceInstance } from '.';
|
||||
import * as helpers from '@educt/helpers';
|
||||
import { IApiRespose, IMe, IUser, IUserContacts } from '@educt/interfaces';
|
||||
import { IApiRespose, IMe, IUser, IUserContacts, IUserInfo } from '@educt/interfaces';
|
||||
import {
|
||||
CreateUserParamsType,
|
||||
FetchUsersParamsType,
|
||||
UpdateUserParamsType,
|
||||
UpdateUserContactsParamsType,
|
||||
UpdateUserInfoParamsType,
|
||||
} from '@educt/types';
|
||||
|
||||
class UserService {
|
||||
@ -45,6 +46,17 @@ class UserService {
|
||||
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
|
||||
*
|
||||
@ -97,6 +109,17 @@ class UserService {
|
||||
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
|
||||
*
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
CreateUserParamsType,
|
||||
FetchUsersParamsType,
|
||||
UpdateUserContactsParamsType,
|
||||
UpdateUserInfoParamsType,
|
||||
UpdateUserParamsType,
|
||||
} from '@educt/types';
|
||||
import { IUser, IMe, IPaginationMeta } from '@educt/interfaces';
|
||||
@ -62,10 +63,25 @@ export default class UserStore {
|
||||
public async loadCurrentUserData() {
|
||||
const result = await UserServiceInstance.fetchMe();
|
||||
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;
|
||||
|
||||
runInAction(() => {
|
||||
// TODO Fix this
|
||||
this.me = {
|
||||
id,
|
||||
first_name,
|
||||
@ -73,6 +89,8 @@ export default class UserStore {
|
||||
email,
|
||||
roles,
|
||||
fullname,
|
||||
about,
|
||||
last_login,
|
||||
likes,
|
||||
contacts,
|
||||
courses,
|
||||
|
||||
5
src/types/index.d.ts
vendored
5
src/types/index.d.ts
vendored
@ -134,6 +134,7 @@ export type CreateUserParamsType = {
|
||||
password: string;
|
||||
};
|
||||
export type UpdateUserParamsType = { first_name?: string; last_name?: string; email?: string };
|
||||
|
||||
export type UpdateUserContactsParamsType = {
|
||||
phone_number?: string | null | undefined;
|
||||
twitter_id?: string | null | undefined;
|
||||
@ -141,6 +142,10 @@ export type UpdateUserContactsParamsType = {
|
||||
vk_id?: string | null | undefined;
|
||||
};
|
||||
|
||||
export type UpdateUserInfoParamsType = {
|
||||
about?: string | null | undefined;
|
||||
} & UpdateUserContactsParamsType;
|
||||
|
||||
export type CreateCourseParamsType = {
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user