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,
|
MdOutlineVideoLibrary,
|
||||||
MdOutlineGroup,
|
MdOutlineGroup,
|
||||||
MdOutlineAccountCircle,
|
MdOutlineAccountCircle,
|
||||||
|
MdOutlineCategory,
|
||||||
} from 'react-icons/md';
|
} from 'react-icons/md';
|
||||||
import { IAppConfig } from './interfaces';
|
import { IAppConfig } from './interfaces';
|
||||||
import packageJson from '../package.json';
|
import packageJson from '../package.json';
|
||||||
@ -33,6 +34,13 @@ const config: IAppConfig = {
|
|||||||
icon: MdOutlineVideoLibrary,
|
icon: MdOutlineVideoLibrary,
|
||||||
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
|
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: '/messages', title: 'Messages', public: true, icon: MdOutlineMessage },
|
||||||
{ location: '/profile', title: 'Profile', public: true, icon: MdOutlineAccountCircle },
|
{ 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 { useUpdateUser } from './user/useUpdateUser';
|
||||||
import { useUpdateUserEmail } from './user/useUpdateUserEmail';
|
import { useUpdateUserEmail } from './user/useUpdateUserEmail';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category
|
||||||
|
*/
|
||||||
|
import { useFetchCategories } from './category/useFetchCategories';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useLogin,
|
useLogin,
|
||||||
useLogout,
|
useLogout,
|
||||||
@ -45,4 +50,5 @@ export {
|
|||||||
useDeleteUser,
|
useDeleteUser,
|
||||||
useUpdateUser,
|
useUpdateUser,
|
||||||
useUpdateUserEmail,
|
useUpdateUserEmail,
|
||||||
|
useFetchCategories,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -75,6 +75,8 @@ export interface ICategory {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
color?: ColorType | undefined | null;
|
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
|
* Hooks
|
||||||
*/
|
*/
|
||||||
import { useContext, useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSetCourseStatus } from '@educt/hooks/queries';
|
import { useSetCourseStatus } from '@educt/hooks/queries';
|
||||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
import { useDisclosure } from '@chakra-ui/hooks';
|
import { useDisclosure } from '@chakra-ui/hooks';
|
||||||
@ -25,7 +25,6 @@ import { useDisclosure } from '@chakra-ui/hooks';
|
|||||||
/**
|
/**
|
||||||
* Contexts
|
* Contexts
|
||||||
*/
|
*/
|
||||||
import { CoursesPageContext } from '@educt/contexts';
|
|
||||||
import { ICourse } from '@educt/interfaces';
|
import { ICourse } from '@educt/interfaces';
|
||||||
|
|
||||||
type CourseListPropsType = {
|
type CourseListPropsType = {
|
||||||
@ -41,7 +40,7 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
|
|||||||
const [deleting, setDeleting] = useState<Pick<ICourse, 'id' | 'title'> | null>(null);
|
const [deleting, setDeleting] = useState<Pick<ICourse, 'id' | 'title'> | null>(null);
|
||||||
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
|
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
|
||||||
const { setCourseStatus } = useSetCourseStatus();
|
const { setCourseStatus } = useSetCourseStatus();
|
||||||
const { courses, isLoading, loadCourses } = courseStore;
|
const { courses, isLoading } = courseStore;
|
||||||
|
|
||||||
const handleDeleteCourse = (course: Pick<ICourse, 'id' | 'title'>) => {
|
const handleDeleteCourse = (course: Pick<ICourse, 'id' | 'title'>) => {
|
||||||
setDeleting(course);
|
setDeleting(course);
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export const StatusButton: React.FC<IStatusButtonProps> = props => {
|
|||||||
setStatus(newStatus);
|
setStatus(newStatus);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Components: { [k in keyof typeof CourseStatusEnum]: any } = {
|
const Components: { [k in keyof typeof CourseStatusEnum]: React.ReactElement } = {
|
||||||
PUBLISHED: (
|
PUBLISHED: (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleChangeStatus(CourseStatusEnum.DRAFT)}
|
onClick={() => handleChangeStatus(CourseStatusEnum.DRAFT)}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import NotFoundPage from '@educt/pages/404/404';
|
|||||||
import CreateCoursePage from '@educt/pages/courses/create/create';
|
import CreateCoursePage from '@educt/pages/courses/create/create';
|
||||||
import CreateLessonPage from '@educt/pages/courses/create-lesson/create-lesson';
|
import CreateLessonPage from '@educt/pages/courses/create-lesson/create-lesson';
|
||||||
import EditLessonPage from './pages/lessons/edit-lesson/edit-lesson';
|
import EditLessonPage from './pages/lessons/edit-lesson/edit-lesson';
|
||||||
|
import CategoriesPage from './pages/categories/categories';
|
||||||
import CoursePage from '@educt/pages/course/course';
|
import CoursePage from '@educt/pages/course/course';
|
||||||
import EditCoursePage from '@educt/pages/courses/edit/edit';
|
import EditCoursePage from '@educt/pages/courses/edit/edit';
|
||||||
import ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
|
import ChangePasswordPage from '@educt/pages/profile/change-password/change-password';
|
||||||
@ -48,6 +49,14 @@ export const routes: IRouteOption[] = [
|
|||||||
exact: true,
|
exact: true,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/categories',
|
||||||
|
title: 'Categories',
|
||||||
|
component: CategoriesPage,
|
||||||
|
exact: true,
|
||||||
|
isPrivate: true,
|
||||||
|
roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/courses/create',
|
path: '/courses/create',
|
||||||
title: 'Create new course',
|
title: 'Create new course',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user