Добавлены новые зависимости: "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
+31 -26
View File
@@ -1,28 +1,33 @@
import React from 'react'
import { ResponsiveBar } from '@nivo/bar'
import { type BarDatum, ResponsiveBar } from '@nivo/bar'
import { useTranslation } from 'react-i18next'
export const Bar = ({ data }) => (
<ResponsiveBar
data={data}
keys={['count']}
indexBy="lessonIndex"
margin={{ top: 50, right: 130, bottom: 50, left: 60 }}
padding={0.3}
valueScale={{ type: 'linear' }}
indexScale={{ type: 'band', round: true }}
colors={{ scheme: 'set3' }}
axisTop={null}
axisRight={null}
labelSkipWidth={12}
labelSkipHeight={12}
labelTextColor={{
from: 'color',
modifiers: [['brighter', 1.4]],
}}
role="application"
ariaLabel="График посещаемости лекций"
barAriaLabel={(e) =>
e.id + ': ' + e.formattedValue + ' on lection: ' + e.indexValue
}
/>
)
export const Bar = ({ data }: { data: BarDatum[] }) => {
const { t } = useTranslation()
return (
<ResponsiveBar
data={data}
keys={['count']}
indexBy="lessonIndex"
margin={{ top: 50, right: 130, bottom: 50, left: 60 }}
padding={0.3}
valueScale={{ type: 'linear' }}
indexScale={{ type: 'band', round: true }}
colors={{ scheme: 'set3' }}
axisTop={null}
axisRight={null}
labelSkipWidth={12}
labelSkipHeight={12}
labelTextColor={{
from: 'color',
modifiers: [['brighter', 1.4]],
}}
role="application"
ariaLabel={t('journal.pl.lesson.attendanceChart')}
barAriaLabel={(e) =>
e.id + ': ' + e.formattedValue + ' on lection: ' + e.indexValue
}
/>
)
}
+11 -9
View File
@@ -13,6 +13,7 @@ import {
useToast,
} from '@chakra-ui/react'
import { EditIcon } from '@chakra-ui/icons'
import { useTranslation } from 'react-i18next'
import { qrCode } from '../../../assets'
@@ -46,10 +47,11 @@ export const Item: React.FC<ItemProps> = ({
const toast = useToast()
const [updateLesson, updateLessonRqst] = api.useUpdateLessonMutation()
const createdLessonRef = useRef(null)
const { t } = useTranslation()
const onSubmit = (lessonData) => {
toastRef.current = toast({
title: 'Отправляем',
title: t('journal.pl.common.sending'),
status: 'loading',
duration: 9000,
})
@@ -58,7 +60,7 @@ export const Item: React.FC<ItemProps> = ({
updateLesson(lessonData)
} else {
toast.update(toastRef.current, {
title: 'Отсутствует интернет',
title: t('journal.pl.lesson.noInternet'),
status: 'error',
duration: 3000
})
@@ -68,8 +70,8 @@ export const Item: React.FC<ItemProps> = ({
useEffect(() => {
if (updateLessonRqst.isSuccess) {
const toastProps = {
title: 'Лекция Обновлена',
description: `Лекция ${createdLessonRef.current?.name} успешно обновлена`,
title: t('journal.pl.lesson.updated'),
description: t('journal.pl.lesson.updateMessage', { name: createdLessonRef.current?.name }),
status: 'success' as const,
duration: 9000,
isClosable: true,
@@ -92,8 +94,8 @@ export const Item: React.FC<ItemProps> = ({
setEdit(false)
}}
lesson={{ _id: id, id, name, date }}
title={'Редактирование лекции'}
nameButton={'Сохранить'}
title={t('journal.pl.lesson.editTitle')}
nameButton={t('journal.pl.save')}
/>
</Td>
</Tr>
@@ -129,13 +131,13 @@ export const Item: React.FC<ItemProps> = ({
setEdit(true)
}}
>
Edit
{t('journal.pl.edit')}
</MenuItem>
<MenuItem onClick={setlessonToDelete}>Delete</MenuItem>
<MenuItem onClick={setlessonToDelete}>{t('journal.pl.delete')}</MenuItem>
</MenuList>
</Menu>
)}
{edit && <Button onClick={setlessonToDelete}>Сохранить</Button>}
{edit && <Button onClick={setlessonToDelete}>{t('journal.pl.save')}</Button>}
</Td>
)}
<Td isNumeric>{students.length}</Td>
@@ -16,6 +16,7 @@ import {
Input,
} from '@chakra-ui/react'
import { AddIcon } from '@chakra-ui/icons'
import { useTranslation } from 'react-i18next'
import { dateToCalendarFormat } from '../../../utils/time'
import { Lesson } from '../../../__data__/model'
@@ -45,6 +46,8 @@ export const LessonForm = ({
title,
nameButton,
}: LessonFormProps) => {
const { t } = useTranslation()
const getNearestTimeSlot = () => {
const now = new Date();
const minutes = now.getMinutes();
@@ -96,21 +99,21 @@ export const LessonForm = ({
<Controller
control={control}
name="date"
rules={{ required: 'Обязательное поле' }}
rules={{ required: t('journal.pl.common.required') }}
render={({ field }) => (
<FormControl>
<FormLabel>Дата</FormLabel>
<FormLabel>{t('journal.pl.lesson.form.date')}</FormLabel>
<Input
{...field}
required={false}
placeholder="Укажите дату лекции"
placeholder={t('journal.pl.lesson.form.datePlaceholder')}
size="md"
type="datetime-local"
/>
{errors.date ? (
<FormErrorMessage>{errors.date?.message}</FormErrorMessage>
) : (
<FormHelperText>Укажите дату и время лекции</FormHelperText>
<FormHelperText>{t('journal.pl.lesson.form.dateTime')}</FormHelperText>
)}
</FormControl>
)}
@@ -119,14 +122,14 @@ export const LessonForm = ({
<Controller
control={control}
name="name"
rules={{ required: 'Обязательное поле' }}
rules={{ required: t('journal.pl.common.required') }}
render={({ field }) => (
<FormControl isRequired isInvalid={Boolean(errors.name)}>
<FormLabel>Название новой лекции:</FormLabel>
<FormLabel>{t('journal.pl.lesson.form.title')}</FormLabel>
<Input
{...field}
required={false}
placeholder="Название лекции"
placeholder={t('journal.pl.lesson.form.namePlaceholder')}
size="md"
/>
{errors.name && (
+23 -22
View File
@@ -26,6 +26,7 @@ import {
AlertDialogOverlay,
} from '@chakra-ui/react'
import { AddIcon } from '@chakra-ui/icons'
import { useTranslation } from 'react-i18next'
import { useAppSelector } from '../../__data__/store'
import { api } from '../../__data__/api/api'
@@ -57,6 +58,7 @@ const LessonList = () => {
const toastRef = useRef(null)
const createdLessonRef = useRef(null)
const [editLesson, setEditLesson] = useState<Lesson>(null)
const { t } = useTranslation()
const sorted = useMemo(
() => [...(data?.body || [])]?.sort((a, b) => (a.date > b.date ? 1 : -1)),
[data, data?.body],
@@ -93,7 +95,7 @@ const LessonList = () => {
const onSubmit = (lessonData) => {
toastRef.current = toast({
title: 'Отправляем',
title: t('journal.pl.common.sending'),
status: 'loading',
duration: 9000,
})
@@ -123,7 +125,7 @@ const LessonList = () => {
title={
<>
<Box pb={3}>
<Text fontSize="xl">{`Удалена лекция ${lesson.name}`}</Text>
<Text fontSize="xl">{t('journal.pl.lesson.deletedMessage', { name: lesson.name })}</Text>
</Box>
<Button
onClick={() => {
@@ -131,7 +133,7 @@ const LessonList = () => {
toast.close(id)
}}
>
Восстановить
{t('journal.pl.common.restored')}
</Button>
</>
}
@@ -145,8 +147,8 @@ const LessonList = () => {
useEffect(() => {
if (crLQuery.isSuccess) {
const toastProps = {
title: 'Лекция создана',
description: `Лекция ${createdLessonRef.current?.name} успешно создана`,
title: t('journal.pl.lesson.created'),
description: t('journal.pl.lesson.successMessage', { name: createdLessonRef.current?.name }),
status: 'success' as const,
duration: 9000,
isClosable: true,
@@ -160,8 +162,8 @@ const LessonList = () => {
useEffect(() => {
if (updateLessonRqst.isSuccess) {
const toastProps = {
title: 'Лекция Обновлена',
description: `Лекция ${createdLessonRef.current?.name} успешно обновлена`,
title: t('journal.pl.lesson.updated'),
description: t('journal.pl.lesson.updateMessage', { name: createdLessonRef.current?.name }),
status: 'success' as const,
duration: 9000,
isClosable: true,
@@ -186,12 +188,11 @@ const LessonList = () => {
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
Удалить занятие от{' '}
{dayjs(lessonToDelete?.date).format('DD.MM.YY')}?
{t('journal.pl.lesson.deleteConfirm', { date: dayjs(lessonToDelete?.date).format('DD.MM.YY') })}
</AlertDialogHeader>
<AlertDialogBody>
Все данные о посещении данного занятия будут удалены
{t('journal.pl.lesson.deleteWarning')}
</AlertDialogBody>
<AlertDialogFooter>
@@ -200,7 +201,7 @@ const LessonList = () => {
ref={cancelRef}
onClick={() => setlessonToDelete(null)}
>
Cancel
{t('journal.pl.cancel')}
</Button>
<Button
colorScheme="red"
@@ -209,7 +210,7 @@ const LessonList = () => {
onClick={() => deleteLesson(lessonToDelete.id)}
ml={3}
>
Delete
{t('journal.pl.delete')}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
@@ -219,12 +220,12 @@ const LessonList = () => {
<Breadcrumb>
<BreadcrumbItem>
<BreadcrumbLink as={Link} to={getNavigationsValue('journal.main')}>
Журнал
{t('journal.pl.common.journal')}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage>
<BreadcrumbLink href="#">Курс</BreadcrumbLink>
<BreadcrumbLink href="#">{t('journal.pl.common.course')}</BreadcrumbLink>
</BreadcrumbItem>
</Breadcrumb>
</BreadcrumbsWrapper>
@@ -242,8 +243,8 @@ const LessonList = () => {
}}
error={(crLQuery.error as any)?.error}
lesson={editLesson}
title={editLesson ? 'Редактирование лекции' : 'Создание лекции'}
nameButton={editLesson ? 'Редактировать' : 'Создать'}
title={editLesson ? t('journal.pl.lesson.editTitle') : t('journal.pl.lesson.createTitle')}
nameButton={editLesson ? t('journal.pl.edit') : t('journal.pl.common.create')}
/>
) : (
<Button
@@ -251,7 +252,7 @@ const LessonList = () => {
colorScheme="green"
onClick={() => setShowForm(true)}
>
Добавить
{t('journal.pl.common.create')}
</Button>
)}
</Box>
@@ -272,15 +273,15 @@ const LessonList = () => {
<Tr>
{isTeacher(user) && (
<Th align="center" width={1}>
ссылка
{t('journal.pl.lesson.link')}
</Th>
)}
<Th textAlign="center" width={1}>
{groupByDate ? 'Время' : 'Дата'}
{groupByDate ? t('journal.pl.lesson.time') : t('journal.pl.common.date')}
</Th>
<Th width="100%">Название</Th>
{isTeacher(user) && <Th>action</Th>}
<Th isNumeric>Отмечено</Th>
<Th width="100%">{t('journal.pl.common.name')}</Th>
{isTeacher(user) && <Th>{t('journal.pl.lesson.action')}</Th>}
<Th isNumeric>{t('journal.pl.common.marked')}</Th>
</Tr>
</Thead>
<Tbody>