Добавлены новые зависимости: "react-select" и "@floating-ui/core". Реализована локализация с использованием i18next, добавлены переводы для английского и русского языков. Обновлены компоненты для поддержки локализации, включая AppHeader, Attendance, Dashboard и другие. Улучшена логика отображения данных и взаимодействия с пользователем.

This commit is contained in:
2025-03-23 11:41:29 +03:00
parent d5b5838e51
commit d3a7f70d12
27 changed files with 995 additions and 191 deletions
+16 -13
View File
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react'
import dayjs from 'dayjs'
import { Link as ConnectedLink, generatePath } from 'react-router-dom'
import { getNavigationsValue } from '@brojs/cli'
import { getNavigationValue } from '@brojs/cli'
import {
Box,
CardHeader,
@@ -16,6 +16,7 @@ import {
Tooltip,
Spinner,
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { api } from '../../__data__/api/api'
import { ArrowUpIcon, LinkIcon } from '@chakra-ui/icons'
@@ -25,6 +26,8 @@ import { CourseDetails } from './course-details'
export const CourseCard = ({ course }: { course: Course }) => {
const [getLessonList, populatedCourse] = api.useLazyGetCourseByIdQuery()
const [isOpened, setIsOpened] = useState(false)
const { t } = useTranslation()
useEffect(() => {
if (isOpened) {
getLessonList(course.id, true)
@@ -46,10 +49,10 @@ export const CourseCard = ({ course }: { course: Course }) => {
<CardBody mt="16px">
<Stack divider={<StackDivider />} spacing="8px">
<Box as="span" textAlign="left">
{`Дата начала курса - ${dayjs(course.startDt).format('DD MMMM YYYYг.')}`}
{`${t('journal.pl.course.startDate')} - ${dayjs(course.startDt).format(t('journal.pl.lesson.dateFormat'))}`}
</Box>
<Box as="span" textAlign="left">
Количество занятий - {course.lessons.length}
{t('journal.pl.course.lessonCount')} - {course.lessons.length}
</Box>
{populatedCourse.isFetching && <Spinner />}
@@ -57,9 +60,9 @@ export const CourseCard = ({ course }: { course: Course }) => {
<CourseDetails populatedCourse={populatedCourse.data} />
)}
{getNavigationsValue('link.journal.attendance') && (
{getNavigationValue('link.journal.attendance') && (
<Tooltip
label="На страницу с лекциями"
label={t('journal.pl.course.attendancePage')}
fontSize="12px"
top="16px"
>
@@ -69,12 +72,12 @@ export const CourseCard = ({ course }: { course: Course }) => {
variant="outline"
colorScheme="blue"
to={generatePath(
`${getNavigationsValue('journal.main')}${getNavigationsValue('link.journal.attendance')}`,
`${getNavigationValue('journal.main')}${getNavigationValue('link.journal.attendance')}`,
{ courseId: course.id },
)}
>
<Box mt={3}></Box>
Посещаемость
{t('journal.pl.course.attendance')}
</Button>
</Tooltip>
)}
@@ -87,17 +90,17 @@ export const CourseCard = ({ course }: { course: Course }) => {
mt="16px"
flexDirection={['column', 'row']}
>
<Tooltip label="На страницу с лекциями" fontSize="12px" top="16px">
<Tooltip label={t('journal.pl.course.attendancePage')} fontSize="12px" top="16px">
<Button
leftIcon={<LinkIcon />}
as={ConnectedLink}
colorScheme="blue"
to={`${getNavigationsValue('journal.main')}/lessons-list/${course._id}`}
to={`${getNavigationValue('journal.main')}/lessons-list/${course._id}`}
>
Открыть
{t('journal.pl.common.open')}
</Button>
</Tooltip>
<Tooltip label="Детали" fontSize="12px" top="16px">
<Tooltip label={t('journal.pl.course.details')} fontSize="12px" top="16px">
<Button
colorScheme="blue"
mt={['16px', 0]}
@@ -107,11 +110,11 @@ export const CourseCard = ({ course }: { course: Course }) => {
transform={isOpened ? 'rotate(0)' : 'rotate(180deg)'}
/>
}
loadingText="Загрузка"
loadingText={t('journal.pl.common.loading')}
isLoading={populatedCourse.isFetching}
onClick={handleToggleOpene}
>
{isOpened ? 'Закрыть' : 'Просмотреть детали'}
{isOpened ? t('journal.pl.close') : t('journal.pl.course.viewDetails')}
</Button>
</Tooltip>
</ButtonGroup>
+11 -9
View File
@@ -3,12 +3,13 @@ import dayjs from 'dayjs'
import { Link as ConnectedLink } from 'react-router-dom'
import { getNavigationValue, getHistory } from '@brojs/cli'
import { Stack, Heading, Link, Button, Tooltip, Box } from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { LinkIcon } from '@chakra-ui/icons'
import { useAppSelector } from '../../__data__/store'
import { isTeacher } from '../../utils/user'
import { PopulatedCourse } from '../../__data__/model'
import { api } from '../../__data__/api/api'
import { LinkIcon } from '@chakra-ui/icons'
type CourseDetailsProps = {
populatedCourse: PopulatedCourse
@@ -21,14 +22,15 @@ export const CourseDetails = ({ populatedCourse }: CourseDetailsProps) => {
const exam = populatedCourse.examWithJury
const [toggleExamWithJury, examWithJuryRequest] =
api.useToggleExamWithJuryMutation()
const { t } = useTranslation()
return (
<>
{isTeacher(user) && (
<Heading as="h3" mt={4} mb={3} size="lg">
Экзамен: {exam?.name}{' '}
{t('journal.pl.exam.title')}: {exam?.name}{' '}
{exam && getNavigationValue('exam.main') && getNavigationValue('link.exam.details') && (
<Tooltip label="Начать экзамен" fontSize="12px" top="16px">
<Tooltip label={t('journal.pl.exam.startExam')} fontSize="12px" top="16px">
<Button
leftIcon={<LinkIcon />}
as={'a'}
@@ -49,7 +51,7 @@ export const CourseDetails = ({ populatedCourse }: CourseDetailsProps) => {
)
}}
>
Открыть
{t('journal.pl.exam.open')}
</Button>
</Tooltip>
)}
@@ -58,10 +60,10 @@ export const CourseDetails = ({ populatedCourse }: CourseDetailsProps) => {
{!Boolean(exam) && (
<>
<Heading as="h3" mt={4} mb={3} size="lg">
Не задан
{t('journal.pl.exam.notSpecified')}
</Heading>
<Box mt={10}>
<Tooltip label="Создать экзамен с жюри" fontSize="12px" top="16px">
<Tooltip label={t('journal.pl.exam.createWithJury')} fontSize="12px" top="16px">
<Button
colorScheme="blue"
mt={['16px', 0]}
@@ -69,7 +71,7 @@ export const CourseDetails = ({ populatedCourse }: CourseDetailsProps) => {
isLoading={examWithJuryRequest.isLoading}
onClick={() => toggleExamWithJury(populatedCourse.id)}
>
Создать
{t('journal.pl.common.create')}
</Button>
</Tooltip>
</Box>
@@ -78,7 +80,7 @@ export const CourseDetails = ({ populatedCourse }: CourseDetailsProps) => {
{Boolean(exam) && (
<>
<Heading as="h3" mt={4} mb={3} size="lg">
Количество членов жюри:
{t('journal.pl.exam.juryCount')}:
</Heading>
<Heading as="h3" mt={4} mb={3} size="lg">
{populatedCourse.examWithJury.jury.length}
@@ -86,7 +88,7 @@ export const CourseDetails = ({ populatedCourse }: CourseDetailsProps) => {
</>
)}
<Heading as="h3" mt={4} mb={3} size="lg">
Список занятий:
{t('journal.pl.lesson.list')}:
</Heading>
<Stack>
{populatedCourse?.lessons?.map((lesson) => (
+17 -15
View File
@@ -20,6 +20,7 @@ import {
} 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'
@@ -40,6 +41,7 @@ export const CoursesList = () => {
const [createUpdateCourse, crucQuery] = api.useCreateUpdateCourseMutation()
const [showForm, setShowForm] = useState(false)
const toastRef = useRef(null)
const { t } = useTranslation()
const { colorMode } = useColorMode();
@@ -52,13 +54,13 @@ export const CoursesList = () => {
} = useForm<NewCourseForm>({
defaultValues: {
startDt: dayjs().format('YYYY-MM-DD'),
name: 'Название',
name: t('journal.pl.course.defaultName'),
},
})
const onSubmit = ({ startDt, name }) => {
toastRef.current = toast({
title: 'Отправляем',
title: t('journal.pl.course.sending'),
status: 'loading',
duration: 9000,
})
@@ -70,8 +72,8 @@ export const CoursesList = () => {
const values = getValues()
if (toastRef.current) {
toast.update(toastRef.current, {
title: 'Курс создан.',
description: `Курс ${values.name} успешно создан`,
title: t('journal.pl.course.created'),
description: t('journal.pl.course.successMessage', { name: values.name }),
status: 'success',
duration: 9000,
isClosable: true,
@@ -79,7 +81,7 @@ export const CoursesList = () => {
}
reset()
}
}, [crucQuery.isSuccess])
}, [crucQuery.isSuccess, t])
if (isLoading) {
return (
@@ -95,7 +97,7 @@ export const CoursesList = () => {
<Card align="left">
<CardHeader display="flex">
<Heading as="h2" mt="0">
Создание курса
{t('journal.pl.course.createTitle')}
</Heading>
<CloseButton ml="auto" onClick={() => setShowForm(false)} />
</CardHeader>
@@ -105,17 +107,17 @@ export const CoursesList = () => {
<Controller
control={control}
name="startDt"
rules={{ required: 'Обязательное поле' }}
rules={{ required: t('journal.pl.common.requiredField') }}
render={({ field }) => (
<FormControl
isRequired
isInvalid={Boolean(errors.startDt)}
>
<FormLabel>Дата начала</FormLabel>
<FormLabel>{t('journal.pl.common.startDate')}</FormLabel>
<Input
{...field}
required={false}
placeholder="Select Date and Time"
placeholder={t('journal.pl.common.selectDateTime')}
size="md"
type="date"
/>
@@ -125,7 +127,7 @@ export const CoursesList = () => {
</FormErrorMessage>
) : (
<FormHelperText>
Укажите дату начала курса
{t('journal.pl.course.specifyStartDate')}
</FormHelperText>
)}
</FormControl>
@@ -135,18 +137,18 @@ export const CoursesList = () => {
control={control}
name="name"
rules={{
required: 'Обязательное поле',
required: t('journal.pl.common.requiredField'),
}}
render={({ field }) => (
<FormControl
isRequired
isInvalid={Boolean(errors.name)}
>
<FormLabel>Название новой лекции:</FormLabel>
<FormLabel>{t('journal.pl.course.newLectureName')}:</FormLabel>
<Input
{...field}
required={false}
placeholder="КФУ-24-2"
placeholder={t('journal.pl.course.namePlaceholder')}
size="md"
/>
{errors.name && (
@@ -165,7 +167,7 @@ export const CoursesList = () => {
leftIcon={<AddIcon />}
colorScheme="blue"
>
Создать
{t('journal.pl.common.create')}
</Button>
</Box>
</VStack>
@@ -183,7 +185,7 @@ export const CoursesList = () => {
colorScheme="green"
onClick={() => setShowForm(true)}
>
Добавить
{t('journal.pl.common.add')}
</Button>
</Box>
)}