mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 02:09:08 +03:00
feat: refactor & added custom async select
This commit is contained in:
parent
3e69a47907
commit
eb33113912
72
package-lock.json
generated
72
package-lock.json
generated
@ -953,18 +953,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@emotion/cache": {
|
||||
"version": "11.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.4.0.tgz",
|
||||
"integrity": "sha512-Zx70bjE7LErRO9OaZrhf22Qye1y4F7iDl+ITjet0J+i+B88PrAOBkKvaAWhxsZf72tDLajwCgfCjJ2dvH77C3g==",
|
||||
"requires": {
|
||||
"@emotion/memoize": "^0.7.4",
|
||||
"@emotion/sheet": "^1.0.0",
|
||||
"@emotion/utils": "^1.0.0",
|
||||
"@emotion/weak-memoize": "^0.2.5",
|
||||
"stylis": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"@emotion/hash": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
@ -1028,11 +1016,6 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"@emotion/sheet": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.0.2.tgz",
|
||||
"integrity": "sha512-QQPB1B70JEVUHuNtzjHftMGv6eC3Y9wqavyarj4x4lg47RACkeSfNo5pxIOKizwS9AEFLohsqoaxGQj4p0vSIw=="
|
||||
},
|
||||
"@emotion/styled": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.3.0.tgz",
|
||||
@ -1206,14 +1189,6 @@
|
||||
"@types/react-router": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz",
|
||||
"integrity": "sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/scheduler": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||
@ -1414,15 +1389,6 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
|
||||
},
|
||||
"dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.870",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.870.tgz",
|
||||
@ -1870,11 +1836,6 @@
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||
},
|
||||
"mini-create-react-context": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
|
||||
@ -2207,25 +2168,19 @@
|
||||
"tiny-warning": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.1.0.tgz",
|
||||
"integrity": "sha512-SkEBD1AYsSXrIdNj5HBt7+Ehe+jxdiB448J0atJqR6lE3l/GcFlRf4JYB3NlHe/02jrW4AnIQLo1t0IqWrxXOw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.0",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.1.1",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"memoize-one": "^5.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react-transition-group": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"react-side-effect": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.1.tgz",
|
||||
"integrity": "sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ=="
|
||||
},
|
||||
"react-spinners": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.11.0.tgz",
|
||||
"integrity": "sha512-rDZc0ABWn/M1OryboGsWVmIPg8uYWl0L35jPUhr40+Yg+syVPjeHwvnB7XWaRpaKus3M0cG9BiJA+ZB0dAwWyw==",
|
||||
"requires": {
|
||||
"@emotion/react": "^11.1.4"
|
||||
}
|
||||
},
|
||||
"react-style-singleton": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz",
|
||||
@ -2243,17 +2198,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
|
||||
"integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.9",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"react-hook-form": "^7.12.2",
|
||||
"react-icons": "^3.11.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "^5.1.0",
|
||||
"react-spinners": "^0.11.0",
|
||||
"validator": "^13.6.0",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
|
||||
25
src/components/AsyncSelect/AsyncSelect.tsx
Normal file
25
src/components/AsyncSelect/AsyncSelect.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Select from '@educt/components/Select';
|
||||
import { ISelectPropsType } from '../Select/Select';
|
||||
import { OptionType } from '@educt/types';
|
||||
|
||||
interface IAsyncSelectPropsType extends ISelectPropsType {
|
||||
loadOptions: () => Promise<OptionType[] | undefined>;
|
||||
}
|
||||
|
||||
const AsyncSelect: React.FC<IAsyncSelectPropsType> = props => {
|
||||
const [options, setOptions] = useState<OptionType[] | []>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { loadOptions, ...selectProps } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
loadOptions()
|
||||
.then(options => options && setOptions(options))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
return <Select options={options} isLoading={isLoading} {...selectProps} />;
|
||||
};
|
||||
|
||||
export default AsyncSelect;
|
||||
3
src/components/AsyncSelect/index.ts
Normal file
3
src/components/AsyncSelect/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import AsyncSelect from './AsyncSelect';
|
||||
|
||||
export default AsyncSelect;
|
||||
@ -46,7 +46,7 @@ const NavMobile: React.FC = () => {
|
||||
padding='10px'
|
||||
borderRadius='md'
|
||||
marginTop='auto'
|
||||
backgroundColor={colorMode === 'dark' ? 'gray.700' : 'gray.100'}
|
||||
backgroundColor={colorMode === 'dark' ? 'gray.700' : 'gray.50'}
|
||||
>
|
||||
<Flex alignItems='center'>
|
||||
<Avatar size='sm' name={me.fullname} marginRight={3} />
|
||||
|
||||
63
src/components/Select/Select.tsx
Normal file
63
src/components/Select/Select.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { BeatLoader } from 'react-spinners';
|
||||
import { Box } from '@chakra-ui/layout';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
|
||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import type { OptionType } from '@educt/types';
|
||||
|
||||
export interface ISelectPropsType {
|
||||
options?: OptionType[];
|
||||
value?: string;
|
||||
onChange: (newValue: string) => void;
|
||||
placeholder?: string;
|
||||
noOptionsMessage?: string;
|
||||
isLoading?: boolean;
|
||||
loadingText?: string;
|
||||
}
|
||||
|
||||
const Select: React.FC<ISelectPropsType> = props => {
|
||||
const { options, value, noOptionsMessage, loadingText, isLoading } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu isLazy matchWidth>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
variant='outline'
|
||||
width='full'
|
||||
textAlign='left'
|
||||
justifyContent='flex-end'
|
||||
isLoading={isLoading}
|
||||
loadingText={loadingText || 'Loading...'}
|
||||
spinner={<BeatLoader size={6} color='gray' />}
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
>
|
||||
{options?.find(option => option.value === value)?.label || props?.placeholder || 'Select Option'}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{options && options.length !== 0 ? (
|
||||
options.map(option => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
props.onChange(option.value);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))
|
||||
) : (
|
||||
<Box textAlign='center'>{noOptionsMessage || 'No options.'}</Box>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
||||
3
src/components/Select/index.ts
Normal file
3
src/components/Select/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import Select from './Select';
|
||||
|
||||
export default Select;
|
||||
@ -1,13 +1,24 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IPageProps } from '@educt/interfaces';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import { IPageProps } from '@educt/interfaces';
|
||||
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
import AuthForm from './components/AuthForm';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
|
||||
/**
|
||||
* Auth page
|
||||
*/
|
||||
@ -16,6 +27,7 @@ const AuthPage: React.FC<IPageProps> = ({ title }) => {
|
||||
|
||||
/**
|
||||
* User already authorized
|
||||
* This is triggered by mobx observer function
|
||||
*/
|
||||
if (authStore.isLoggedIn) {
|
||||
return <Redirect to='/' />;
|
||||
|
||||
@ -16,8 +16,6 @@ import {
|
||||
import { MdCloudUpload } from 'react-icons/md';
|
||||
import { SubmitHandler, useForm, useWatch, Controller } from 'react-hook-form';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import * as helpers from '@educt/helpers';
|
||||
|
||||
/**
|
||||
* Types
|
||||
@ -37,6 +35,8 @@ import useIsMountedRef from '@educt/hooks/useIsMountedRef';
|
||||
* Schema
|
||||
*/
|
||||
import CreateCourseSchema from './CreateCourseForm.validator';
|
||||
import AsyncSelect from '@educt/components/AsyncSelect';
|
||||
import Select from '@educt/components/Select';
|
||||
|
||||
type CreateCourseInputType = {
|
||||
title: string;
|
||||
@ -79,17 +79,14 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
|
||||
/**
|
||||
* Load users into select field
|
||||
*/
|
||||
const loadUsersOptions = async (inputValue: string): Promise<OptionType[] | undefined> => {
|
||||
const loadUsersOptions = async (): Promise<OptionType[] | undefined> => {
|
||||
try {
|
||||
const users = await userService.fetchAll({ search: inputValue, role: UserRoleEnum.TEACHER });
|
||||
const users = await userService.fetchAll({ limit: 12, role: UserRoleEnum.TEACHER });
|
||||
|
||||
// TODO: remove filter later
|
||||
return users.data
|
||||
.filter(user => helpers.userContainRoles(user.roles, [UserRoleEnum.TEACHER]))
|
||||
.map(user => ({
|
||||
label: user.fullname,
|
||||
value: user.id,
|
||||
}));
|
||||
return users.data.map(user => ({
|
||||
label: user.fullname,
|
||||
value: user.id,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
toast({ title: 'Error' });
|
||||
@ -102,7 +99,7 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
|
||||
/**
|
||||
* Load categories into select filed
|
||||
*/
|
||||
const loadCategoriesOptions = async (inputValue: string): Promise<OptionType[] | undefined> => {
|
||||
const loadCategoriesOptions = async (): Promise<OptionType[] | undefined> => {
|
||||
try {
|
||||
const categories = await categoryService.fetchAll();
|
||||
return categories.data.map(category => ({
|
||||
@ -163,8 +160,8 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
|
||||
<InputRightElement
|
||||
mr='2'
|
||||
children={
|
||||
<Text as='small' color={!!errors.title || title.length > 60 ? 'red.500' : 'gray.500'}>
|
||||
{title.length}/60
|
||||
<Text as='small' color={!!errors.title || title.length > 90 ? 'red.500' : 'gray.500'}>
|
||||
{title.length}/90
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
@ -187,16 +184,14 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
|
||||
<Controller
|
||||
control={control}
|
||||
name='category_id'
|
||||
render={({ field: { onChange } }) => (
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<AsyncSelect
|
||||
defaultOptions
|
||||
cacheOptions
|
||||
loadOptions={loadCategoriesOptions}
|
||||
isSearchable={false}
|
||||
onChange={selected => onChange(selected?.value)}
|
||||
onChange={selected => onChange(selected)}
|
||||
value={value}
|
||||
placeholder='Select Category'
|
||||
loadingMessage={() => 'Loading...'}
|
||||
noOptionsMessage={() => 'There are no categories.'}
|
||||
noOptionsMessage='There are no categories.'
|
||||
loadingText='Wait...'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -209,15 +204,14 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
|
||||
<Controller
|
||||
control={control}
|
||||
name='teacher_id'
|
||||
render={({ field: { onChange } }) => (
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<AsyncSelect
|
||||
defaultOptions
|
||||
cacheOptions
|
||||
loadOptions={loadUsersOptions}
|
||||
onChange={selected => onChange(selected?.value)}
|
||||
onChange={selected => onChange(selected)}
|
||||
value={value}
|
||||
placeholder='Select Teacher'
|
||||
loadingMessage={() => 'Loading...'}
|
||||
noOptionsMessage={() => 'Cannot find any teacher.'}
|
||||
noOptionsMessage='There are no teachers.'
|
||||
loadingText='Wait...'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -4,7 +4,7 @@ const FILE_SIZE = 160 * 1024;
|
||||
const SUPPORTED_FORMATS = ['image/jpg', 'image/jpeg', 'image/gif', 'image/png'];
|
||||
|
||||
const CreateCourseSchema = yup.object().shape({
|
||||
title: yup.string().required('Title field name is required').max(60, 'Title must be at most 60 characters'),
|
||||
title: yup.string().required('Title field name is required').max(90, 'Title must be at most 90 characters'),
|
||||
description: yup.string().required('Description field name is required'),
|
||||
image: yup
|
||||
.mixed()
|
||||
|
||||
@ -7,22 +7,12 @@ import { observer } from 'mobx-react';
|
||||
/**
|
||||
* Main page
|
||||
*/
|
||||
const MainPage: React.FC<IPageProps> = ({ title }) => {
|
||||
const MainPage: React.FC<IPageProps> = () => {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb fontWeight='medium' fontSize='sm'>
|
||||
<BreadcrumbItem isCurrentPage>
|
||||
<BreadcrumbLink href='/'>{title}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
<Box mt='10'>
|
||||
<Box>
|
||||
<Heading as='h1'>Main page</Heading>
|
||||
<Grid templateColumns='3fr 1fr' mt='25px'></Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
<Box>
|
||||
<Heading as='h1'>Main page</Heading>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(MainPage);
|
||||
export default MainPage;
|
||||
|
||||
@ -6,23 +6,11 @@ import { IPageProps } from '@educt/interfaces';
|
||||
/**
|
||||
* Messages page
|
||||
*/
|
||||
const MessagesPage: React.FC<IPageProps> = ({ title }) => {
|
||||
const MessagesPage: React.FC<IPageProps> = () => {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb fontWeight='medium' fontSize='sm'>
|
||||
<BreadcrumbItem isCurrentPage>
|
||||
<BreadcrumbLink href='/'>{title}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
<Box mt='10'>
|
||||
<Box>
|
||||
<Heading as='h1'>Messages page</Heading>
|
||||
<Grid templateColumns='3fr 1fr' mt='25px'>
|
||||
...
|
||||
</Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
<Box>
|
||||
<Heading as='h1'>Messages page</Heading>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -147,4 +147,4 @@ const ProfilePage: React.FC<IPageProps> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(ProfilePage);
|
||||
export default ProfilePage;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user