Enhance localization support by integrating i18next for translations across various components and pages. Update UI elements to utilize translated strings for improved user experience in both English and Russian. Additionally, refactor the Toaster component to support a context-based approach for toast notifications.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Box,
|
||||
Heading,
|
||||
@@ -9,8 +10,6 @@ import {
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Flex,
|
||||
Stack,
|
||||
Field,
|
||||
Tabs,
|
||||
} from '@chakra-ui/react'
|
||||
@@ -29,7 +28,7 @@ export const TaskFormPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const isEdit = !!id
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { data: task, isLoading: isLoadingTask, error: loadError } = useGetTaskQuery(id!, {
|
||||
skip: !id,
|
||||
})
|
||||
@@ -54,8 +53,8 @@ export const TaskFormPage: React.FC = () => {
|
||||
|
||||
if (!title.trim() || !description.trim()) {
|
||||
toaster.create({
|
||||
title: 'Ошибка валидации',
|
||||
description: 'Заполните обязательные поля',
|
||||
title: t('challenge.admin.common.validation.error'),
|
||||
description: t('challenge.admin.tasks.validation.fill.required.fields'),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
@@ -72,8 +71,8 @@ export const TaskFormPage: React.FC = () => {
|
||||
},
|
||||
}).unwrap()
|
||||
toaster.create({
|
||||
title: 'Успешно',
|
||||
description: 'Задание обновлено',
|
||||
title: t('challenge.admin.common.success'),
|
||||
description: t('challenge.admin.tasks.updated'),
|
||||
type: 'success',
|
||||
})
|
||||
} else {
|
||||
@@ -83,34 +82,42 @@ export const TaskFormPage: React.FC = () => {
|
||||
hiddenInstructions: hiddenInstructions.trim() || undefined,
|
||||
}).unwrap()
|
||||
toaster.create({
|
||||
title: 'Успешно',
|
||||
description: 'Задание создано',
|
||||
title: t('challenge.admin.common.success'),
|
||||
description: t('challenge.admin.tasks.created'),
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
navigate(URLs.tasks)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage =
|
||||
(err && typeof err === 'object' && 'data' in err &&
|
||||
err.data && typeof err.data === 'object' && 'error' in err.data &&
|
||||
err.data.error && typeof err.data.error === 'object' && 'message' in err.data.error &&
|
||||
typeof err.data.error.message === 'string')
|
||||
? err.data.error.message
|
||||
: t('challenge.admin.tasks.save.error')
|
||||
|
||||
toaster.create({
|
||||
title: 'Ошибка',
|
||||
description: err?.data?.error?.message || 'Не удалось сохранить задание',
|
||||
title: t('challenge.admin.common.error'),
|
||||
description: errorMessage,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isEdit && isLoadingTask) {
|
||||
return <LoadingSpinner message="Загрузка задания..." />
|
||||
return <LoadingSpinner message={t('challenge.admin.tasks.loading')} />
|
||||
}
|
||||
|
||||
if (isEdit && loadError) {
|
||||
return <ErrorAlert message="Не удалось загрузить задание" />
|
||||
return <ErrorAlert message={t('challenge.admin.tasks.load.error')} />
|
||||
}
|
||||
|
||||
const isLoading = isCreating || isUpdating
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading mb={6}>{isEdit ? 'Редактировать задание' : 'Создать задание'}</Heading>
|
||||
<Heading mb={6}>{isEdit ? t('challenge.admin.tasks.edit.title') : t('challenge.admin.tasks.create.title')}</Heading>
|
||||
|
||||
<Box
|
||||
as="form"
|
||||
@@ -125,33 +132,33 @@ export const TaskFormPage: React.FC = () => {
|
||||
<VStack gap={6} align="stretch">
|
||||
{/* Title */}
|
||||
<Field.Root required>
|
||||
<Field.Label>Название задания</Field.Label>
|
||||
<Field.Label>{t('challenge.admin.tasks.field.title')}</Field.Label>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Введите название задания"
|
||||
placeholder={t('challenge.admin.tasks.field.title.placeholder')}
|
||||
maxLength={255}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Field.HelperText>Максимум 255 символов</Field.HelperText>
|
||||
<Field.HelperText>{t('challenge.admin.tasks.field.title.helper')}</Field.HelperText>
|
||||
</Field.Root>
|
||||
|
||||
{/* Description with Markdown */}
|
||||
<Field.Root required>
|
||||
<Field.Label>Описание (Markdown)</Field.Label>
|
||||
<Field.Label>{t('challenge.admin.tasks.field.description')}</Field.Label>
|
||||
<Tabs.Root
|
||||
value={showDescPreview ? 'preview' : 'editor'}
|
||||
onValueChange={(e) => setShowDescPreview(e.value === 'preview')}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="editor">Редактор</Tabs.Trigger>
|
||||
<Tabs.Trigger value="preview">Превью</Tabs.Trigger>
|
||||
<Tabs.Trigger value="editor">{t('challenge.admin.tasks.tab.editor')}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="preview">{t('challenge.admin.tasks.tab.preview')}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="editor" pt={4}>
|
||||
<Textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="# Заголовок задания Описание задания в формате Markdown..."
|
||||
placeholder={t('challenge.admin.tasks.field.description.placeholder')}
|
||||
rows={15}
|
||||
fontFamily="monospace"
|
||||
disabled={isLoading}
|
||||
@@ -172,13 +179,13 @@ export const TaskFormPage: React.FC = () => {
|
||||
</Box>
|
||||
) : (
|
||||
<Text color="gray.400" fontStyle="italic">
|
||||
Предпросмотр появится здесь...
|
||||
{t('challenge.admin.tasks.preview.empty')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
<Field.HelperText>Используйте Markdown для форматирования текста</Field.HelperText>
|
||||
<Field.HelperText>{t('challenge.admin.tasks.field.description.helper')}</Field.HelperText>
|
||||
</Field.Root>
|
||||
|
||||
{/* Hidden Instructions */}
|
||||
@@ -186,22 +193,21 @@ export const TaskFormPage: React.FC = () => {
|
||||
<Box bg="purple.50" p={4} borderRadius="md" borderWidth="1px" borderColor="purple.200">
|
||||
<HStack mb={2}>
|
||||
<Text fontWeight="bold" color="purple.800">
|
||||
🔒 Скрытые инструкции для LLM
|
||||
{t('challenge.admin.tasks.field.hidden.instructions')}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="sm" color="purple.700" mb={3}>
|
||||
Эти инструкции будут переданы LLM при проверке решений студентов. Студенты их не
|
||||
увидят.
|
||||
{t('challenge.admin.tasks.field.hidden.instructions.description')}
|
||||
</Text>
|
||||
<Textarea
|
||||
value={hiddenInstructions}
|
||||
onChange={(e) => setHiddenInstructions(e.target.value)}
|
||||
placeholder="Например: Проверь, что сложность алгоритма O(n log n). Код должен обрабатывать edge cases..."
|
||||
placeholder={t('challenge.admin.tasks.field.hidden.instructions.placeholder')}
|
||||
rows={6}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Field.HelperText>
|
||||
Опционально. Используйте для тонкой настройки проверки LLM.
|
||||
{t('challenge.admin.tasks.field.hidden.instructions.helper')}
|
||||
</Field.HelperText>
|
||||
</Box>
|
||||
</Field.Root>
|
||||
@@ -210,17 +216,17 @@ export const TaskFormPage: React.FC = () => {
|
||||
{isEdit && task && (
|
||||
<Box p={4} bg="gray.50" borderRadius="md">
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Создано:</strong>{' '}
|
||||
<strong>{t('challenge.admin.tasks.meta.created')}</strong>{' '}
|
||||
{new Date(task.createdAt).toLocaleString('ru-RU')}
|
||||
</Text>
|
||||
{task.creator && (
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Автор:</strong> {task.creator.preferred_username}
|
||||
<strong>{t('challenge.admin.tasks.meta.author')}</strong> {task.creator.preferred_username}
|
||||
</Text>
|
||||
)}
|
||||
{task.updatedAt !== task.createdAt && (
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Обновлено:</strong>{' '}
|
||||
<strong>{t('challenge.admin.tasks.meta.updated')}</strong>{' '}
|
||||
{new Date(task.updatedAt).toLocaleString('ru-RU')}
|
||||
</Text>
|
||||
)}
|
||||
@@ -230,10 +236,10 @@ export const TaskFormPage: React.FC = () => {
|
||||
{/* Actions */}
|
||||
<HStack gap={3} justify="flex-end">
|
||||
<Button variant="outline" onClick={() => navigate(URLs.tasks)} disabled={isLoading}>
|
||||
Отмена
|
||||
{t('challenge.admin.common.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" colorPalette="teal" loading={isLoading}>
|
||||
{isEdit ? 'Сохранить изменения' : 'Создать задание'}
|
||||
<Button type="submit" colorPalette="teal" disabled={isLoading}>
|
||||
{isEdit ? t('challenge.admin.tasks.button.save') : t('challenge.admin.tasks.button.create')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
Reference in New Issue
Block a user