toggle exam
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
BaseResponse,
|
||||
Course,
|
||||
Lesson,
|
||||
PopulatedCourse,
|
||||
User,
|
||||
UserData,
|
||||
} from '../model'
|
||||
@@ -20,9 +21,9 @@ export const api = createApi({
|
||||
init?: RequestInit | undefined,
|
||||
) => {
|
||||
const response = await fetch(input, init)
|
||||
|
||||
|
||||
if (response.status === 403) keycloak.login()
|
||||
|
||||
|
||||
return response
|
||||
},
|
||||
headers: {
|
||||
@@ -32,15 +33,15 @@ export const api = createApi({
|
||||
headers.set('Authorization', `Bearer ${keycloak.token}`)
|
||||
},
|
||||
}),
|
||||
tagTypes: ['LessonList', 'CourseList'],
|
||||
tagTypes: ['LessonList', 'CourseList', 'Course'],
|
||||
endpoints: (builder) => ({
|
||||
coursesList: builder.query<BaseResponse<Course[]>, void>({
|
||||
query: () => '/course/list',
|
||||
providesTags: ['CourseList'],
|
||||
}),
|
||||
createUpdateCourse: builder.mutation<
|
||||
BaseResponse<Course>,
|
||||
Partial<Course> & Pick<Course, 'name'>
|
||||
BaseResponse<Course>,
|
||||
Partial<Course> & Pick<Course, 'name'>
|
||||
>({
|
||||
query: (course) => ({
|
||||
url: '/course',
|
||||
@@ -53,8 +54,8 @@ export const api = createApi({
|
||||
query: (courseId) => `/course/students/${courseId}`,
|
||||
}),
|
||||
manualAddStudent: builder.mutation<
|
||||
BaseResponse<void>,
|
||||
{ lessonId: string; user: User }
|
||||
BaseResponse<void>,
|
||||
{ lessonId: string; user: User }
|
||||
>({
|
||||
query: ({ lessonId, user }) => ({
|
||||
url: `/lesson/add-student/${lessonId}`,
|
||||
@@ -62,14 +63,14 @@ export const api = createApi({
|
||||
body: user,
|
||||
}),
|
||||
}),
|
||||
|
||||
|
||||
lessonList: builder.query<BaseResponse<Lesson[]>, string>({
|
||||
query: (courseId) => `/lesson/list/${courseId}`,
|
||||
providesTags: ['LessonList'],
|
||||
}),
|
||||
createLesson: builder.mutation<
|
||||
BaseResponse<Lesson>,
|
||||
Partial<Lesson> & Pick<Lesson, 'name' | 'date'> & { courseId: string }
|
||||
BaseResponse<Lesson>,
|
||||
Partial<Lesson> & Pick<Lesson, 'name' | 'date'> & { courseId: string }
|
||||
>({
|
||||
query: (data) => ({
|
||||
url: '/lesson',
|
||||
@@ -78,7 +79,7 @@ export const api = createApi({
|
||||
}),
|
||||
invalidatesTags: ['LessonList'],
|
||||
}),
|
||||
updateLesson: builder.mutation<BaseResponse<Lesson>, Partial<Lesson> & Pick<Lesson, '_id'>>({
|
||||
updateLesson: builder.mutation<BaseResponse<Lesson>, Partial<Lesson> & Pick<Lesson, 'id'>>({
|
||||
query: (data) => ({
|
||||
method: 'PUT',
|
||||
url: '/lesson',
|
||||
@@ -96,10 +97,10 @@ export const api = createApi({
|
||||
lessonById: builder.query<BaseResponse<Lesson>, string>({
|
||||
query: (lessonId: string) => `/lesson/${lessonId}`,
|
||||
}),
|
||||
|
||||
|
||||
createAccessCode: builder.query<
|
||||
BaseResponse<AccessCode>,
|
||||
{ lessonId: string }
|
||||
BaseResponse<AccessCode>,
|
||||
{ lessonId: string }
|
||||
>({
|
||||
query: ({ lessonId }) => ({
|
||||
url: '/lesson/access-code',
|
||||
@@ -108,13 +109,25 @@ export const api = createApi({
|
||||
}),
|
||||
}),
|
||||
getAccess: builder.query<
|
||||
BaseResponse<{ user: UserData; accessCode: AccessCode }>,
|
||||
{ accessCode: string }
|
||||
BaseResponse<{ user: UserData; accessCode: AccessCode }>,
|
||||
{ accessCode: string }
|
||||
>({
|
||||
query: ({ accessCode }) => ({
|
||||
url: `/lesson/access-code/${accessCode}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
}),
|
||||
getCourseById: builder.query<PopulatedCourse, string>({
|
||||
query: (courseId) => `/course/${courseId}`,
|
||||
transformResponse: (response: BaseResponse<PopulatedCourse>) => response.body,
|
||||
providesTags: ['Course'],
|
||||
}),
|
||||
toggleExamWithJury: builder.mutation<void, string>({
|
||||
query: (courseId) => ({
|
||||
url: `/course/toggle-exam-with-jury/${courseId}`,
|
||||
method: 'POST',
|
||||
}),
|
||||
invalidatesTags: ['Course']
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -50,7 +50,7 @@ export type BaseResponse<Data> = {
|
||||
};
|
||||
|
||||
export interface Lesson {
|
||||
_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
students: User[];
|
||||
date: string;
|
||||
@@ -79,10 +79,46 @@ export interface User {
|
||||
|
||||
export interface Course {
|
||||
_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
teachers: User[];
|
||||
lessons: Lesson[];
|
||||
lessons: string[];
|
||||
creator: User;
|
||||
startDt: string;
|
||||
examWithJury?: string;
|
||||
created: string;
|
||||
}
|
||||
|
||||
export interface PopulatedCourse extends Omit<Course, 'examWithJury' | 'lessons'> {
|
||||
examWithJury: ExamWithJury;
|
||||
lessons: Lesson[];
|
||||
}
|
||||
|
||||
interface ExamWithJury {
|
||||
name: string;
|
||||
description: string;
|
||||
jury: any[];
|
||||
date: string;
|
||||
created: string;
|
||||
criterias: Criteria[][];
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface Criteria {
|
||||
title: string;
|
||||
type?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
wight?: number;
|
||||
subcriterias?: Subcriteria[];
|
||||
}
|
||||
|
||||
interface Subcriteria {
|
||||
title: string;
|
||||
type: string;
|
||||
wight: number;
|
||||
trueText?: string;
|
||||
falseText?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
20
src/components/page-loader/page-loader.tsx
Normal file
20
src/components/page-loader/page-loader.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Spinner,
|
||||
Container,
|
||||
Center,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
export const PageLoader = () => (
|
||||
<Container maxW="container.xl">
|
||||
<Center h="300px">
|
||||
<Spinner
|
||||
thickness="4px"
|
||||
speed="0.65s"
|
||||
emptyColor="gray.200"
|
||||
color="blue.500"
|
||||
size="xl"
|
||||
/>
|
||||
</Center>
|
||||
</Container>
|
||||
)
|
||||
93
src/pages/course-list/course-card.tsx
Normal file
93
src/pages/course-list/course-card.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { Link as ConnectedLink } from 'react-router-dom'
|
||||
import { getNavigationsValue } from '@brojs/cli'
|
||||
import {
|
||||
Box,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
ButtonGroup,
|
||||
Stack,
|
||||
StackDivider,
|
||||
Button,
|
||||
Card,
|
||||
Heading,
|
||||
Tooltip,
|
||||
Spinner,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
import { api } from '../../__data__/api/api'
|
||||
import { ArrowUpIcon, LinkIcon } from '@chakra-ui/icons'
|
||||
import { Course } from '../../__data__/model'
|
||||
import { CourseDetails } from './course-details'
|
||||
|
||||
export const CourseCard = ({
|
||||
course,
|
||||
}: {
|
||||
course: Course
|
||||
}) => {
|
||||
const [getLessonList, populatedCourse] = api.useLazyGetCourseByIdQuery()
|
||||
const [isOpened, setIsOpened] = useState(false)
|
||||
useEffect(() => {
|
||||
if (isOpened) {
|
||||
getLessonList(course.id, true)
|
||||
}
|
||||
}, [isOpened])
|
||||
|
||||
const handleToggleOpene = useCallback(() => {
|
||||
setIsOpened(opened => !opened)
|
||||
}, [setIsOpened])
|
||||
|
||||
return (
|
||||
<Card key={course._id} align="left">
|
||||
<CardHeader>
|
||||
<Heading as="h2" mt="0">
|
||||
{course.name}
|
||||
</Heading>
|
||||
</CardHeader>
|
||||
{isOpened && (
|
||||
<CardBody mt="16px">
|
||||
<Stack divider={<StackDivider />} spacing="8px">
|
||||
<Box as="span" textAlign="left">
|
||||
{`Дата начала курса - ${dayjs(course.startDt).format('DD MMMM YYYYг.')}`}
|
||||
</Box>
|
||||
<Box as="span" textAlign="left">
|
||||
Количество занятий - {course.lessons.length}
|
||||
</Box>
|
||||
|
||||
{populatedCourse.isFetching && <Spinner />}
|
||||
{!populatedCourse.isFetching && populatedCourse.isSuccess && <CourseDetails populatedCourse={populatedCourse.data} />}
|
||||
</Stack>
|
||||
</CardBody>
|
||||
)}
|
||||
<CardFooter>
|
||||
<ButtonGroup spacing={[0, 4]} mt="16px" flexDirection={['column', 'row']}>
|
||||
<Tooltip label="На страницу с лекциями" fontSize="12px" top="16px">
|
||||
<Button
|
||||
leftIcon={<LinkIcon />}
|
||||
as={ConnectedLink}
|
||||
colorScheme="blue"
|
||||
to={`${getNavigationsValue('journal.main')}/lessons-list/${course._id}`}
|
||||
>
|
||||
Открыть
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Детали" fontSize="12px" top="16px">
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
mt={["16px", 0]}
|
||||
variant="outline"
|
||||
leftIcon={<ArrowUpIcon transform={isOpened ? 'rotate(0)' : 'rotate(180deg)'} />}
|
||||
loadingText="Загрузка"
|
||||
isLoading={populatedCourse.isFetching}
|
||||
onClick={handleToggleOpene}
|
||||
>
|
||||
{isOpened ? 'Закрыть' : 'Просмотреть детали'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
83
src/pages/course-list/course-details.tsx
Normal file
83
src/pages/course-list/course-details.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { Link as ConnectedLink } from 'react-router-dom'
|
||||
import { getNavigationsValue } from '@brojs/cli'
|
||||
import {
|
||||
Stack,
|
||||
Heading,
|
||||
Link,
|
||||
Button,
|
||||
Tooltip,
|
||||
Box,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
import { useAppSelector } from '../../__data__/store'
|
||||
import { isTeacher } from '../../utils/user'
|
||||
import { PopulatedCourse } from '../../__data__/model'
|
||||
import { api } from '../../__data__/api/api'
|
||||
|
||||
type CourseDetailsProps = {
|
||||
populatedCourse: PopulatedCourse;
|
||||
}
|
||||
|
||||
export const CourseDetails = ({ populatedCourse }: CourseDetailsProps) => {
|
||||
const user = useAppSelector((s) => s.user)
|
||||
const exam = populatedCourse.examWithJury
|
||||
const [toggleExamWithJury, examWithJuryRequest] = api.useToggleExamWithJuryMutation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading as="h3" mt={4} mb={3} size="lg">
|
||||
Экзамен:
|
||||
</Heading>
|
||||
{!Boolean(exam) && (
|
||||
<>
|
||||
<Heading as="h3" mt={4} mb={3} size="lg">
|
||||
Не задан
|
||||
</Heading>
|
||||
<Box mt={10}>
|
||||
<Tooltip label="Детали" fontSize="12px" top="16px">
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
mt={["16px", 0]}
|
||||
variant="outline"
|
||||
isLoading={examWithJuryRequest.isLoading}
|
||||
onClick={() => toggleExamWithJury(populatedCourse.id)}
|
||||
>
|
||||
Создать
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{Boolean(exam) && (
|
||||
<>
|
||||
<Heading as="h3" mt={4} mb={3} size="lg">
|
||||
Количество членов жюри:
|
||||
</Heading>
|
||||
<Heading as="h3" mt={4} mb={3} size="lg">
|
||||
{populatedCourse.examWithJury.jury.length}
|
||||
</Heading></>
|
||||
|
||||
)}
|
||||
<Heading as="h3" mt={4} mb={3} size="lg">
|
||||
Список занятий:
|
||||
</Heading>
|
||||
<Stack>
|
||||
{populatedCourse?.lessons?.map((lesson) => (
|
||||
<Link
|
||||
as={ConnectedLink}
|
||||
key={lesson.id}
|
||||
to={
|
||||
isTeacher(user)
|
||||
? `${getNavigationsValue('journal.main')}/lesson/${populatedCourse.id}/${lesson.id}`
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{lesson.name}
|
||||
</Link>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,56 +1,44 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { Link as ConnectedLink } from 'react-router-dom'
|
||||
import { getNavigationsValue } from '@brojs/cli'
|
||||
import {
|
||||
Box,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
ButtonGroup,
|
||||
Stack,
|
||||
StackDivider,
|
||||
Button,
|
||||
Card,
|
||||
Heading,
|
||||
Tooltip,
|
||||
Spinner,
|
||||
Container,
|
||||
VStack,
|
||||
Link,
|
||||
Input,
|
||||
CloseButton,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
FormHelperText,
|
||||
Center,
|
||||
FormErrorMessage,
|
||||
useToast,
|
||||
} from '@chakra-ui/react'
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
|
||||
import { ErrorSpan } from './style'
|
||||
import { ErrorSpan } from '../style'
|
||||
|
||||
import { useAppSelector } from '../__data__/store'
|
||||
import { api } from '../__data__/api/api'
|
||||
import { isTeacher } from '../utils/user'
|
||||
import { AddIcon, ArrowDownIcon, ArrowUpIcon, LinkIcon } from '@chakra-ui/icons'
|
||||
import { Course } from '../__data__/model'
|
||||
import { useAppSelector } from '../../__data__/store'
|
||||
import { api } from '../../__data__/api/api'
|
||||
import { isTeacher } from '../../utils/user'
|
||||
import { AddIcon } from '@chakra-ui/icons'
|
||||
import { PageLoader } from '../../components/page-loader/page-loader'
|
||||
import { CourseCard } from './course-card'
|
||||
|
||||
interface NewCourseForm {
|
||||
startDt: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const CoursesList = () => {
|
||||
export const CoursesList = () => {
|
||||
const toast = useToast()
|
||||
const user = useAppSelector((s) => s.user)
|
||||
const { data, isLoading } = api.useCoursesListQuery()
|
||||
const [createUpdateCourse, crucQuery] = api.useCreateUpdateCourseMutation()
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [courseDetailsOpenedId, setCourseDetailsOpenedId] = useState<
|
||||
string | null
|
||||
>(null)
|
||||
const toastRef = useRef(null)
|
||||
|
||||
const {
|
||||
@@ -93,17 +81,7 @@ const CoursesList = () => {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Container maxW="container.xl">
|
||||
<Center h="300px">
|
||||
<Spinner
|
||||
thickness="4px"
|
||||
speed="0.65s"
|
||||
emptyColor="gray.200"
|
||||
color="blue.500"
|
||||
size="xl"
|
||||
/>
|
||||
</Center>
|
||||
</Container>
|
||||
<PageLoader />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -212,110 +190,11 @@ const CoursesList = () => {
|
||||
<VStack as="ul" align="stretch">
|
||||
{data?.body?.map((c) => (
|
||||
<CourseCard
|
||||
isOpened={courseDetailsOpenedId === c._id}
|
||||
key={c._id}
|
||||
course={c}
|
||||
openDetails={() =>
|
||||
courseDetailsOpenedId === c._id
|
||||
? setCourseDetailsOpenedId(null)
|
||||
: setCourseDetailsOpenedId(c._id)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const CourseCard = ({
|
||||
course,
|
||||
isOpened,
|
||||
openDetails,
|
||||
}: {
|
||||
course: Course
|
||||
isOpened: boolean
|
||||
openDetails: () => void
|
||||
}) => {
|
||||
const [getLessonList, lessonList] = api.useLazyLessonListQuery()
|
||||
useEffect(() => {
|
||||
if (isOpened) {
|
||||
getLessonList(course._id, true)
|
||||
}
|
||||
}, [isOpened])
|
||||
const user = useAppSelector((s) => s.user)
|
||||
|
||||
return (
|
||||
<Card key={course._id} align="left">
|
||||
<CardHeader>
|
||||
<Heading as="h2" mt="0">
|
||||
{course.name}
|
||||
</Heading>
|
||||
</CardHeader>
|
||||
{isOpened && (
|
||||
<CardBody mt="16px">
|
||||
<Stack divider={<StackDivider />} spacing="8px">
|
||||
<Box as="span" textAlign="left">
|
||||
{`Дата начала курса - ${dayjs(course.startDt).format('DD MMMM YYYYг.')}`}
|
||||
</Box>
|
||||
<Box as="span" textAlign="left">
|
||||
Количество занятий - {course.lessons.length}
|
||||
</Box>
|
||||
{lessonList.isFetching ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<Heading as="h3" mt={4} mb={3} size="lg">
|
||||
Список занятий:
|
||||
</Heading>
|
||||
<Stack>
|
||||
{lessonList.data?.body?.map((lesson) => (
|
||||
<Link
|
||||
as={ConnectedLink}
|
||||
key={lesson._id}
|
||||
to={
|
||||
isTeacher(user)
|
||||
? `${getNavigationsValue('journal.main')}/lesson/${course._id}/${lesson._id}`
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{lesson.name}
|
||||
</Link>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</CardBody>
|
||||
)}
|
||||
<CardFooter>
|
||||
<ButtonGroup spacing={[0, 4]} mt="16px" flexDirection={['column', 'row']}>
|
||||
<Tooltip label="На страницу с лекциями" fontSize="12px" top="16px">
|
||||
<Button
|
||||
leftIcon={<LinkIcon />}
|
||||
as={ConnectedLink}
|
||||
colorScheme="blue"
|
||||
to={`${getNavigationsValue('journal.main')}/lessons-list/${course._id}`}
|
||||
>
|
||||
Открыть
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Детали" fontSize="12px" top="16px">
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
mt={["16px", 0]}
|
||||
variant="outline"
|
||||
leftIcon={isOpened ? <ArrowUpIcon /> : <ArrowDownIcon />}
|
||||
loadingText="Загрузка"
|
||||
isLoading={lessonList.isFetching}
|
||||
onClick={openDetails}
|
||||
>
|
||||
Просмотреть детали
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default CoursesList
|
||||
3
src/pages/course-list/index.ts
Normal file
3
src/pages/course-list/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { CoursesList } from './course-list'
|
||||
|
||||
export default CoursesList
|
||||
Reference in New Issue
Block a user