mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: added students table list
This commit is contained in:
parent
ba514954e6
commit
093247899a
51
src/components/Modals/AddStudentsModal/AddSudentsModal.tsx
Normal file
51
src/components/Modals/AddStudentsModal/AddSudentsModal.tsx
Normal 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;
|
||||
3
src/components/Modals/AddStudentsModal/index.ts
Normal file
3
src/components/Modals/AddStudentsModal/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import AddStudentsModal from './AddSudentsModal';
|
||||
|
||||
export default AddStudentsModal;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,4 +0,0 @@
|
||||
import StudentList from './StudentList';
|
||||
import StudentItem from './StudentItem';
|
||||
|
||||
export { StudentList, StudentItem };
|
||||
@ -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;
|
||||
@ -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>
|
||||
));
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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);
|
||||
@ -0,0 +1,4 @@
|
||||
import StudentTableList from './StudentTableList';
|
||||
import StudentTableRow from './StudentTableRow';
|
||||
|
||||
export { StudentTableList, StudentTableRow };
|
||||
@ -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>
|
||||
|
||||
@ -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}"`);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user