mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: some refactor & added draggable lesson
This commit is contained in:
parent
aa5bf76cc7
commit
cc5581bbc4
5
package-lock.json
generated
5
package-lock.json
generated
@ -2101,6 +2101,11 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"react-movable": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-movable/-/react-movable-3.0.2.tgz",
|
||||
"integrity": "sha512-dDDYm3CRnDy8YLXMyyaR2MbcQiTwhPOP+dfl3fZukiI6mN1flVatcjSozT7HXjVk2yHwBC67ZOWGVAmjY6F/dA=="
|
||||
},
|
||||
"react-refresh": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz",
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.12.2",
|
||||
"react-icons": "^3.11.0",
|
||||
"react-movable": "^3.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-spinners": "^0.11.0",
|
||||
"validator": "^13.6.0",
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai';
|
||||
import { CourseStatusEnum } from '@educt/enums';
|
||||
import useSetCourseStatusQuery from '@educt/hooks/useSetCourseStatusQuery';
|
||||
|
||||
type SetCourseStatusButtonPropsType = {
|
||||
courseId: string;
|
||||
currentStatus: CourseStatusEnum;
|
||||
loadingText?: string | undefined;
|
||||
};
|
||||
|
||||
const SetCourseStatusButton: React.FC<SetCourseStatusButtonPropsType> = ({
|
||||
courseId,
|
||||
currentStatus,
|
||||
loadingText = 'Wait a second...',
|
||||
}) => {
|
||||
const [status, setStatus] = useState<keyof typeof CourseStatusEnum>(currentStatus);
|
||||
const { setCourseStatus, isLoading } = useSetCourseStatusQuery();
|
||||
|
||||
const handleChangeStatus = async (newStatus: CourseStatusEnum) => {
|
||||
await setCourseStatus(courseId, newStatus);
|
||||
setStatus(newStatus);
|
||||
};
|
||||
|
||||
const Components: { [k in keyof typeof CourseStatusEnum]: any } = {
|
||||
PUBLISHED: (
|
||||
<Button
|
||||
onClick={() => handleChangeStatus(CourseStatusEnum.DRAFT)}
|
||||
isLoading={isLoading}
|
||||
loadingText={loadingText}
|
||||
leftIcon={<AiOutlineEyeInvisible />}
|
||||
>
|
||||
Mark as Draft
|
||||
</Button>
|
||||
),
|
||||
DRAFT: (
|
||||
<Button
|
||||
onClick={() => handleChangeStatus(CourseStatusEnum.PUBLISHED)}
|
||||
isLoading={isLoading}
|
||||
loadingText={loadingText}
|
||||
leftIcon={<AiOutlineEye />}
|
||||
>
|
||||
Publish course
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
return Components[status];
|
||||
};
|
||||
|
||||
export default SetCourseStatusButton;
|
||||
3
src/components/SetCourseStatusButton/index.ts
Normal file
3
src/components/SetCourseStatusButton/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import SetCourseStatusButton from './SetCourseStatusButton';
|
||||
|
||||
export default SetCourseStatusButton;
|
||||
49
src/hooks/useSetCourseStatusQuery.ts
Normal file
49
src/hooks/useSetCourseStatusQuery.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { CourseStatusEnum } from '@educt/enums';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
import useAsync from './useAsync';
|
||||
import { useRootStore } from './useRootStore';
|
||||
import { useToast } from '@chakra-ui/toast';
|
||||
|
||||
type QueryResponseDataType = {} | undefined;
|
||||
export type SetCourseStatusFnType = (
|
||||
id: string,
|
||||
newStatus: CourseStatusEnum
|
||||
) => Promise<QueryResponseDataType | undefined>;
|
||||
|
||||
const useSetCourseStatusQuery = () => {
|
||||
const { courseStore } = useRootStore();
|
||||
const toast = useToast();
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
const setCourseStatus = async (id: string, newStatus: CourseStatusEnum) => {
|
||||
try {
|
||||
const result = await courseStore.setCourseStatus(id, newStatus);
|
||||
switch (newStatus) {
|
||||
case CourseStatusEnum.PUBLISHED:
|
||||
toast({ title: 'Course has been published.', status: 'success' });
|
||||
break;
|
||||
case CourseStatusEnum.DRAFT:
|
||||
toast({ title: 'Course has been marked as draft.', status: 'info' });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return result.data;
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
toast({ title: error.message, status: 'error' });
|
||||
} else {
|
||||
handleError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { execute, ...state } = useAsync<QueryResponseDataType, Parameters<SetCourseStatusFnType>>(setCourseStatus);
|
||||
|
||||
return { setCourseStatus: execute, ...state };
|
||||
};
|
||||
|
||||
export default useSetCourseStatusQuery;
|
||||
@ -50,6 +50,7 @@ export interface ICategory {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
color?: ColorType | undefined | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -22,6 +22,7 @@ export type CategoryItemPropsType = {
|
||||
|
||||
const CategoryItem: React.FC<CategoryItemPropsType> = ({ category }) => {
|
||||
const { selectedCategory, setSelectedCategory } = useContext(CoursesPageContext);
|
||||
const isSelected = selectedCategory?.id === category.id;
|
||||
|
||||
/**
|
||||
* Handle click on change category
|
||||
@ -31,10 +32,12 @@ const CategoryItem: React.FC<CategoryItemPropsType> = ({ category }) => {
|
||||
return (
|
||||
<Tag
|
||||
borderRadius='full'
|
||||
variant={selectedCategory?.id === category.id ? 'solid' : 'outline'}
|
||||
colorScheme='blue'
|
||||
variant={isSelected ? 'solid' : 'outline'}
|
||||
boxShadow={`inset 0 0 0px 1px ${category.color?.hex}`}
|
||||
color={isSelected ? 'white' : category.color?.hex}
|
||||
cursor='pointer'
|
||||
transition='all .1s'
|
||||
bg={isSelected ? `${category.color?.hex}` : 'none'}
|
||||
_hover={{ opacity: '.8' }}
|
||||
onClick={onCategoryChange}
|
||||
>
|
||||
|
||||
@ -13,6 +13,7 @@ import { DeleteIcon, EditIcon } from '@chakra-ui/icons';
|
||||
*/
|
||||
import { ICourse } from '@educt/interfaces';
|
||||
import { CourseStatusEnum } from '@educt/enums';
|
||||
import { SetCourseStatusFnType } from '@educt/hooks/useSetCourseStatusQuery';
|
||||
|
||||
/**
|
||||
* Components
|
||||
@ -29,7 +30,7 @@ import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
export type CourseItemPropsType = {
|
||||
course: Omit<ICourse, 'teacher' | 'students' | 'lessons'>;
|
||||
onDelete: (course: Pick<ICourse, 'id' | 'title'>) => void;
|
||||
onSetStatus: (id: string, status: CourseStatusEnum) => Promise<void>;
|
||||
onSetStatus: SetCourseStatusFnType;
|
||||
};
|
||||
|
||||
const CourseItem: React.FC<CourseItemPropsType> = ({ course, onDelete, onSetStatus }) => {
|
||||
|
||||
@ -20,13 +20,12 @@ import LoadingList from '@educt/components/LoadingList';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||
import { useDisclosure } from '@chakra-ui/hooks';
|
||||
import { useToast } from '@chakra-ui/toast';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
|
||||
/**
|
||||
* Contexts
|
||||
*/
|
||||
import { CoursesPageContext } from '@educt/contexts';
|
||||
import useSetCourseStatusQuery from '@educt/hooks/useSetCourseStatusQuery';
|
||||
|
||||
type CourseListPropsType = {
|
||||
render: React.FC<CourseItemPropsType>;
|
||||
@ -39,8 +38,7 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
|
||||
} = useRootStore();
|
||||
const { selectedCategory, courseStatus, deletingCourse, setDeletingCourse } = useContext(CoursesPageContext);
|
||||
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
|
||||
const handleError = useErrorHandler();
|
||||
const toast = useToast();
|
||||
const { setCourseStatus } = useSetCourseStatusQuery();
|
||||
const { courses, isLoading } = courseStore;
|
||||
|
||||
/**
|
||||
@ -55,22 +53,6 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
|
||||
}
|
||||
}, [courseStore, selectedCategory, courseStatus]);
|
||||
|
||||
/**
|
||||
* Handle update course status
|
||||
*/
|
||||
const onSetStatus = async (id: string, status: CourseStatusEnum) => {
|
||||
try {
|
||||
await courseStore.setCourseStatus(id, status);
|
||||
toast({ title: 'Status updated.', status: 'info' });
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
toast({ title: error.message, status: 'error' });
|
||||
} else {
|
||||
handleError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (courses === null || isLoading) return <LoadingList />;
|
||||
|
||||
return (
|
||||
@ -88,7 +70,7 @@ const CourseList: React.FC<CourseListPropsType> = ({ render: Item }) => {
|
||||
setDeletingCourse(course);
|
||||
onOpenDeleteDialog();
|
||||
}}
|
||||
onSetStatus={onSetStatus}
|
||||
onSetStatus={setCourseStatus}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@ -1,91 +0,0 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { Flex, Box, Text, Icon, Button, IconButton } from '@chakra-ui/react';
|
||||
import { DeleteIcon } from '@chakra-ui/icons';
|
||||
import { MdTimer, MdAttachment } from 'react-icons/md';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import type { ILesson } from '@educt/interfaces';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useHistory } from 'react-router';
|
||||
import { useColorModeValue } from '@chakra-ui/color-mode';
|
||||
|
||||
export type LessonItemPropsType = {
|
||||
lesson: ILesson;
|
||||
};
|
||||
|
||||
const LessonItem: React.FC<LessonItemPropsType> = ({ lesson }) => {
|
||||
const history = useHistory();
|
||||
|
||||
const handleEditLesson = (): void => history.push(`/lessons/edit/${lesson.id}`);
|
||||
const handleDeleteLesson = (): void => undefined;
|
||||
|
||||
return (
|
||||
<Box key={lesson.title} p='3' borderBottomWidth='1px'>
|
||||
<Flex flexDirection={{ base: 'column', md: 'row' }} justifyContent='space-between'>
|
||||
<Flex
|
||||
flexDirection={{ base: 'column', md: 'row' }}
|
||||
textAlign={{ base: 'center', md: 'left' }}
|
||||
alignItems='center'
|
||||
mr={{ md: '4' }}
|
||||
>
|
||||
<Box mr='3'>
|
||||
<Box bg={lesson.color?.hex || 'gray.100'} minH='50px' minW='50px' borderRadius='md' />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fontSize='lg' fontWeight='medium' mt={{ base: '3', md: '0' }}>
|
||||
<Text as='span' color='gray.500' mr='1' fontSize='md' fontWeight='normal'>
|
||||
1.
|
||||
</Text>
|
||||
{lesson.title}
|
||||
</Text>
|
||||
<Text mt='2' color='gray.500'>
|
||||
Sergey Yarkov
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box>
|
||||
<Flex flexDirection='column' alignItems={{ base: 'center' }} mt={{ base: '4' }}>
|
||||
<Flex flexDirection={{ base: 'column', md: 'row' }}>
|
||||
<Button onClick={handleEditLesson} size='md' p='0 60px' mr='1'>
|
||||
Edit
|
||||
</Button>
|
||||
<IconButton
|
||||
onClick={handleDeleteLesson}
|
||||
aria-label='Delete lesson'
|
||||
variant='ghost'
|
||||
colorScheme='red'
|
||||
icon={<DeleteIcon />}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justifyContent='flex-end' mt={{ base: '2' }}>
|
||||
<Text as='span' color='gray.500' mr='3'>
|
||||
<Text as='span' verticalAlign='middle' mr='1'>
|
||||
<Icon as={MdTimer} />
|
||||
</Text>
|
||||
<Text as='small' verticalAlign='middle'>
|
||||
~{moment.duration(lesson.duration, 'minutes').humanize()}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text as='span' color='gray.500'>
|
||||
<Text as='span' verticalAlign='middle' mr='1'>
|
||||
<Icon as={MdAttachment} />
|
||||
</Text>
|
||||
<Text as='small' verticalAlign='middle'>
|
||||
4 attachments
|
||||
</Text>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LessonItem;
|
||||
@ -1,19 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Box, Stack, Text, Button, StackDivider } from '@chakra-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import { List, arrayMove } from 'react-movable';
|
||||
import { Flex, Box, Stack, Text, IconButton, Icon, Button } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import type { LessonItemPropsType } from './LessonItem';
|
||||
import type { ICourse } from '@educt/interfaces';
|
||||
import type { ICourse, ILesson } from '@educt/interfaces';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
import { useColorMode } from '@chakra-ui/react';
|
||||
import { DeleteIcon } from '@chakra-ui/icons';
|
||||
import { MdTimer, MdAttachment } from 'react-icons/md';
|
||||
import { DragHandleIcon } from '@chakra-ui/icons';
|
||||
|
||||
type LessonListPropsType = {
|
||||
render: React.FC<LessonItemPropsType>;
|
||||
course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
|
||||
};
|
||||
|
||||
@ -31,16 +36,103 @@ const CreateLessonButton: React.FC<{ id: string }> = ({ id }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LessonList: React.FC<LessonListPropsType> = ({ render: Item, course }) => {
|
||||
const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
|
||||
const history = useHistory();
|
||||
const [lessons, setLessons] = useState<ILesson[]>(course.lessons);
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const handleEditLesson = (id: string): void => history.push(`/lessons/edit/${id}`);
|
||||
const handleDeleteLesson = (id: string): void => undefined;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{course.lessons.length !== 0 ? (
|
||||
<Box>
|
||||
<Stack mt='4'>
|
||||
{course.lessons.map(lesson => (
|
||||
<Item key={lesson.id} lesson={lesson} />
|
||||
))}
|
||||
</Stack>
|
||||
<List
|
||||
transitionDuration={100}
|
||||
lockVertically
|
||||
values={lessons}
|
||||
onChange={({ oldIndex, newIndex }) => setLessons(arrayMove(lessons, oldIndex, newIndex))}
|
||||
renderList={({ children, props }) => <Stack {...props}>{children}</Stack>}
|
||||
renderItem={({ value: lesson, props, isDragged, index }) => (
|
||||
<Box
|
||||
{...props}
|
||||
zIndex='1'
|
||||
bg={colorMode === 'dark' ? 'gray.700' : 'gray.50'}
|
||||
borderRadius='lg'
|
||||
position='relative'
|
||||
p='3'
|
||||
borderBottomWidth='1px'
|
||||
>
|
||||
<Box
|
||||
data-movable-handle
|
||||
cursor={isDragged ? 'grabbing' : 'grab'}
|
||||
position='absolute'
|
||||
bottom='50%'
|
||||
left='-20px'
|
||||
>
|
||||
<DragHandleIcon />
|
||||
</Box>
|
||||
<Flex flexDirection={{ base: 'column', md: 'row' }} justifyContent='space-between'>
|
||||
<Flex
|
||||
flexDirection={{ base: 'column', md: 'row' }}
|
||||
textAlign={{ base: 'center', md: 'left' }}
|
||||
alignItems='center'
|
||||
mr={{ md: '4' }}
|
||||
>
|
||||
<Box mr='3'>
|
||||
<Box bg={lesson.color?.hex || 'gray.100'} minH='50px' minW='50px' borderRadius='md' />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fontSize='lg' fontWeight='medium' mt={{ base: '3', md: '0' }}>
|
||||
<Text as='span' color='gray.500' mr='1' fontSize='md' fontWeight='normal'>
|
||||
{index}.
|
||||
</Text>
|
||||
{lesson.title}
|
||||
</Text>
|
||||
<Text mt='2' color='gray.500'>
|
||||
Sergey Yarkov
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box>
|
||||
<Flex flexDirection='column' alignItems={{ base: 'center' }} mt={{ base: '4' }}>
|
||||
<Flex flexDirection={{ base: 'column', md: 'row' }}>
|
||||
<Button onClick={() => handleEditLesson(lesson.id)} size='md' p='0 60px' mr='1'>
|
||||
Edit
|
||||
</Button>
|
||||
<IconButton
|
||||
onClick={() => handleDeleteLesson(lesson.id)}
|
||||
aria-label='Delete lesson'
|
||||
variant='ghost'
|
||||
colorScheme='red'
|
||||
icon={<DeleteIcon />}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justifyContent='flex-end' mt={{ base: '2' }}>
|
||||
<Text as='span' color='gray.500' mr='3'>
|
||||
<Text as='span' verticalAlign='middle' mr='1'>
|
||||
<Icon as={MdTimer} />
|
||||
</Text>
|
||||
<Text as='small' verticalAlign='middle'>
|
||||
~{moment.duration(lesson.duration, 'minutes').humanize()}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text as='span' color='gray.500'>
|
||||
<Text as='span' verticalAlign='middle' mr='1'>
|
||||
<Icon as={MdAttachment} />
|
||||
</Text>
|
||||
<Text as='small' verticalAlign='middle'>
|
||||
4 attachments
|
||||
</Text>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
/>
|
||||
<CreateLessonButton id={course.id} />
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import LessonList from './LessonList';
|
||||
import LessonItem from './LessonItem';
|
||||
|
||||
export { LessonList, LessonItem };
|
||||
export default LessonList;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { Box, Heading, Text, Tab, TabList, Flex, Tabs, TabPanels, TabPanel } from '@chakra-ui/react';
|
||||
|
||||
@ -13,7 +14,8 @@ import type { IPageProps } from '@educt/interfaces';
|
||||
*/
|
||||
import LoadingPage from '@educt/components/LoadingPage';
|
||||
import { EditCourseForm } from '@educt/components/Forms/CourseForm';
|
||||
import { LessonList, LessonItem } from './components/LessonList';
|
||||
import SetCourseStatusButton from '@educt/components/SetCourseStatusButton';
|
||||
import LessonList from './components/LessonList';
|
||||
import { StudentList, StudentItem } from './components/StudentList';
|
||||
|
||||
/**
|
||||
@ -44,12 +46,14 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
||||
<Helmet>
|
||||
<title>Editing {course.title}</title>
|
||||
</Helmet>
|
||||
<Flex>
|
||||
<Box>
|
||||
<Flex justifyContent='space-between' alignItems='center' flexWrap='wrap'>
|
||||
<Box mr='3'>
|
||||
<Heading as='h1'>Course editor</Heading>
|
||||
<Text mt='2'>{course.title}</Text>
|
||||
</Box>
|
||||
<Box></Box>
|
||||
<Box ml='auto' alignSelf='flex-end' justifySelf='flex-end' mt={{ base: '7', md: '0' }}>
|
||||
<SetCourseStatusButton courseId={course.id} currentStatus={course.status} />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box>
|
||||
<Tabs isLazy mt='8'>
|
||||
@ -74,7 +78,7 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
||||
</Box>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<LessonList render={LessonItem} course={course} />
|
||||
<LessonList course={course} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<StudentList render={StudentItem} students={course.students} />
|
||||
@ -86,4 +90,4 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default EditCoursePage;
|
||||
export default observer(EditCoursePage);
|
||||
|
||||
@ -30,7 +30,13 @@ const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Tag variant='subtle' colorScheme='green' borderRadius='full' alignSelf='flex-start'>
|
||||
<Tag
|
||||
color='white'
|
||||
boxShadow={`inset 0 0 0px 1px ${course.category.color?.hex}`}
|
||||
bg={course.category.color?.hex}
|
||||
borderRadius='full'
|
||||
alignSelf='flex-start'
|
||||
>
|
||||
<TagLabel>{course.category.title}</TagLabel>
|
||||
</Tag>
|
||||
</Box>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user