feat: attach students modal layout

This commit is contained in:
Sergey Yarkov 2021-12-09 23:22:29 +03:00
parent d1d345ca88
commit 7303dd5b43
8 changed files with 226 additions and 53 deletions

View File

@ -9,14 +9,20 @@ export const CreateButton: React.FC<ButtonProps> = props => (
</Button> </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 => ( 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 Delete
</Button> </Button>
); );
export const EditButton: React.FC<ButtonProps> = props => ( 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 Edit
</Button> </Button>
); );

View 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;

View File

@ -0,0 +1,3 @@
import BaseModal from './BaseModal';
export default BaseModal;

View File

@ -1,48 +1,147 @@
import React from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Button } from '@chakra-ui/button';
import { Text, Flex } from '@chakra-ui/react';
import { import {
Modal, Tag,
ModalOverlay, TagLabel,
ModalContent, TagCloseButton,
ModalHeader, Input,
ModalFooter, Box,
ModalBody, Flex,
ModalCloseButton, IconButton,
BoxProps,
InputGroup,
Text,
Stack,
InputRightElement,
Avatar,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useDisclosure, Divider } from '@chakra-ui/react'; import BaseModal from '@educt/components/Modal';
import { MdGroup } from 'react-icons/md'; 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 = { type AddStudentsModalPropsType = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; 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... &nbsp;
<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 }) => { const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({ isOpen, onClose }) => {
return ( return (
<> <BaseModal
<Modal isOpen={isOpen} onClose={onClose} size='md'> icon={MdGroup}
<ModalOverlay /> heading='Add new students'
<ModalContent> description='Attach new students to current course.'
<ModalHeader> confirmText='Add students'
<Flex alignItems='center'> isOpen={isOpen}
<MdGroup /> onClose={onClose}
<Text ml={2}>Add students</Text> >
</Flex> <SelectUsersInput />
</ModalHeader> </BaseModal>
<ModalCloseButton />
<Divider />
<ModalBody></ModalBody>
<ModalFooter>
<Button colorScheme='blue' variant='outline' onClick={onClose}>
Save
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
); );
}; };

View File

@ -10,6 +10,7 @@ import { IPageProps } from '@educt/interfaces';
*/ */
import { PageWrapper, PageHeading, PageContent } from '@educt/components/PageElements'; import { PageWrapper, PageHeading, PageContent } from '@educt/components/PageElements';
import { CreateButton } from '@educt/components/Buttons'; import { CreateButton } from '@educt/components/Buttons';
import { Box } from '@chakra-ui/layout';
import LoadingPage from '@educt/components/LoadingPage'; import LoadingPage from '@educt/components/LoadingPage';
import CourseList from './components/CourseList'; import CourseList from './components/CourseList';
import CategoryList from './components/CategoryList'; import CategoryList from './components/CategoryList';
@ -32,12 +33,17 @@ const CoursesPage: React.FC<IPageProps> = () => {
} = useRootStore(); } = useRootStore();
const history = useHistory(); const history = useHistory();
const handleCreateCourse = () => history.push('/courses/create');
if (me === null) return <LoadingPage />; if (me === null) return <LoadingPage />;
return ( return (
<PageWrapper> <PageWrapper>
<PageHeading heading='Courses' description='List of all courses' /> <PageHeading heading='Courses' description='List of all courses'>
{(me.isAdmin || me.isTeacher) && <CreateButton onClick={() => history.push('/courses/create')} />} <Box mt='5'>
{(me.isAdmin || me.isTeacher) && <CreateButton onClick={handleCreateCourse} ml='auto' display='block' />}
</Box>
</PageHeading>
<PageContent mt='10'> <PageContent mt='10'>
<CoursesTabs mb='7' /> <CoursesTabs mb='7' />
<CategoryList render={CategoryItem} /> <CategoryList render={CategoryItem} />

View File

@ -3,6 +3,7 @@ import { Table, Tbody, Button, Input, InputGroup, InputLeftElement, Flex, Box, T
import { MdSearch } from 'react-icons/md'; import { MdSearch } from 'react-icons/md';
import AddStudentsModal from '@educt/components/Modals/AddStudentsModal'; import AddStudentsModal from '@educt/components/Modals/AddStudentsModal';
import { AddButton } from '@educt/components/Buttons';
import BulkActionsMenu from './BulkActionsMenu'; import BulkActionsMenu from './BulkActionsMenu';
import StudentTableHead from './StudentTableHead'; import StudentTableHead from './StudentTableHead';
@ -17,8 +18,6 @@ import type { ICourse } from '@educt/interfaces';
*/ */
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useDisclosure } from '@chakra-ui/react'; import { useDisclosure } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import { CreateButton } from '@educt/components/Buttons';
type StudentTableListPropsType = { type StudentTableListPropsType = {
render: React.FC<StudentTableRowPropsType>; render: React.FC<StudentTableRowPropsType>;
@ -105,7 +104,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = ({ render: Row, st
<BulkActionsMenu selected={selected} rows={rows} setRows={setRows} /> <BulkActionsMenu selected={selected} rows={rows} setRows={setRows} />
</> </>
)} )}
<CreateButton onClick={onOpenAddStudentModal} /> <AddButton onClick={onOpenAddStudentModal} />
</Flex> </Flex>
</Flex> </Flex>
<Table overflow='hidden' borderRadius='lg' mt='2'> <Table overflow='hidden' borderRadius='lg' mt='2'>

View File

@ -24,7 +24,7 @@ import { useParams } from 'react-router-dom';
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
import { useFetchCourse } from '@educt/hooks/queries'; import { useFetchCourse } from '@educt/hooks/queries';
import { useDisclosure } from '@chakra-ui/hooks'; 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 { DeleteButton } from '@educt/components/Buttons';
import { StatusButton } from './components'; import { StatusButton } from './components';
@ -60,18 +60,12 @@ const EditCoursePage: React.FC<IPageProps> = () => {
</Helmet> </Helmet>
<DeleteCourseDialog course={course} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} /> <DeleteCourseDialog course={course} onClose={onCloseDeleteDialog} isOpen={isOpenDeleteDialog} />
<PageHeading heading='Course editor' description={course.title}>
<Flex justifyContent='space-between' alignItems='center' flexWrap='wrap'> <Flex mt='5' justifyContent='flex-end'>
<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' }}>
<StatusButton courseId={course.id} currentStatus={course.status} /> <StatusButton courseId={course.id} currentStatus={course.status} />
<DeleteButton onClick={onOpenDeleteDialog} ml='2' /> <DeleteButton onClick={onOpenDeleteDialog} ml='2' />
</Box> </Flex>
</Flex> </PageHeading>
<PageContent> <PageContent>
<Box> <Box>
<Tabs isLazy mt='8'> <Tabs isLazy mt='8'>

View File

@ -28,7 +28,7 @@ class UserService {
* @returns Array of users * @returns Array of users
*/ */
public async fetchAll(params?: FetchUsersParamsType): Promise<IApiRespose<IUser[]>> { 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', { const result = await this.api.get('/v1/users', {
params: { params: {
page, page,