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>
|
</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>
|
||||||
);
|
);
|
||||||
|
|||||||
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 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...
|
||||||
|
<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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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} />
|
||||||
|
|||||||
@ -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'>
|
||||||
|
|||||||
@ -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'>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user