mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
feat: attach students modal layout
This commit is contained in:
parent
d1d345ca88
commit
7303dd5b43
@ -9,14 +9,20 @@ export const CreateButton: React.FC<ButtonProps> = props => (
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const AddButton: React.FC<ButtonProps> = props => (
|
||||
<Button variant='outline' size='sm' loadingText='Adding...' leftIcon={<AddIcon />} {...props}>
|
||||
Add new
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const DeleteButton: React.FC<ButtonProps> = props => (
|
||||
<Button variant='outline' leftIcon={<DeleteIcon />} colorScheme='red' size='sm' {...props}>
|
||||
<Button variant='outline' leftIcon={<DeleteIcon />} loadingText='Deleting...' colorScheme='red' size='sm' {...props}>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const EditButton: React.FC<ButtonProps> = props => (
|
||||
<Button leftIcon={<EditIcon />} variant='outline' size='sm' {...props}>
|
||||
<Button leftIcon={<EditIcon />} variant='outline' loadingText='Saving...' size='sm' {...props}>
|
||||
Edit
|
||||
</Button>
|
||||
);
|
||||
|
||||
66
src/components/Modal/BaseModal.tsx
Normal file
66
src/components/Modal/BaseModal.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { Text, Flex, Box, Divider } from '@chakra-ui/react';
|
||||
import {
|
||||
Icon,
|
||||
ModalProps,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { IconType } from 'react-icons';
|
||||
|
||||
export interface IBaseModalProps extends ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
heading: string;
|
||||
icon?: IconType;
|
||||
description?: string;
|
||||
confirmText?: string;
|
||||
}
|
||||
|
||||
const BaseModal: React.FC<IBaseModalProps> = props => {
|
||||
const { heading, description, isOpen, onClose, confirmText, icon, ...modalProps } = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} {...modalProps}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<Flex alignItems='center'>
|
||||
{icon && <Icon as={icon} mr='2' />}
|
||||
<Text fontWeight='bold'>{heading}</Text>
|
||||
</Flex>
|
||||
{description && (
|
||||
<Box>
|
||||
<Text fontSize='md' fontWeight='normal'>
|
||||
{description}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</ModalHeader>
|
||||
|
||||
<Divider />
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody>
|
||||
<Box>{props.children}</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onClick={onClose} borderRadius='md' mr='2'>
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={onClose} borderRadius='md' variant='outline' colorScheme='blue'>
|
||||
{confirmText || 'Proceed'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseModal;
|
||||
3
src/components/Modal/index.ts
Normal file
3
src/components/Modal/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import BaseModal from './BaseModal';
|
||||
|
||||
export default BaseModal;
|
||||
@ -1,48 +1,147 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { Text, Flex } from '@chakra-ui/react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Tag,
|
||||
TagLabel,
|
||||
TagCloseButton,
|
||||
Input,
|
||||
Box,
|
||||
Flex,
|
||||
IconButton,
|
||||
BoxProps,
|
||||
InputGroup,
|
||||
Text,
|
||||
Stack,
|
||||
InputRightElement,
|
||||
Avatar,
|
||||
} from '@chakra-ui/react';
|
||||
import { useDisclosure, Divider } from '@chakra-ui/react';
|
||||
import BaseModal from '@educt/components/Modal';
|
||||
import { MdGroup } from 'react-icons/md';
|
||||
import { AddIcon, SearchIcon } from '@chakra-ui/icons';
|
||||
import { UserServiceInstance } from '@educt/services';
|
||||
import { IUser } from '@educt/interfaces';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
import { BeatLoader } from 'react-spinners';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { FetchUsersParamsType } from '@educt/types';
|
||||
|
||||
type AddStudentsModalPropsType = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const SelectUsersInput: React.FC<BoxProps> = props => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [selected, setSelected] = useState<Array<IUser>>([]);
|
||||
const [users, setUsers] = useState<Array<IUser>>([]);
|
||||
const [limit, setLimit] = useState<number>(5);
|
||||
const filtered = users.filter(u => selected.every(s => s.id !== u.id));
|
||||
const handleError = useErrorHandler();
|
||||
const isEmpty = filtered.length === 0;
|
||||
|
||||
const fetch = async (params?: FetchUsersParamsType | undefined) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const { data } = await UserServiceInstance.fetchAll({ search, limit });
|
||||
setUsers(data);
|
||||
} catch (error: any) {
|
||||
handleError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value);
|
||||
const handleSelect = (user: IUser) => () => setSelected(prev => [...prev, user]);
|
||||
const handleRemoveSelected = (id: IUser['id']) => () => setSelected(prev => prev.filter(u => u.id !== id));
|
||||
|
||||
useEffect(() => {
|
||||
fetch();
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<Box {...props}>
|
||||
{selected.length !== 0 && (
|
||||
<Box mt='1'>
|
||||
{selected.map(selected => (
|
||||
<Tag key={selected.id} size='md' colorScheme='blue' borderRadius='full' mr='1' mb='1'>
|
||||
<Avatar size='xs' name={selected.fullname} ml={-1} mr={2} />
|
||||
<TagLabel>{selected.fullname}</TagLabel>
|
||||
<TagCloseButton onClick={handleRemoveSelected(selected.id)} />
|
||||
</Tag>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<InputGroup mt='2'>
|
||||
<Input
|
||||
value={search}
|
||||
onFocus={() => fetch()}
|
||||
onChange={handleSearch}
|
||||
type='text'
|
||||
placeholder='Search for users...'
|
||||
/>
|
||||
<InputRightElement pointerEvents='none' children={<SearchIcon color='gray.300' />} />
|
||||
</InputGroup>
|
||||
|
||||
{isLoading && (
|
||||
<Text as='small' color='gray.500'>
|
||||
Loading...
|
||||
<BeatLoader size={6} color='gray' />
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isLoading && isEmpty && (
|
||||
<Text as='small' color='red.500'>
|
||||
Cannot find any user
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isLoading && !isEmpty && (
|
||||
<Stack borderWidth='1px' mt='2' borderRadius='md'>
|
||||
<Virtuoso
|
||||
style={{ height: filtered.length * 54, maxHeight: 216 }}
|
||||
data={filtered}
|
||||
itemContent={(index, user) => (
|
||||
<Flex key={user.id} borderBottomWidth='1px' p='7px 16px' alignItems='center'>
|
||||
<Avatar name={user.fullname} size='xs' mr={{ base: '0', sm: '3' }} mb={{ base: '1', sm: '0' }} />
|
||||
<Box textAlign={{ base: 'center', sm: 'left' }}>
|
||||
<Text fontWeight='medium' fontSize='sm'>
|
||||
{user.fullname}
|
||||
</Text>
|
||||
<Text color='gray.500' fontSize='xs' overflowWrap='anywhere'>
|
||||
{user.email}
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton
|
||||
onClick={handleSelect(user)}
|
||||
aria-label='Add student'
|
||||
icon={<AddIcon />}
|
||||
ml='auto'
|
||||
borderRadius='full'
|
||||
size='sm'
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({ isOpen, onClose }) => {
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isOpen} onClose={onClose} size='md'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<Flex alignItems='center'>
|
||||
<MdGroup />
|
||||
<Text ml={2}>Add students</Text>
|
||||
</Flex>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalCloseButton />
|
||||
<Divider />
|
||||
|
||||
<ModalBody></ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme='blue' variant='outline' onClick={onClose}>
|
||||
Save
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
<BaseModal
|
||||
icon={MdGroup}
|
||||
heading='Add new students'
|
||||
description='Attach new students to current course.'
|
||||
confirmText='Add students'
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<SelectUsersInput />
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import { IPageProps } from '@educt/interfaces';
|
||||
*/
|
||||
import { PageWrapper, PageHeading, PageContent } from '@educt/components/PageElements';
|
||||
import { CreateButton } from '@educt/components/Buttons';
|
||||
import { Box } from '@chakra-ui/layout';
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
import CourseList from './components/CourseList';
|
||||
import CategoryList from './components/CategoryList';
|
||||
@ -32,12 +33,17 @@ const CoursesPage: React.FC<IPageProps> = () => {
|
||||
} = useRootStore();
|
||||
const history = useHistory();
|
||||
|
||||
const handleCreateCourse = () => history.push('/courses/create');
|
||||
|
||||
if (me === null) return <LoadingPage />;
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageHeading heading='Courses' description='List of all courses' />
|
||||
{(me.isAdmin || me.isTeacher) && <CreateButton onClick={() => history.push('/courses/create')} />}
|
||||
<PageHeading 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'>
|
||||
<CoursesTabs mb='7' />
|
||||
<CategoryList render={CategoryItem} />
|
||||
|
||||
@ -3,6 +3,7 @@ import { Table, Tbody, Button, Input, InputGroup, InputLeftElement, Flex, Box, T
|
||||
import { MdSearch } from 'react-icons/md';
|
||||
|
||||
import AddStudentsModal from '@educt/components/Modals/AddStudentsModal';
|
||||
import { AddButton } from '@educt/components/Buttons';
|
||||
import BulkActionsMenu from './BulkActionsMenu';
|
||||
import StudentTableHead from './StudentTableHead';
|
||||
|
||||
@ -17,8 +18,6 @@ import type { ICourse } from '@educt/interfaces';
|
||||
*/
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { CreateButton } from '@educt/components/Buttons';
|
||||
|
||||
type StudentTableListPropsType = {
|
||||
render: React.FC<StudentTableRowPropsType>;
|
||||
@ -105,7 +104,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = ({ render: Row, st
|
||||
<BulkActionsMenu selected={selected} rows={rows} setRows={setRows} />
|
||||
</>
|
||||
)}
|
||||
<CreateButton onClick={onOpenAddStudentModal} />
|
||||
<AddButton onClick={onOpenAddStudentModal} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Table overflow='hidden' borderRadius='lg' mt='2'>
|
||||
|
||||
@ -24,7 +24,7 @@ import { useParams } from 'react-router-dom';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
import { useFetchCourse } from '@educt/hooks/queries';
|
||||
import { useDisclosure } from '@chakra-ui/hooks';
|
||||
import { PageContent, PageWrapper } from '@educt/components/PageElements';
|
||||
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
|
||||
import { DeleteButton } from '@educt/components/Buttons';
|
||||
import { StatusButton } from './components';
|
||||
|
||||
@ -60,18 +60,12 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
||||
</Helmet>
|
||||
|
||||
<DeleteCourseDialog course={course} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />
|
||||
|
||||
<Flex justifyContent='space-between' alignItems='center' flexWrap='wrap'>
|
||||
<Box mr='3'>
|
||||
<Heading as='h1'>Course editor</Heading>
|
||||
<Text mt='2'>{course.title}</Text>
|
||||
</Box>
|
||||
<Box ml='auto' alignSelf='flex-end' justifySelf='flex-end' mt={{ base: '7', md: '0' }}>
|
||||
<PageHeading heading='Course editor' description={course.title}>
|
||||
<Flex mt='5' justifyContent='flex-end'>
|
||||
<StatusButton courseId={course.id} currentStatus={course.status} />
|
||||
<DeleteButton onClick={onOpenDeleteDialog} ml='2' />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
</Flex>
|
||||
</PageHeading>
|
||||
<PageContent>
|
||||
<Box>
|
||||
<Tabs isLazy mt='8'>
|
||||
|
||||
@ -28,7 +28,7 @@ class UserService {
|
||||
* @returns Array of users
|
||||
*/
|
||||
public async fetchAll(params?: FetchUsersParamsType): Promise<IApiRespose<IUser[]>> {
|
||||
const { page = 1, limit = 6, role, search } = params || {};
|
||||
const { page, limit, role, search } = params || {};
|
||||
const result = await this.api.get('/v1/users', {
|
||||
params: {
|
||||
page,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user