From e277308ec23c36ad2329e4c3313f7927fb1f8550 Mon Sep 17 00:00:00 2001 From: primakov Date: Sun, 23 Mar 2025 13:26:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D0=BE=D0=B4=D1=8B=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=BE=D0=BC=20=D1=83=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2=20=D0=B2=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20(en.json=20=D0=B8=20ru.json).=20=D0=9E=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=82=20CoursesList:=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=BA=D1=83?= =?UTF-8?q?=D1=80=D1=81=D0=B0=20=D1=81=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20=D1=85=D1=83=D0=BA=D0=B0=20use?= =?UTF-8?q?CreateCourse,=20=D0=B0=20=D1=82=D0=B0=D0=BA=D0=B6=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20YearGroup=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BA=D1=83=D1=80=D1=81=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=B3=D0=BE=D0=B4=D0=B0=D0=BC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.json | 2 + locales/ru.json | 3 + .../components/CreateCourseForm.tsx | 141 +++++++++++ .../course-list/components/YearGroup.tsx | 34 +++ src/pages/course-list/components/index.ts | 2 + src/pages/course-list/course-list.tsx | 236 ++---------------- src/pages/course-list/hooks/index.ts | 2 + .../course-list/hooks/useCreateCourse.ts | 72 ++++++ .../course-list/hooks/useGroupedCourses.ts | 32 +++ 9 files changed, 310 insertions(+), 214 deletions(-) create mode 100644 src/pages/course-list/components/CreateCourseForm.tsx create mode 100644 src/pages/course-list/components/YearGroup.tsx create mode 100644 src/pages/course-list/components/index.ts create mode 100644 src/pages/course-list/hooks/index.ts create mode 100644 src/pages/course-list/hooks/useCreateCourse.ts create mode 100644 src/pages/course-list/hooks/useGroupedCourses.ts diff --git a/locales/en.json b/locales/en.json index b5cb81d..349ec5f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -73,6 +73,8 @@ "journal.pl.lesson.link": "Link", "journal.pl.lesson.time": "Time", "journal.pl.lesson.action": "Actions", + "journal.pl.lesson.expand": "Expand lesson list", + "journal.pl.lesson.collapse": "Collapse lesson list", "journal.pl.exam.title": "Exam", "journal.pl.exam.startExam": "Start exam", diff --git a/locales/ru.json b/locales/ru.json index 70548c8..21a246b 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -69,6 +69,8 @@ "journal.pl.lesson.link": "Ссылка", "journal.pl.lesson.time": "Время", "journal.pl.lesson.action": "Действия", + "journal.pl.lesson.expand": "Развернуть список занятий", + "journal.pl.lesson.collapse": "Свернуть список занятий", "journal.pl.exam.title": "Экзамен", "journal.pl.exam.startExam": "Начать экзамен", @@ -90,6 +92,7 @@ "journal.pl.attendance.emojis.good": "Хорошая посещаемость", "journal.pl.attendance.emojis.average": "Средняя посещаемость", "journal.pl.attendance.emojis.poor": "Низкая посещаемость", + "journal.pl.attendance.emojis.critical": "Критическая посещаемость", "journal.pl.attendance.emojis.none": "Нет посещений", "journal.pl.attendance.table.copy": "Копировать таблицу", diff --git a/src/pages/course-list/components/CreateCourseForm.tsx b/src/pages/course-list/components/CreateCourseForm.tsx new file mode 100644 index 0000000..5170878 --- /dev/null +++ b/src/pages/course-list/components/CreateCourseForm.tsx @@ -0,0 +1,141 @@ +import React from 'react' +import { + Box, + CardHeader, + CardBody, + Button, + Card, + Heading, + Input, + CloseButton, + FormControl, + FormLabel, + FormHelperText, + FormErrorMessage, + useBreakpointValue, + Flex, + Stack +} from '@chakra-ui/react' +import { Controller } from 'react-hook-form' +import { AddIcon } from '@chakra-ui/icons' +import { useTranslation } from 'react-i18next' + +import { ErrorSpan } from '../../style' +import { useCreateCourse } from '../hooks' + +interface CreateCourseFormProps { + onClose: () => void +} + +/** + * Компонент формы создания нового курса + */ +export const CreateCourseForm = ({ onClose }: CreateCourseFormProps) => { + const { t } = useTranslation() + const { control, errors, handleSubmit, onSubmit, isLoading, error } = useCreateCourse(onClose) + + const headingSize = useBreakpointValue({ base: 'md', md: 'lg' }) + const formSpacing = useBreakpointValue({ base: 5, md: 10 }) + const buttonSize = useBreakpointValue({ base: 'md', md: 'lg' }) + + return ( + + + + {t('journal.pl.course.createTitle')} + + + + +
+ + ( + + {t('journal.pl.common.startDate')} + + {errors.startDt ? ( + + {errors.startDt?.message} + + ) : ( + + {t('journal.pl.course.specifyStartDate')} + + )} + + )} + /> + ( + + {t('journal.pl.course.newLectureName')}: + + {errors.name && ( + + {errors.name.message} + + )} + + )} + /> + + + + + + + + + + {error && ( + + {(error as any).error} + + )} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/pages/course-list/components/YearGroup.tsx b/src/pages/course-list/components/YearGroup.tsx new file mode 100644 index 0000000..fb19875 --- /dev/null +++ b/src/pages/course-list/components/YearGroup.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { Box, Flex, Heading, Divider, VStack } from '@chakra-ui/react' +import { Course } from '../../../__data__/model' +import { CourseCard } from '../course-card' + +interface YearGroupProps { + year: string + courses: Course[] + colorMode: string +} + +/** + * Компонент для отображения курсов одного года + */ +export const YearGroup = ({ year, courses, colorMode }: YearGroupProps) => { + return ( + + + + {year} + + + + + {courses.map((course) => ( + + ))} + + + ) +} \ No newline at end of file diff --git a/src/pages/course-list/components/index.ts b/src/pages/course-list/components/index.ts new file mode 100644 index 0000000..7461583 --- /dev/null +++ b/src/pages/course-list/components/index.ts @@ -0,0 +1,2 @@ +export * from './YearGroup' +export * from './CreateCourseForm' \ No newline at end of file diff --git a/src/pages/course-list/course-list.tsx b/src/pages/course-list/course-list.tsx index 0adfc44..0db44fc 100644 --- a/src/pages/course-list/course-list.tsx +++ b/src/pages/course-list/course-list.tsx @@ -1,232 +1,50 @@ -import React, { useEffect, useRef, useState, useMemo } from 'react' -import dayjs from 'dayjs' +import React, { useState } from 'react' import { Box, - CardHeader, - CardBody, Button, - Card, - Heading, Container, - VStack, - Input, - CloseButton, - FormControl, - FormLabel, - FormHelperText, - FormErrorMessage, - useToast, + Text, useColorMode, - useBreakpointValue, - Flex, - Stack, - Divider, - Text + useBreakpointValue } from '@chakra-ui/react' -import { useForm, Controller } from 'react-hook-form' import { AddIcon } from '@chakra-ui/icons' import { useTranslation } from 'react-i18next' -import { ErrorSpan } from '../style' import { useAppSelector } from '../../__data__/store' import { api } from '../../__data__/api/api' import { isTeacher } from '../../utils/user' import { PageLoader } from '../../components/page-loader/page-loader' -import { CourseCard } from './course-card' - -interface NewCourseForm { - startDt: string - name: string -} +import { useGroupedCourses } from './hooks' +import { CreateCourseForm, YearGroup } from './components' +/** + * Основной компонент списка курсов + */ 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 toastRef = useRef(null) const { t } = useTranslation() - - const { colorMode } = useColorMode(); + const { colorMode } = useColorMode() - // Определяем размеры для адаптивного дизайна const buttonSize = useBreakpointValue({ base: 'md', md: 'lg' }) - const headingSize = useBreakpointValue({ base: 'md', md: 'lg' }) - const formSpacing = useBreakpointValue({ base: 5, md: 10 }) const containerPadding = useBreakpointValue({ base: '2', md: '4' }) - const { - control, - handleSubmit, - reset, - formState: { errors }, - getValues, - } = useForm({ - defaultValues: { - startDt: dayjs().format('YYYY-MM-DD'), - name: t('journal.pl.course.defaultName'), - }, - }) - - const onSubmit = ({ startDt, name }) => { - toastRef.current = toast({ - title: t('journal.pl.course.sending'), - status: 'loading', - duration: 9000, - }) - createUpdateCourse({ name, startDt }) - } - - useEffect(() => { - if (crucQuery.isSuccess) { - const values = getValues() - if (toastRef.current) { - toast.update(toastRef.current, { - title: t('journal.pl.course.created'), - description: t('journal.pl.course.successMessage', { name: values.name }), - status: 'success', - duration: 9000, - isClosable: true, - }) - } - reset() - setShowForm(false) // Закрываем форму после успешного создания - } - }, [crucQuery.isSuccess, t]) - - // Группировка курсов по годам - const groupedCourses = useMemo(() => { - if (!data?.body?.length) return {} - - const grouped: Record = {} - - // Сортируем курсы по дате начала (от новых к старым) - const sortedCourses = [...data.body].sort((a, b) => - dayjs(b.startDt).valueOf() - dayjs(a.startDt).valueOf() - ) - - // Группируем по годам - sortedCourses.forEach(course => { - const year = dayjs(course.startDt).format('YYYY') - if (!grouped[year]) { - grouped[year] = [] - } - grouped[year].push(course) - }) - - return grouped - }, [data?.body]) + // Используем хук для группировки курсов по годам + const groupedCourses = useGroupedCourses(data?.body) if (isLoading) { - return ( - - ) + return } + const handleCloseForm = () => setShowForm(false) + return ( {isTeacher(user) && ( {showForm ? ( - - - - {t('journal.pl.course.createTitle')} - - setShowForm(false)} /> - - -
- - ( - - {t('journal.pl.common.startDate')} - - {errors.startDt ? ( - - {errors.startDt?.message} - - ) : ( - - {t('journal.pl.course.specifyStartDate')} - - )} - - )} - /> - ( - - {t('journal.pl.course.newLectureName')}: - - {errors.name && ( - - {errors.name.message} - - )} - - )} - /> - - - - - - - - - - {crucQuery?.error && ( - - {(crucQuery?.error as any).error} - - )} -
-
-
+ ) : (