mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
feat: added categories page
This commit is contained in:
parent
8b8651bda8
commit
1cefa219a9
@ -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;
|
||||
@ -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;
|
||||
26
src/components/Tables/CategoryTableList/CategoryTableRow.tsx
Normal file
26
src/components/Tables/CategoryTableList/CategoryTableRow.tsx
Normal 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;
|
||||
54
src/components/Tables/CategoryTableList/Cells/index.tsx
Normal file
54
src/components/Tables/CategoryTableList/Cells/index.tsx
Normal 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>
|
||||
);
|
||||
4
src/components/Tables/CategoryTableList/index.tsx
Normal file
4
src/components/Tables/CategoryTableList/index.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import CategoryTableList from './CategoryTableList';
|
||||
import CategoryTableRow from './CategoryTableRow';
|
||||
|
||||
export { CategoryTableList, CategoryTableRow };
|
||||
@ -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 },
|
||||
{
|
||||
|
||||
29
src/hooks/queries/category/useFetchCategories.ts
Normal file
29
src/hooks/queries/category/useFetchCategories.ts
Normal 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 };
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -75,6 +75,8 @@ export interface ICategory {
|
||||
title: string;
|
||||
description: string;
|
||||
color?: ColorType | undefined | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
26
src/pages/categories/categories.tsx
Normal file
26
src/pages/categories/categories.tsx
Normal 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;
|
||||
@ -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);
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user