mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: refactor layout & added file select component
This commit is contained in:
parent
eb33113912
commit
ff88ad75a3
86
src/components/FileSelect/FileSelect.tsx
Normal file
86
src/components/FileSelect/FileSelect.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { Flex, Box, Text } from '@chakra-ui/layout';
|
||||||
|
import { Button } from '@chakra-ui/button';
|
||||||
|
import { MdAttachFile } from 'react-icons/md';
|
||||||
|
import * as helpres from '@educt/helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useColorModeValue } from '@chakra-ui/color-mode';
|
||||||
|
import { Input } from '@chakra-ui/input';
|
||||||
|
|
||||||
|
type FileSelectPropsType = {
|
||||||
|
onChange: (file: File) => void;
|
||||||
|
supportedFormats?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileSelect: React.FC<FileSelectPropsType> = ({ onChange, supportedFormats }) => {
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | undefined>(undefined);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the file input selection
|
||||||
|
*/
|
||||||
|
const onSelectFileHandler = () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeFileHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const { files } = e.target;
|
||||||
|
|
||||||
|
if (files && files.length !== 0) {
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
onChange(file);
|
||||||
|
setSelectedFile(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
backgroundColor={useColorModeValue('gray.50', 'gray.700')}
|
||||||
|
p='15px 20px'
|
||||||
|
lineHeight='20px'
|
||||||
|
borderRadius='md'
|
||||||
|
borderWidth='1px'
|
||||||
|
>
|
||||||
|
<Input type='file' ref={fileInputRef} onChange={onChangeFileHandler} display='none' />
|
||||||
|
<Flex justifyContent='space-between' alignItems='center' sx={{ gap: '10px' }}>
|
||||||
|
<Box>
|
||||||
|
{selectedFile ? (
|
||||||
|
<Text fontSize='lg' fontWeight='bold'>
|
||||||
|
{selectedFile.name}
|
||||||
|
<br />
|
||||||
|
<Text as='small' fontWeight='normal'>
|
||||||
|
{helpres.transformBytes(selectedFile.size)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text fontWeight='normal' color='gray'>
|
||||||
|
Choose a file...
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{supportedFormats && (
|
||||||
|
<Box mt='1'>
|
||||||
|
<Text as='small' fontSize='xs' color='gray.500'>
|
||||||
|
Supported formats: {supportedFormats.join(', ')}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box p='0 5px'>
|
||||||
|
<Button onClick={onSelectFileHandler} rightIcon={<MdAttachFile />}>
|
||||||
|
Select file
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileSelect;
|
||||||
12
src/components/LoadingList/LoadingList.tsx
Normal file
12
src/components/LoadingList/LoadingList.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from '@chakra-ui/layout';
|
||||||
|
|
||||||
|
const LoadingList: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Box textAlign='center' margin='10px 0' userSelect='none'>
|
||||||
|
<Text color='gray.500'>Wait a second...</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingList;
|
||||||
3
src/components/LoadingList/index.ts
Normal file
3
src/components/LoadingList/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import LoadingList from './LoadingList';
|
||||||
|
|
||||||
|
export default LoadingList;
|
||||||
@ -29,6 +29,8 @@ const Select: React.FC<ISelectPropsType> = props => {
|
|||||||
<MenuButton
|
<MenuButton
|
||||||
as={Button}
|
as={Button}
|
||||||
variant='outline'
|
variant='outline'
|
||||||
|
fontWeight='400'
|
||||||
|
fontSize='md'
|
||||||
width='full'
|
width='full'
|
||||||
textAlign='left'
|
textAlign='left'
|
||||||
justifyContent='flex-end'
|
justifyContent='flex-end'
|
||||||
|
|||||||
@ -50,3 +50,22 @@ export function transformToFormData(data: object): FormData {
|
|||||||
|
|
||||||
return formData;
|
return formData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform bytes to readable format
|
||||||
|
*
|
||||||
|
* @param bytes Number of bytes
|
||||||
|
* @param decimals
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
export function transformBytes(bytes: number, decimals = 2): string {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|||||||
@ -54,7 +54,7 @@ const AuthForm: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box w='full' maxW='450px' borderWidth='1px' borderRadius='lg' p='8'>
|
<Box flexBasis='500px' borderWidth='1px' borderRadius='lg' p='8'>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Flex justifyContent='center' alignItems='center'>
|
<Flex justifyContent='center' alignItems='center'>
|
||||||
<Box w='full'>
|
<Box w='full'>
|
||||||
@ -67,27 +67,24 @@ const AuthForm: React.FC = () => {
|
|||||||
<FormLabel>Login</FormLabel>
|
<FormLabel>Login</FormLabel>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputLeftElement children={<Icon as={MdAccountCircle} />} />
|
<InputLeftElement children={<Icon as={MdAccountCircle} />} />
|
||||||
<Input type='text' placeholder='Login' {...register('login')} isInvalid={errors.login ? true : false} />
|
<Input type='text' placeholder='Login' {...register('login')} isInvalid={!!errors.login} />
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<Text as='small' color='red.500'>
|
<Text as='small' color='red.500'>
|
||||||
{errors.login?.message}
|
{errors.login?.message}
|
||||||
</Text>
|
</Text>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl mt={5}>
|
<FormControl mt={5}>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputLeftElement children={<Icon as={MdVpnKey} />} />
|
<InputLeftElement children={<Icon as={MdVpnKey} />} />
|
||||||
<Input
|
<Input type='password' placeholder='******' {...register('password')} isInvalid={!!errors.password} />
|
||||||
type='password'
|
|
||||||
placeholder='******'
|
|
||||||
{...register('password')}
|
|
||||||
isInvalid={errors.password ? true : false}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<Text as='small' color='red.500'>
|
<Text as='small' color='red.500'>
|
||||||
{errors.password?.message}
|
{errors.password?.message}
|
||||||
</Text>
|
</Text>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
loadingText='Logging in...'
|
loadingText='Logging in...'
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import yup from '@educt/schema';
|
|||||||
|
|
||||||
const LoginSchema = yup.object().shape({
|
const LoginSchema = yup.object().shape({
|
||||||
login: yup.string().required('Login is required'),
|
login: yup.string().required('Login is required'),
|
||||||
password: yup.string().min(6, 'Password must be at least 6 characters').required('Password is required'),
|
password: yup.string().required('Password is required'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default LoginSchema;
|
export default LoginSchema;
|
||||||
|
|||||||
@ -7,13 +7,13 @@ import { Box, Grid, Text } from '@chakra-ui/react';
|
|||||||
*/
|
*/
|
||||||
import { ICourse } from '@educt/interfaces';
|
import { ICourse } from '@educt/interfaces';
|
||||||
import { CourseStatusEnum } from '@educt/enums';
|
import { CourseStatusEnum } from '@educt/enums';
|
||||||
|
import { CourseItemPropsType } from './CourseItem';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Components
|
* Components
|
||||||
*/
|
*/
|
||||||
import CourseItem, { CourseItemPropsType } from './CourseItem';
|
|
||||||
import CourseListLoading from './CourseListLoading';
|
|
||||||
import DeleteCourseDialog from '../DeleteCourseDialog';
|
import DeleteCourseDialog from '../DeleteCourseDialog';
|
||||||
|
import LoadingList from '@educt/components/LoadingList';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks
|
* Hooks
|
||||||
@ -84,7 +84,7 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
|
|||||||
/**
|
/**
|
||||||
* Loading
|
* Loading
|
||||||
*/
|
*/
|
||||||
if (courses === null || isLoading) return <CourseListLoading />;
|
if (courses === null || isLoading) return <LoadingList />;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No courses
|
* No courses
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Box, Text } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
const CourseListLoading: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<Box textAlign='center' mt='10' userSelect='none'>
|
|
||||||
<Text color='gray.500'>Wait a second...</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CourseListLoading;
|
|
||||||
@ -13,9 +13,10 @@ import {
|
|||||||
InputRightElement,
|
InputRightElement,
|
||||||
Input,
|
Input,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { MdCloudUpload } from 'react-icons/md';
|
import { MdAttachFile, MdCloudUpload } from 'react-icons/md';
|
||||||
import { SubmitHandler, useForm, useWatch, Controller } from 'react-hook-form';
|
import { SubmitHandler, useForm, useWatch, Controller } from 'react-hook-form';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import AsyncSelect from '@educt/components/AsyncSelect';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
@ -29,18 +30,18 @@ import { CourseStatusEnum, UserRoleEnum } from '@educt/enums';
|
|||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
|
import { useColorModeValue } from '@chakra-ui/color-mode';
|
||||||
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
|
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema
|
* Schema
|
||||||
*/
|
*/
|
||||||
import CreateCourseSchema from './CreateCourseForm.validator';
|
import CreateCourseSchema from './CreateCourseForm.validator';
|
||||||
import AsyncSelect from '@educt/components/AsyncSelect';
|
import FileSelect from '@educt/components/FileSelect/FileSelect';
|
||||||
import Select from '@educt/components/Select';
|
|
||||||
|
|
||||||
type CreateCourseInputType = {
|
type CreateCourseInputType = {
|
||||||
title: string;
|
title: string;
|
||||||
image: FileList;
|
image: File;
|
||||||
category_id: string;
|
category_id: string;
|
||||||
description: string;
|
description: string;
|
||||||
teacher_id: string;
|
teacher_id: string;
|
||||||
@ -126,7 +127,7 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
teacher_id: data.teacher_id,
|
teacher_id: data.teacher_id,
|
||||||
category_id: data.category_id,
|
category_id: data.category_id,
|
||||||
image: data.image[0],
|
image: data.image,
|
||||||
status: status ?? CourseStatusEnum.DRAFT,
|
status: status ?? CourseStatusEnum.DRAFT,
|
||||||
});
|
});
|
||||||
toast({ title: `Course successfully created.`, status: 'success' });
|
toast({ title: `Course successfully created.`, status: 'success' });
|
||||||
@ -173,7 +174,13 @@ const CreateCourseForm: React.FC<CreateCourseFormProps> = () => {
|
|||||||
|
|
||||||
<FormControl id='image' isInvalid={!!errors.image}>
|
<FormControl id='image' isInvalid={!!errors.image}>
|
||||||
<FormLabel>Image</FormLabel>
|
<FormLabel>Image</FormLabel>
|
||||||
<Input size='md' variant='unstyled' {...register('image')} type='file' accept='image/*' />
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name='image'
|
||||||
|
render={({ field: { onChange } }) => (
|
||||||
|
<FileSelect onChange={file => onChange(file)} supportedFormats={['JPG', 'JPEG', 'PNG', 'WEBP']} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Text as='small' color='red.500'>
|
<Text as='small' color='red.500'>
|
||||||
{errors.image?.message}
|
{errors.image?.message}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
import yup from '@educt/schema';
|
import yup from '@educt/schema';
|
||||||
|
|
||||||
const FILE_SIZE = 160 * 1024;
|
const FILE_SIZE = 1000 * 1024;
|
||||||
const SUPPORTED_FORMATS = ['image/jpg', 'image/jpeg', 'image/gif', 'image/png'];
|
const SUPPORTED_FORMATS = ['image/jpg', 'image/jpeg', 'image/png', 'image/webp'];
|
||||||
|
|
||||||
const CreateCourseSchema = yup.object().shape({
|
const CreateCourseSchema = yup.object().shape({
|
||||||
title: yup.string().required('Title field name is required').max(90, 'Title must be at most 90 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'),
|
description: yup.string().required('Description field name is required'),
|
||||||
image: yup
|
image: yup
|
||||||
.mixed()
|
.mixed()
|
||||||
.test('fileSize', 'File size is too large', (value: FileList) => {
|
.test('fileSize', 'File size is too large', (value: File) => {
|
||||||
if (!value.length) return true;
|
if (!value) return true;
|
||||||
return value[0] && value[0].size <= FILE_SIZE;
|
return value && value.size <= FILE_SIZE;
|
||||||
})
|
})
|
||||||
.test('fileFormat', 'This file format is not supported', (value: FileList) => {
|
.test('fileFormat', 'This file format is not supported', (value: File) => {
|
||||||
if (!value.length) return true;
|
if (!value) return true;
|
||||||
return value[0] && SUPPORTED_FORMATS.includes(value[0].type);
|
return value && SUPPORTED_FORMATS.includes(value.type);
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
category_id: yup.string().required('Category field is required'),
|
category_id: yup.string().required('Category field is required'),
|
||||||
|
|||||||
@ -52,17 +52,19 @@ const UserItem: React.FC<UserItemPropsType> = ({ user, onEdit, onDelete }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box borderBottomWidth='1px' borderRadius='lg' w='full' p='3' bg={useColorModeValue('gray.50', 'gray.700')}>
|
<Box borderBottomWidth='1px' borderRadius='lg' w='full' p='3' bg={useColorModeValue('gray.50', 'gray.700')}>
|
||||||
<Flex alignItems='center' justifyContent='space-between' sx={{ gap: '20px' }}>
|
<Flex flexDir={{ base: 'column', sm: 'row' }} alignItems='center' sx={{ gap: '20px' }}>
|
||||||
<Flex alignItems='center' flexBasis='230px'>
|
<Flex alignItems='center' flexDir={{ base: 'column', sm: 'row' }} flex='1 1 0'>
|
||||||
<Avatar name={user.fullname} size='sm' mr='3' />
|
<Avatar name={user.fullname} size='sm' mr={{ base: '0', sm: '3' }} mb={{ base: '1', sm: '0' }} />
|
||||||
<Box>
|
<Box textAlign={{ base: 'center', sm: 'left' }}>
|
||||||
<Link as={ReactRouterLink} to={`/user/${user.id}`} fontSize='md' fontWeight='medium'>
|
<Link as={ReactRouterLink} to={`/user/${user.id}`} fontSize='md' fontWeight='medium'>
|
||||||
{user.fullname}
|
{user.fullname}
|
||||||
</Link>
|
</Link>
|
||||||
<Text fontSize='sm'>{user.email}</Text>
|
<Text fontSize='sm' overflowWrap='anywhere'>
|
||||||
|
{user.email}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex sx={{ gap: '5px' }}>
|
<Flex sx={{ gap: '5px' }} justifyContent='center' flex='1 1 0'>
|
||||||
<UserBadge roles={user.roles} />
|
<UserBadge roles={user.roles} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { useLocation } from 'react-router';
|
|||||||
import { useErrorHandler } from 'react-error-boundary';
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
import { useDisclosure } from '@chakra-ui/hooks';
|
import { useDisclosure } from '@chakra-ui/hooks';
|
||||||
|
import LoadingList from '@educt/components/LoadingList';
|
||||||
|
|
||||||
type UserListPropsType = { render: React.FC<UserItemPropsType> };
|
type UserListPropsType = { render: React.FC<UserItemPropsType> };
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ const UserList: React.FC<UserListPropsType> = ({ render: Item }) => {
|
|||||||
userStore.loadUsersData().catch(error => handleError(error));
|
userStore.loadUsersData().catch(error => handleError(error));
|
||||||
}, [handleError, userStore, location.search]);
|
}, [handleError, userStore, location.search]);
|
||||||
|
|
||||||
if (pagination === undefined || users === null) return <div>Loading users...</div>;
|
if (pagination === undefined || users === null) return <LoadingList />;
|
||||||
|
|
||||||
const pagesCount = Math.ceil(pagination.total / pagination.per_page);
|
const pagesCount = Math.ceil(pagination.total / pagination.per_page);
|
||||||
|
|
||||||
@ -123,23 +124,21 @@ const UserList: React.FC<UserListPropsType> = ({ render: Item }) => {
|
|||||||
<Text>Total: ({pagination.total})</Text>
|
<Text>Total: ({pagination.total})</Text>
|
||||||
<Text>Actions</Text>
|
<Text>Actions</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
{loading ? (
|
{!loading ? (
|
||||||
<Box textAlign='center' mt='10' userSelect='none'>
|
|
||||||
<Text color='gray.500'>Wait a second...</Text>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Stack mt='4' spacing='2'>
|
<Stack mt='4' spacing='2'>
|
||||||
{users.map(user => (
|
{users.map(user => (
|
||||||
<Item key={user.id} user={user} onEdit={onEditUser} onDelete={onDeleteUser} />
|
<Item key={user.id} user={user} onEdit={onEditUser} onDelete={onDeleteUser} />
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<LoadingList />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Flex margin='2rem 0' flexDirection={{ base: 'column', sm: 'column', md: 'row' }}>
|
<Flex margin='2rem 0' flexDirection={{ base: 'column', sm: 'column', md: 'row' }}>
|
||||||
<Box textAlign={{ base: 'center', sm: 'center', md: 'left' }} mb={{ base: '2', md: '0' }}>
|
<Box textAlign={{ base: 'center', sm: 'center', md: 'left' }} mb={{ base: '2', md: '0' }}>
|
||||||
Page <b>{pagination.current_page}</b> of {pagesCount}
|
Page <b>{pagination.current_page}</b> of {pagesCount}
|
||||||
</Box>
|
</Box>
|
||||||
<Flex ml='auto' mr='auto' alignItems='center' sx={{ gap: '30px' }}>
|
<Flex ml='auto' mr='auto' justifyContent='center' flexWrap='wrap' alignItems='center' sx={{ gap: '30px' }}>
|
||||||
<Button
|
<Button
|
||||||
onClick={prevPage}
|
onClick={prevPage}
|
||||||
disabled={pagination.current_page <= 1}
|
disabled={pagination.current_page <= 1}
|
||||||
|
|||||||
@ -43,10 +43,8 @@ const UsersPage: React.FC<IPageProps> = () => {
|
|||||||
<Flex mt='5' flexDir='column' h='full'>
|
<Flex mt='5' flexDir='column' h='full'>
|
||||||
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
|
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
|
||||||
<UserSearch />
|
<UserSearch />
|
||||||
<Flex sx={{ gap: '10px' }}>
|
|
||||||
<CreateUserForm me={me} />
|
<CreateUserForm me={me} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
|
||||||
<UserList render={UserItem} />
|
<UserList render={UserItem} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user