feat: added students table list

This commit is contained in:
Sergey Yarkov 2021-12-05 23:11:27 +03:00
parent ba514954e6
commit 093247899a
14 changed files with 369 additions and 62 deletions

View File

@ -0,0 +1,51 @@
import React from 'react';
import { Button } from '@chakra-ui/button';
import { Text, Flex } from '@chakra-ui/react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
} from '@chakra-ui/react';
import { useDisclosure, Divider } from '@chakra-ui/react';
import { MdGroup } from 'react-icons/md';
type AddStudentsModalPropsType = {};
const AddStudentsModal: React.FC<AddStudentsModalPropsType> = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Button onClick={onOpen} mt='3' size='sm' colorScheme='blue' variant='outline'>
Add students
</Button>
<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>
</>
);
};
export default AddStudentsModal;

View File

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

View File

@ -23,10 +23,10 @@ export interface ICourse {
title: string;
description: string;
status: CourseStatusEnum;
teacher: Pick<IUser, 'id' | 'first_name' | 'fullname' | 'last_name' | 'email'>;
teacher: Pick<IUser, 'id' | 'first_name' | 'fullname' | 'last_name' | 'email' | 'created_at' | 'updated_at'>;
category: ICategory;
lessons: ILesson[];
students: Array<Pick<IUser, 'id' | 'first_name' | 'fullname' | 'last_name' | 'email'>>;
students: Array<Pick<IUser, 'id' | 'first_name' | 'fullname' | 'last_name' | 'email' | 'created_at' | 'updated_at'>>;
color: ColorType | null;
students_count: string;
likes_count: string;
@ -92,6 +92,8 @@ export interface IUser {
email: string;
roles: IUserRole[];
contacts: IUserContacts | null;
created_at: string;
updated_at: string;
}
/**

View File

@ -1,16 +0,0 @@
import React from 'react';
/**
* Types
*/
import { IUser } from '@educt/interfaces';
export type StudentItemPropsType = {
student: Pick<IUser, 'id' | 'first_name' | 'fullname' | 'last_name' | 'email'>;
};
const StudentItem: React.FC<StudentItemPropsType> = ({ student }) => {
return <div>{student.fullname}</div>;
};
export default StudentItem;

View File

@ -1,36 +0,0 @@
import React from 'react';
import { Stack, Box, Text, Button } from '@chakra-ui/react';
/**
* Types
*/
import { StudentItemPropsType } from './StudentItem';
import { ICourse } from '@educt/interfaces';
type StudentListPropsType = {
render: React.FC<StudentItemPropsType>;
students: ICourse['students'];
};
const StudentList: React.FC<StudentListPropsType> = ({ render: Item, students }) => {
return (
<Box>
{students.length !== 0 ? (
<Stack>
{students.map(student => (
<Item key={student.id} student={student} />
))}
</Stack>
) : (
<Box textAlign='center' mt='10'>
<Text>There are no students in the course yet</Text>
<Button mt='3' size='sm' colorScheme='blue' variant='outline'>
Add student
</Button>
</Box>
)}
</Box>
);
};
export default StudentList;

View File

@ -1,4 +0,0 @@
import StudentList from './StudentList';
import StudentItem from './StudentItem';
export { StudentList, StudentItem };

View File

@ -0,0 +1,45 @@
import React from 'react';
import { Button, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react';
import { ChevronDownIcon, DeleteIcon } from '@chakra-ui/icons';
import { MdRemove, MdImportExport } from 'react-icons/md';
import type { ICourse } from '@educt/interfaces';
type BulkActionsMenuPropsType = {
selected: ICourse['students'];
rows: ICourse['students'];
setRows: React.Dispatch<React.SetStateAction<ICourse['students']>>;
};
const BulkActionsMenu: React.FC<BulkActionsMenuPropsType> = ({ selected, rows, setRows }) => {
const handleDetach = async () => {
console.log(
`Detaching from course: `,
selected.map(s => s.id)
);
setRows(rows.filter(r => selected.every(s => s.id !== r.id)));
};
const handleDelete = async () => {};
const handleExport = async () => {};
return (
<Menu isLazy>
<MenuButton as={Button} mr='3' ml='auto' rightIcon={<ChevronDownIcon />}>
Bulk Actions
</MenuButton>
<MenuList>
<MenuItem onClick={handleDetach} icon={<MdRemove />}>
Remove from course
</MenuItem>
<MenuItem onClick={handleExport} icon={<MdImportExport />} isDisabled>
Export
</MenuItem>
<MenuItem onClick={handleDelete} icon={<DeleteIcon />} color='red' isDisabled>
Delete
</MenuItem>
</MenuList>
</Menu>
);
};
export default BulkActionsMenu;

View File

@ -0,0 +1,68 @@
import React, { memo } from 'react';
import { Link as ReactRouterLink } from 'react-router-dom';
import { Flex, Box, Avatar, Td, Checkbox, Link, Text, IconButton } from '@chakra-ui/react';
import { Menu, MenuList, MenuButton, MenuItem } from '@chakra-ui/react';
import { MdEdit, MdMoreHoriz, MdRemove } from 'react-icons/md';
import { DeleteIcon } from '@chakra-ui/icons';
/**
* Types
*/
import type { StudentTableRowPropsType } from '../StudentTableRow';
/**
* Hooks
*/
import { useColorModeValue } from '@chakra-ui/react';
import { IUser } from '@educt/interfaces';
type CheckBoxCellPropsType = Pick<StudentTableRowPropsType, 'isSelected' | 'onSelect' | 'student'>;
export const CheckBoxCell: React.FC<CheckBoxCellPropsType> = ({ isSelected, onSelect, student }) => (
<Td>
<Checkbox isChecked={isSelected} onChange={e => onSelect(e, student)} />
</Td>
);
type InfoCellPropsType = Pick<IUser, 'id' | 'fullname' | 'email'>;
export const InfoCell: React.FC<InfoCellPropsType> = memo(({ id, fullname, email }) => (
<Td>
<Flex alignItems='center' flexDir={{ base: 'column', sm: 'row' }} flex='1 1 0'>
<Avatar name={fullname} size='sm' mr={{ base: '0', sm: '3' }} mb={{ base: '1', sm: '0' }} />
<Box textAlign={{ base: 'center', sm: 'left' }}>
<Link
color={useColorModeValue('gray.700', 'gray.100')}
fontWeight='medium'
as={ReactRouterLink}
to={`/user/${id}`}
fontSize='md'
>
{fullname}
</Link>
<Text color='gray.500' fontSize='sm' overflowWrap='anywhere'>
{email}
</Text>
</Box>
</Flex>
</Td>
));
type RegisteredCellPropsType = { registered: string };
export const RegisteredCell: React.FC<RegisteredCellPropsType> = memo(({ registered }) => (
<Td>{new Date(registered).toLocaleDateString()}</Td>
));
type ActionsCellPropsType = Pick<IUser, 'id'>;
export const ActionsCell: React.FC<ActionsCellPropsType> = memo(({ id }) => (
<Td textAlign='center'>
<Menu isLazy>
<MenuButton as={IconButton} aria-label='Actions' icon={<MdMoreHoriz />} variant='ghost' />
<MenuList>
<MenuItem icon={<MdEdit />}>Edit</MenuItem>
<MenuItem icon={<MdRemove />}>Remove from course</MenuItem>
<MenuItem icon={<DeleteIcon />} color='red.400'>
Delete
</MenuItem>
</MenuList>
</Menu>
</Td>
));

View File

@ -0,0 +1,44 @@
import React from 'react';
import { Icon, Flex, Checkbox, Text } from '@chakra-ui/react';
import { Thead, Tr, Th } from '@chakra-ui/react';
import { MdSettings, MdSupervisorAccount } from 'react-icons/md';
/**
* Hooks
*/
import { useColorModeValue } from '@chakra-ui/react';
type StudentTableHeadPropsType = {
onSelectAll?: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
const StudentTableHead: React.FC<StudentTableHeadPropsType> = ({ onSelectAll }) => {
return (
<Thead bg={useColorModeValue('gray.50', 'gray.700')}>
<Tr>
<Th width='12px'>
<Checkbox onChange={onSelectAll} />
</Th>
<Th>
<Flex alignItems='center'>
<Icon as={MdSupervisorAccount} w={4} h={4} />
<Text as='span' ml='1'>
Name
</Text>
</Flex>
</Th>
<Th>Registered</Th>
<Th width='20px'>
<Flex>
<Icon as={MdSettings} w={4} h={4} />
<Text as='span' ml='1'>
Actions
</Text>
</Flex>
</Th>
</Tr>
</Thead>
);
};
export default StudentTableHead;

View File

@ -0,0 +1,119 @@
import React from 'react';
import { Table, Tbody, Button, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react';
import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react';
import { Flex, Box, Text } from '@chakra-ui/react';
import { DeleteIcon, ChevronDownIcon } from '@chakra-ui/icons';
import { MdSearch, MdImportExport, MdRemove } from 'react-icons/md';
import AddStudentsModal from '@educt/components/Modals/AddStudentsModal';
import StudentTableHead from './StudentTableHead';
/**
* Types
*/
import type { StudentTableRowPropsType } from './StudentTableRow';
import type { ICourse } from '@educt/interfaces';
/**
* Hooks
*/
import { useCallback, useState } from 'react';
import BulkActionsMenu from './BulkActionsMenu';
type StudentTableListPropsType = {
render: React.FC<StudentTableRowPropsType>;
students: ICourse['students'];
};
const StudentTableList: React.FC<StudentTableListPropsType> = ({ render: Row, students }) => {
const [rows, setRows] = useState<ICourse['students']>(students);
const [selected, setSelected] = useState<ICourse['students']>([]);
const [search, setSearch] = useState<string>('');
/**
* Search for a student by input
*/
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
setRows([
...students.filter(
s => s.fullname.toLowerCase().includes(e.target.value) || s.email.toLowerCase().includes(e.target.value)
),
]);
};
/**
* Select all students in a table
*/
const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
setSelected(rows);
return;
}
setSelected([]);
};
/**
* Select a student with checkbox
*/
const handleSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>, student: ICourse['students'][number]) => {
if (e.target.checked) {
setSelected(prev => [...prev, student]);
return;
}
setSelected(prev => prev.filter(s => s.id !== student.id));
}, []);
if (rows.length === 0) {
return (
<Box textAlign='center' mt='10'>
<Text>There are no students in the course yet</Text>
<AddStudentsModal />
</Box>
);
}
return (
<Box>
<Flex justifyContent='space-between' flexDir={{ base: 'column', lg: 'row' }}>
<Flex mb='2'>
<InputGroup mr='2'>
<InputLeftElement pointerEvents='none' children={<MdSearch color='gray.300' />} />
<Input value={search} onChange={handleSearch} type='text' placeholder='Search for student...' />
</InputGroup>
</Flex>
<Flex alignItems='center' justifyContent='space-between' pr={{ base: '8px', lg: '0' }}>
{selected.length !== 0 && (
<>
<Text mr='4'>
You have selected <b>{selected.length}</b> student(s).
</Text>
<BulkActionsMenu selected={selected} rows={rows} setRows={setRows} />
</>
)}
<Button variant='outline' colorScheme='blue'>
Add new
</Button>
</Flex>
</Flex>
<Table overflowX='hidden' borderRadius='lg' mt='2'>
<StudentTableHead onSelectAll={handleSelectAll} />
<Tbody>
{rows.map(student => (
<Row
key={student.id}
student={student}
isSelected={!!selected.find(s => s.id === student.id)}
onSelect={handleSelect}
/>
))}
</Tbody>
</Table>
</Box>
);
};
export default StudentTableList;

View File

@ -0,0 +1,27 @@
import React, { memo } from 'react';
import { Tr } from '@chakra-ui/react';
/**
* Cells
*/
import { CheckBoxCell, InfoCell, RegisteredCell, ActionsCell } from './Cells';
import { ICourse } from '@educt/interfaces';
export type StudentTableRowPropsType = {
isSelected: boolean;
student: ICourse['students'][number];
onSelect: (e: React.ChangeEvent<HTMLInputElement>, student: ICourse['students'][number]) => void;
};
const StudentTableItem: React.FC<StudentTableRowPropsType> = ({ student, isSelected, onSelect }) => {
return (
<Tr>
<CheckBoxCell student={student} isSelected={isSelected} onSelect={onSelect} />
<InfoCell id={student.id} fullname={student.fullname} email={student.email} />
<RegisteredCell registered={student.created_at} />
<ActionsCell id={student.id} />
</Tr>
);
};
export default memo(StudentTableItem);

View File

@ -0,0 +1,4 @@
import StudentTableList from './StudentTableList';
import StudentTableRow from './StudentTableRow';
export { StudentTableList, StudentTableRow };

View File

@ -15,9 +15,9 @@ import type { IPageProps } from '@educt/interfaces';
import LoadingPage from '@educt/components/LoadingPage';
import { EditCourseForm } from '@educt/components/Forms/CourseForm';
import SetCourseStatusButton from '@educt/components/SetCourseStatusButton';
import LessonList from './components/LessonList';
import { StudentList, StudentItem } from './components/StudentList';
import DeleteCourseDialog from '@educt/components/Dialogs/DeleteCourseDialog';
import { StudentTableList, StudentTableRow } from './components/StudentTableList';
import LessonList from './components/LessonList';
/**
* Hooks
@ -96,7 +96,7 @@ const EditCoursePage: React.FC<IPageProps> = () => {
<LessonList course={course} />
</TabPanel>
<TabPanel>
<StudentList render={StudentItem} students={course.students} />
<StudentTableList render={StudentTableRow} students={course.students} />
</TabPanel>
</TabPanels>
</Tabs>

View File

@ -35,7 +35,7 @@ export default class UIStore {
}
public setLocation(location: string): void {
console.log(`[history]: location changed to "${location}"`);
this.location = location;
console.info(`[history]: location changed to "${location}"`);
}
}