feat: added categories page

This commit is contained in:
Sergey Yarkov 2022-01-18 22:40:17 +03:00
parent 8b8651bda8
commit 1cefa219a9
13 changed files with 272 additions and 4 deletions

View File

@ -0,0 +1,41 @@
import React from 'react';
import { Icon, Flex, Text } from '@chakra-ui/react';
import { Thead, Tr, Th } from '@chakra-ui/react';
import { MdCategory, MdColorLens, MdSettings } from 'react-icons/md';
/**
* Hooks
*/
import { useColorModeValue } from '@chakra-ui/react';
const CategoryTableHead: React.FC = () => {
return (
<Thead bg={useColorModeValue('gray.50', 'gray.700')}>
<Tr>
<Th width='4px'>
<Icon as={MdColorLens} w={4} h={4} />
</Th>
<Th>
<Flex alignItems='center'>
<Icon as={MdCategory} w={4} h={4} />
<Text as='span' ml='1'>
Name
</Text>
</Flex>
</Th>
<Th>Created</Th>
<Th>Last update</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 CategoryTableHead;

View File

@ -0,0 +1,64 @@
import React from 'react';
import { Flex, Box, Text, Table, Tbody } from '@chakra-ui/react';
import { CreateButton } from '@educt/components/Buttons';
import { ICategory } from '@educt/interfaces';
import CategoryTableHead from './CategoryTableHead';
import { CategoryTableRowPropsType } from './CategoryTableRow';
/**
* Hooks
*/
import { useState } from 'react';
type CategoryTableListPropsType = {
render: React.FC<CategoryTableRowPropsType>;
categories: ICategory[];
};
const CategoryTableList: React.FC<CategoryTableListPropsType> = props => {
const { render: Row, categories } = props;
const [rows, setRows] = useState<ICategory[]>(categories);
const isEmptyRows = rows.length === 0;
// TODO make delete query
const handleDelete = () => () => undefined;
// TODO make edit query
const handleEdit = () => () => undefined;
return (
<Box>
<Flex justifyContent={'flex-end'}>
<CreateButton />
</Flex>
<Table borderRadius='lg' mt='2'>
<CategoryTableHead />
<Tbody>
{rows.map(category => (
<Row
key={category.id}
category={category}
actions={{
onDelete: handleDelete,
onEdit: handleEdit,
}}
/>
))}
</Tbody>
</Table>
{isEmptyRows && (
<Box textAlign='center' m='6'>
<Text color='gray.500' userSelect='none'>
Cannot find any category.
</Text>
</Box>
)}
<Text mt='4' fontSize='sm' color='gray.500'>
{rows.length} categories
</Text>
</Box>
);
};
export default CategoryTableList;

View File

@ -0,0 +1,26 @@
import React from 'react';
import { ICategory } from '@educt/interfaces';
import { Tr } from '@chakra-ui/react';
import { ActionsCell, ColorCell, CreatedCell, NameCell, UpdatedCell } from './Cells';
export type CategoryTableRowPropsType = {
category: ICategory;
actions: {
onEdit: (category: ICategory) => () => void;
onDelete: (id: string) => () => void;
};
};
const CategoryTableRow: React.FC<CategoryTableRowPropsType> = ({ category, actions }) => {
return (
<Tr>
<ColorCell color={category.color} />
<NameCell name={category.title} />
<CreatedCell created={category.created_at} />
<UpdatedCell updated={category.updated_at} />
<ActionsCell onEdit={actions.onEdit(category)} onDelete={actions.onDelete(category.id)} />
</Tr>
);
};
export default CategoryTableRow;

View File

@ -0,0 +1,54 @@
import React from 'react';
import { Box, Td, IconButton } from '@chakra-ui/react';
import { Menu, MenuList, MenuButton, MenuItem } from '@chakra-ui/react';
import { MdEdit, MdMoreHoriz } from 'react-icons/md';
import { DeleteIcon } from '@chakra-ui/icons';
import { ICategory } from '@educt/interfaces';
type ColorCellPropsType = {
color: ICategory['color'];
};
export const ColorCell: React.FC<ColorCellPropsType> = ({ color }) => (
<Td>
<Box backgroundColor={color?.hex || 'blue.500'} borderRadius={'3px'} h='20px' w='20px' />
</Td>
);
type NameCellPropsType = {
name: string;
};
export const NameCell: React.FC<NameCellPropsType> = ({ name }) => <Td>{name}</Td>;
type CreatedCellPropsType = {
created: string;
};
export const CreatedCell: React.FC<CreatedCellPropsType> = ({ created }) => (
<Td>{new Date(created).toLocaleDateString()}</Td>
);
type UpdatedCellPropsType = {
updated: string;
};
export const UpdatedCell: React.FC<UpdatedCellPropsType> = ({ updated }) => (
<Td>{new Date(updated).toLocaleDateString()}</Td>
);
type ActionsCellPropsType = {
onEdit: () => void;
onDelete: () => void;
};
export const ActionsCell: React.FC<ActionsCellPropsType> = ({ onEdit, onDelete }) => (
<Td textAlign='center'>
<Menu isLazy>
<MenuButton as={IconButton} aria-label='Actions' icon={<MdMoreHoriz />} variant='ghost' />
<MenuList>
<MenuItem onClick={onEdit} icon={<MdEdit />}>
Edit
</MenuItem>
<MenuItem onClick={onDelete} icon={<DeleteIcon />} color='red.400'>
Delete
</MenuItem>
</MenuList>
</Menu>
</Td>
);

View File

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

View File

@ -6,6 +6,7 @@ import {
MdOutlineVideoLibrary,
MdOutlineGroup,
MdOutlineAccountCircle,
MdOutlineCategory,
} from 'react-icons/md';
import { IAppConfig } from './interfaces';
import packageJson from '../package.json';
@ -33,6 +34,13 @@ const config: IAppConfig = {
icon: MdOutlineVideoLibrary,
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
},
{
location: '/categories',
title: 'Categories',
public: false,
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
icon: MdOutlineCategory,
},
{ location: '/messages', title: 'Messages', public: true, icon: MdOutlineMessage },
{ location: '/profile', title: 'Profile', public: true, icon: MdOutlineAccountCircle },
{

View File

@ -0,0 +1,29 @@
import useAsync from '@educt/hooks/useAsync';
import { ICategory } from '@educt/interfaces';
import { CategoryServiceInstance } from '@educt/services';
import { useEffect } from 'react';
import { useErrorHandler } from 'react-error-boundary';
const useFetchCategories = () => {
const handleError = useErrorHandler();
const fetchCategories = async () => {
try {
const result = await CategoryServiceInstance.fetchAll();
return result.data;
} catch (error) {
handleError(error);
return Promise.reject(error);
}
};
const { execute: fetch, ...state } = useAsync<ICategory[], Parameters<typeof fetchCategories>>(fetchCategories);
useEffect(() => {
fetch();
}, []);
return { ...state };
};
export { useFetchCategories };

View File

@ -29,6 +29,11 @@ import { useDeleteUser } from './user/useDeleteUser';
import { useUpdateUser } from './user/useUpdateUser';
import { useUpdateUserEmail } from './user/useUpdateUserEmail';
/**
* Category
*/
import { useFetchCategories } from './category/useFetchCategories';
export {
useLogin,
useLogout,
@ -45,4 +50,5 @@ export {
useDeleteUser,
useUpdateUser,
useUpdateUserEmail,
useFetchCategories,
};

View File

@ -75,6 +75,8 @@ export interface ICategory {
title: string;
description: string;
color?: ColorType | undefined | null;
created_at: string;
updated_at: string;
}
/**

View File

@ -0,0 +1,26 @@
import React from 'react';
import { IPageProps } from '@educt/interfaces';
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
import { CategoryTableList, CategoryTableRow } from '@educt/components/Tables/CategoryTableList';
import { useFetchCategories } from '@educt/hooks/queries';
import LoadingPage from '@educt/components/LoadingPage';
/**
* Categories page
*/
const CategoriesPage: React.FC<IPageProps> = () => {
const { isLoading, data: categories } = useFetchCategories();
if (isLoading || categories === null) return <LoadingPage />;
return (
<PageWrapper>
<PageHeading heading='Categories' description='Edit categories in the system.' />
<PageContent>
<CategoryTableList categories={categories} render={CategoryTableRow} />
</PageContent>
</PageWrapper>
);
};
export default CategoriesPage;

View File

@ -17,7 +17,7 @@ import LoadingList from '@educt/components/LoadingList';
/**
* Hooks
*/
import { useContext, useEffect } from 'react';
import { useEffect } from 'react';
import { useSetCourseStatus } from '@educt/hooks/queries';
import { useRootStore } from '@educt/hooks/useRootStore';
import { useDisclosure } from '@chakra-ui/hooks';
@ -25,7 +25,6 @@ import { useDisclosure } from '@chakra-ui/hooks';
/**
* Contexts
*/
import { CoursesPageContext } from '@educt/contexts';
import { ICourse } from '@educt/interfaces';
type CourseListPropsType = {
@ -41,7 +40,7 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
const [deleting, setDeleting] = useState<Pick<ICourse, 'id' | 'title'> | null>(null);
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
const { setCourseStatus } = useSetCourseStatus();
const { courses, isLoading, loadCourses } = courseStore;
const { courses, isLoading } = courseStore;
const handleDeleteCourse = (course: Pick<ICourse, 'id' | 'title'>) => {
setDeleting(course);

View File

@ -24,7 +24,7 @@ export const StatusButton: React.FC<IStatusButtonProps> = props => {
setStatus(newStatus);
};
const Components: { [k in keyof typeof CourseStatusEnum]: any } = {
const Components: { [k in keyof typeof CourseStatusEnum]: React.ReactElement } = {
PUBLISHED: (
<Button
onClick={() => handleChangeStatus(CourseStatusEnum.DRAFT)}

View File

@ -9,6 +9,7 @@ import NotFoundPage from '@educt/pages/404/404';
import CreateCoursePage from '@educt/pages/courses/create/create';
import CreateLessonPage from '@educt/pages/courses/create-lesson/create-lesson';
import EditLessonPage from './pages/lessons/edit-lesson/edit-lesson';
import CategoriesPage from './pages/categories/categories';
import CoursePage from '@educt/pages/course/course';
import EditCoursePage from '@educt/pages/courses/edit/edit';
import ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
@ -48,6 +49,14 @@ export const routes: IRouteOption[] = [
exact: true,
isPrivate: true,
},
{
path: '/categories',
title: 'Categories',
component: CategoriesPage,
exact: true,
isPrivate: true,
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
},
{
path: '/courses/create',
title: 'Create new course',