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,
|
||||
@@ -29,6 +30,7 @@ export const ChainFormPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const isEdit = !!id
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: chain, isLoading: isLoadingChain, error: loadError } = useGetChainQuery(id!, {
|
||||
skip: !id,
|
||||
@@ -53,8 +55,8 @@ export const ChainFormPage: React.FC = () => {
|
||||
|
||||
if (!name.trim()) {
|
||||
toaster.create({
|
||||
title: 'Ошибка валидации',
|
||||
description: 'Введите название цепочки',
|
||||
title: t('challenge.admin.common.validation.error'),
|
||||
description: t('challenge.admin.chains.validation.enter.name'),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
@@ -62,8 +64,8 @@ export const ChainFormPage: React.FC = () => {
|
||||
|
||||
if (selectedTasks.length === 0) {
|
||||
toaster.create({
|
||||
title: 'Ошибка валидации',
|
||||
description: 'Добавьте хотя бы одно задание',
|
||||
title: t('challenge.admin.common.validation.error'),
|
||||
description: t('challenge.admin.chains.validation.add.task'),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
@@ -81,8 +83,8 @@ export const ChainFormPage: React.FC = () => {
|
||||
},
|
||||
}).unwrap()
|
||||
toaster.create({
|
||||
title: 'Успешно',
|
||||
description: 'Цепочка обновлена',
|
||||
title: t('challenge.admin.common.success'),
|
||||
description: t('challenge.admin.chains.updated'),
|
||||
type: 'success',
|
||||
})
|
||||
} else {
|
||||
@@ -91,16 +93,24 @@ export const ChainFormPage: React.FC = () => {
|
||||
tasks: taskIds,
|
||||
}).unwrap()
|
||||
toaster.create({
|
||||
title: 'Успешно',
|
||||
description: 'Цепочка создана',
|
||||
title: t('challenge.admin.common.success'),
|
||||
description: t('challenge.admin.chains.created'),
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
navigate(URLs.chains)
|
||||
} 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.chains.save.error')
|
||||
|
||||
toaster.create({
|
||||
title: 'Ошибка',
|
||||
description: err?.data?.error?.message || 'Не удалось сохранить цепочку',
|
||||
title: t('challenge.admin.common.error'),
|
||||
description: errorMessage,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
@@ -131,19 +141,19 @@ export const ChainFormPage: React.FC = () => {
|
||||
}
|
||||
|
||||
if (isEdit && isLoadingChain) {
|
||||
return <LoadingSpinner message="Загрузка цепочки..." />
|
||||
return <LoadingSpinner message={t('challenge.admin.chains.loading')} />
|
||||
}
|
||||
|
||||
if (isEdit && loadError) {
|
||||
return <ErrorAlert message="Не удалось загрузить цепочку" />
|
||||
return <ErrorAlert message={t('challenge.admin.chains.load.error')} />
|
||||
}
|
||||
|
||||
if (isLoadingTasks) {
|
||||
return <LoadingSpinner message="Загрузка заданий..." />
|
||||
return <LoadingSpinner message={t('challenge.admin.common.loading.tasks')} />
|
||||
}
|
||||
|
||||
if (!allTasks) {
|
||||
return <ErrorAlert message="Не удалось загрузить список заданий" />
|
||||
return <ErrorAlert message={t('challenge.admin.chains.tasks.load.error')} />
|
||||
}
|
||||
|
||||
const isLoading = isCreating || isUpdating
|
||||
@@ -156,7 +166,7 @@ export const ChainFormPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading mb={6}>{isEdit ? 'Редактировать цепочку' : 'Создать цепочку'}</Heading>
|
||||
<Heading mb={6}>{isEdit ? t('challenge.admin.chains.edit.title') : t('challenge.admin.chains.create.title')}</Heading>
|
||||
|
||||
<Box
|
||||
as="form"
|
||||
@@ -171,11 +181,11 @@ export const ChainFormPage: React.FC = () => {
|
||||
<VStack gap={6} align="stretch">
|
||||
{/* Name */}
|
||||
<Field.Root required>
|
||||
<Field.Label>Название цепочки</Field.Label>
|
||||
<Field.Label>{t('challenge.admin.chains.field.name')}</Field.Label>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Введите название цепочки"
|
||||
placeholder={t('challenge.admin.chains.field.name.placeholder')}
|
||||
maxLength={255}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -184,7 +194,7 @@ export const ChainFormPage: React.FC = () => {
|
||||
{/* Selected Tasks */}
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={3}>
|
||||
Задания в цепочке ({selectedTasks.length})
|
||||
{t('challenge.admin.chains.selected.tasks')} ({selectedTasks.length})
|
||||
</Text>
|
||||
{selectedTasks.length === 0 ? (
|
||||
<Box
|
||||
@@ -195,7 +205,7 @@ export const ChainFormPage: React.FC = () => {
|
||||
borderRadius="md"
|
||||
textAlign="center"
|
||||
>
|
||||
<Text color="gray.500">Добавьте задания из списка ниже</Text>
|
||||
<Text color="gray.500">{t('challenge.admin.chains.selected.tasks.empty')}</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<VStack gap={2} align="stretch">
|
||||
@@ -255,10 +265,10 @@ export const ChainFormPage: React.FC = () => {
|
||||
{/* Available Tasks */}
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={3}>
|
||||
Доступные задания
|
||||
{t('challenge.admin.chains.available.tasks')}
|
||||
</Text>
|
||||
<Input
|
||||
placeholder="Поиск заданий..."
|
||||
placeholder={t('challenge.admin.chains.search.placeholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
mb={3}
|
||||
@@ -273,8 +283,8 @@ export const ChainFormPage: React.FC = () => {
|
||||
>
|
||||
<Text color="gray.500">
|
||||
{allTasks.length === selectedTasks.length
|
||||
? 'Все задания уже добавлены'
|
||||
: 'Ничего не найдено'}
|
||||
? t('challenge.admin.chains.all.tasks.added')
|
||||
: t('challenge.admin.common.not.found')}
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
@@ -295,7 +305,7 @@ export const ChainFormPage: React.FC = () => {
|
||||
>
|
||||
<Text>{task.title}</Text>
|
||||
<Button size="sm" colorPalette="teal" variant="ghost">
|
||||
+ Добавить
|
||||
{t('challenge.admin.chains.button.add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
))}
|
||||
@@ -306,10 +316,10 @@ export const ChainFormPage: React.FC = () => {
|
||||
{/* Actions */}
|
||||
<HStack gap={3} justify="flex-end">
|
||||
<Button variant="outline" onClick={() => navigate(URLs.chains)} 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.chains.button.save') : t('challenge.admin.chains.button.create')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Box,
|
||||
Heading,
|
||||
@@ -22,6 +23,7 @@ import { toaster } from '../../components/ui/toaster'
|
||||
|
||||
export const ChainsListPage: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
const { data: chains, isLoading, error, refetch } = useGetChainsQuery()
|
||||
const [deleteChain, { isLoading: isDeleting }] = useDeleteChainMutation()
|
||||
|
||||
@@ -34,26 +36,26 @@ export const ChainsListPage: React.FC = () => {
|
||||
try {
|
||||
await deleteChain(chainToDelete.id).unwrap()
|
||||
toaster.create({
|
||||
title: 'Успешно',
|
||||
description: 'Цепочка удалена',
|
||||
title: t('challenge.admin.common.success'),
|
||||
description: t('challenge.admin.chains.deleted'),
|
||||
type: 'success',
|
||||
})
|
||||
setChainToDelete(null)
|
||||
} catch (err) {
|
||||
toaster.create({
|
||||
title: 'Ошибка',
|
||||
description: 'Не удалось удалить цепочку',
|
||||
title: t('challenge.admin.common.error'),
|
||||
description: t('challenge.admin.chains.delete.error'),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner message="Загрузка цепочек..." />
|
||||
return <LoadingSpinner message={t('challenge.admin.chains.list.loading')} />
|
||||
}
|
||||
|
||||
if (error || !chains) {
|
||||
return <ErrorAlert message="Не удалось загрузить список цепочек" onRetry={refetch} />
|
||||
return <ErrorAlert message={t('challenge.admin.chains.list.load.error')} onRetry={refetch} />
|
||||
}
|
||||
|
||||
const filteredChains = chains.filter((chain) =>
|
||||
@@ -71,16 +73,16 @@ export const ChainsListPage: React.FC = () => {
|
||||
return (
|
||||
<Box>
|
||||
<Flex justify="space-between" align="center" mb={6}>
|
||||
<Heading>Цепочки заданий</Heading>
|
||||
<Heading>{t('challenge.admin.chains.list.title')}</Heading>
|
||||
<Button colorPalette="teal" onClick={() => navigate(URLs.chainNew)}>
|
||||
+ Создать цепочку
|
||||
{t('challenge.admin.chains.list.create.button')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{chains.length > 0 && (
|
||||
<Box mb={4}>
|
||||
<Input
|
||||
placeholder="Поиск по названию..."
|
||||
placeholder={t('challenge.admin.chains.list.search.placeholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
maxW="400px"
|
||||
@@ -90,25 +92,25 @@ export const ChainsListPage: React.FC = () => {
|
||||
|
||||
{filteredChains.length === 0 && chains.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Нет цепочек"
|
||||
description="Создайте первую цепочку заданий"
|
||||
actionLabel="Создать цепочку"
|
||||
title={t('challenge.admin.chains.list.empty.title')}
|
||||
description={t('challenge.admin.chains.list.empty.description')}
|
||||
actionLabel={t('challenge.admin.chains.list.empty.action')}
|
||||
onAction={() => navigate(URLs.chainNew)}
|
||||
/>
|
||||
) : filteredChains.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Ничего не найдено"
|
||||
description={`По запросу "${searchQuery}" ничего не найдено`}
|
||||
title={t('challenge.admin.common.not.found')}
|
||||
description={t('challenge.admin.chains.list.search.empty', { query: searchQuery })}
|
||||
/>
|
||||
) : (
|
||||
<Box bg="white" borderRadius="lg" boxShadow="sm" borderWidth="1px" borderColor="gray.200" overflowX="auto">
|
||||
<Table.Root size="sm">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeader>Название</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>Количество заданий</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>Дата создания</Table.ColumnHeader>
|
||||
<Table.ColumnHeader textAlign="right">Действия</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>{t('challenge.admin.chains.list.table.name')}</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>{t('challenge.admin.chains.list.table.tasks.count')}</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>{t('challenge.admin.chains.list.table.created')}</Table.ColumnHeader>
|
||||
<Table.ColumnHeader textAlign="right">{t('challenge.admin.chains.list.table.actions')}</Table.ColumnHeader>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
@@ -117,7 +119,7 @@ export const ChainsListPage: React.FC = () => {
|
||||
<Table.Cell fontWeight="medium">{chain.name}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge colorPalette="teal" variant="subtle">
|
||||
{chain.tasks.length} заданий
|
||||
{chain.tasks.length} {t('challenge.admin.chains.list.badge.tasks')}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
@@ -132,7 +134,7 @@ export const ChainsListPage: React.FC = () => {
|
||||
variant="ghost"
|
||||
onClick={() => navigate(URLs.chainEdit(chain.id))}
|
||||
>
|
||||
Редактировать
|
||||
{t('challenge.admin.chains.list.button.edit')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -140,7 +142,7 @@ export const ChainsListPage: React.FC = () => {
|
||||
colorPalette="red"
|
||||
onClick={() => setChainToDelete(chain)}
|
||||
>
|
||||
Удалить
|
||||
{t('challenge.admin.chains.list.button.delete')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</Table.Cell>
|
||||
@@ -155,9 +157,9 @@ export const ChainsListPage: React.FC = () => {
|
||||
isOpen={!!chainToDelete}
|
||||
onClose={() => setChainToDelete(null)}
|
||||
onConfirm={handleDeleteChain}
|
||||
title="Удалить цепочку"
|
||||
message={`Вы уверены, что хотите удалить цепочку "${chainToDelete?.name}"? Это действие нельзя отменить.`}
|
||||
confirmLabel="Удалить"
|
||||
title={t('challenge.admin.chains.delete.confirm.title')}
|
||||
message={t('challenge.admin.chains.delete.confirm.message', { name: chainToDelete?.name })}
|
||||
confirmLabel={t('challenge.admin.chains.delete.confirm.button')}
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user