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:
Primakov Alexandr Alexandrovich
2025-11-04 10:25:12 +03:00
parent daa44521b9
commit 44a7ac2bfd
19 changed files with 892 additions and 293 deletions

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
Box,
Heading,
@@ -23,19 +24,19 @@ import { useGetUsersQuery, useGetUserStatsQuery } from '../../__data__/api/api'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { ErrorAlert } from '../../components/ErrorAlert'
import { EmptyState } from '../../components/EmptyState'
import type { ChallengeUser } from '../../types/challenge'
export const UsersPage: React.FC = () => {
const { t } = useTranslation()
const { data: users, isLoading, error, refetch } = useGetUsersQuery()
const [searchQuery, setSearchQuery] = useState('')
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
if (isLoading) {
return <LoadingSpinner message="Загрузка пользователей..." />
return <LoadingSpinner message={t('challenge.admin.users.loading')} />
}
if (error || !users) {
return <ErrorAlert message="Не удалось загрузить список пользователей" onRetry={refetch} />
return <ErrorAlert message={t('challenge.admin.users.load.error')} onRetry={refetch} />
}
const filteredUsers = users.filter((user) =>
@@ -52,12 +53,12 @@ export const UsersPage: React.FC = () => {
return (
<Box>
<Heading mb={6}>Пользователи</Heading>
<Heading mb={6}>{t('challenge.admin.users.title')}</Heading>
{users.length > 0 && (
<Box mb={4}>
<Input
placeholder="Поиск по nickname..."
placeholder={t('challenge.admin.users.search.placeholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
maxW="400px"
@@ -66,21 +67,21 @@ export const UsersPage: React.FC = () => {
)}
{filteredUsers.length === 0 && users.length === 0 ? (
<EmptyState title="Нет пользователей" description="Пользователи появятся после регистрации" />
<EmptyState title={t('challenge.admin.users.empty.title')} description={t('challenge.admin.users.empty.description')} />
) : filteredUsers.length === 0 ? (
<EmptyState
title="Ничего не найдено"
description={`По запросу "${searchQuery}" ничего не найдено`}
title={t('challenge.admin.common.not.found')}
description={t('challenge.admin.users.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>Nickname</Table.ColumnHeader>
<Table.ColumnHeader>ID</Table.ColumnHeader>
<Table.ColumnHeader>Дата регистрации</Table.ColumnHeader>
<Table.ColumnHeader textAlign="right">Действия</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.table.nickname')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.table.id')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.table.registered')}</Table.ColumnHeader>
<Table.ColumnHeader textAlign="right">{t('challenge.admin.users.table.actions')}</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
@@ -104,7 +105,7 @@ export const UsersPage: React.FC = () => {
colorPalette="teal"
onClick={() => setSelectedUserId(user.id)}
>
Статистика
{t('challenge.admin.users.button.stats')}
</Button>
</Table.Cell>
</Table.Row>
@@ -131,6 +132,7 @@ interface UserStatsModalProps {
}
const UserStatsModal: React.FC<UserStatsModalProps> = ({ userId, isOpen, onClose }) => {
const { t } = useTranslation()
const { data: stats, isLoading } = useGetUserStatsQuery(userId!, {
skip: !userId,
})
@@ -139,20 +141,20 @@ const UserStatsModal: React.FC<UserStatsModalProps> = ({ userId, isOpen, onClose
<DialogRoot open={isOpen} onOpenChange={(e) => !e.open && onClose()} size="xl">
<DialogContent>
<DialogHeader>
<DialogTitle>Статистика пользователя</DialogTitle>
<DialogTitle>{t('challenge.admin.users.stats.title')}</DialogTitle>
</DialogHeader>
<DialogBody>
{isLoading ? (
<LoadingSpinner message="Загрузка статистики..." />
<LoadingSpinner message={t('challenge.admin.users.stats.loading')} />
) : !stats ? (
<Text color="gray.600">Нет данных</Text>
<Text color="gray.600">{t('challenge.admin.users.stats.no.data')}</Text>
) : (
<VStack gap={6} align="stretch">
{/* Overview */}
<Grid templateColumns="repeat(auto-fit, minmax(150px, 1fr))" gap={4}>
<Box>
<Text fontSize="sm" color="gray.600">
Выполнено
{t('challenge.admin.users.stats.completed')}
</Text>
<Text fontSize="2xl" fontWeight="bold" color="green.600">
{stats.completedTasks}
@@ -160,7 +162,7 @@ const UserStatsModal: React.FC<UserStatsModalProps> = ({ userId, isOpen, onClose
</Box>
<Box>
<Text fontSize="sm" color="gray.600">
Всего попыток
{t('challenge.admin.users.stats.total.submissions')}
</Text>
<Text fontSize="2xl" fontWeight="bold" color="blue.600">
{stats.totalSubmissions}
@@ -168,7 +170,7 @@ const UserStatsModal: React.FC<UserStatsModalProps> = ({ userId, isOpen, onClose
</Box>
<Box>
<Text fontSize="sm" color="gray.600">
В процессе
{t('challenge.admin.users.stats.in.progress')}
</Text>
<Text fontSize="2xl" fontWeight="bold" color="orange.600">
{stats.inProgressTasks}
@@ -176,7 +178,7 @@ const UserStatsModal: React.FC<UserStatsModalProps> = ({ userId, isOpen, onClose
</Box>
<Box>
<Text fontSize="sm" color="gray.600">
Требует доработки
{t('challenge.admin.users.stats.needs.revision')}
</Text>
<Text fontSize="2xl" fontWeight="bold" color="red.600">
{stats.needsRevisionTasks}
@@ -188,7 +190,7 @@ const UserStatsModal: React.FC<UserStatsModalProps> = ({ userId, isOpen, onClose
{stats.chainStats.length > 0 && (
<Box>
<Text fontWeight="bold" mb={3}>
Прогресс по цепочкам
{t('challenge.admin.users.stats.chains.progress')}
</Text>
<VStack gap={3} align="stretch">
{stats.chainStats.map((chain) => (
@@ -201,7 +203,11 @@ const UserStatsModal: React.FC<UserStatsModalProps> = ({ userId, isOpen, onClose
{chain.completedTasks} / {chain.totalTasks}
</Text>
</HStack>
<Progress value={chain.progress} colorPalette="teal" size="sm" />
<Progress.Root value={chain.progress} colorPalette="teal" size="sm">
<Progress.Track>
<Progress.Range />
</Progress.Track>
</Progress.Root>
</Box>
))}
</VStack>
@@ -212,45 +218,39 @@ const UserStatsModal: React.FC<UserStatsModalProps> = ({ userId, isOpen, onClose
{stats.taskStats.length > 0 && (
<Box>
<Text fontWeight="bold" mb={3}>
Задания
{t('challenge.admin.users.stats.tasks')}
</Text>
<VStack gap={2} align="stretch" maxH="300px" overflowY="auto">
{stats.taskStats.map((taskStat) => (
<Box
key={taskStat.taskId}
p={3}
bg="gray.50"
borderRadius="md"
borderWidth="1px"
borderColor="gray.200"
>
<HStack justify="space-between" mb={1}>
<Text fontSize="sm" fontWeight="medium">
{taskStat.taskTitle}
{stats.taskStats.map((taskStat) => {
const getBadgeColor = () => {
if (taskStat.status === 'completed') return 'green'
if (taskStat.status === 'needs_revision') return 'red'
return 'gray'
}
return (
<Box
key={taskStat.taskId}
p={3}
bg="gray.50"
borderRadius="md"
borderWidth="1px"
borderColor="gray.200"
>
<HStack justify="space-between" mb={1}>
<Text fontSize="sm" fontWeight="medium">
{taskStat.taskTitle}
</Text>
<Badge colorPalette={getBadgeColor()}>
{t(`challenge.admin.users.stats.status.${taskStat.status}`)}
</Badge>
</HStack>
<Text fontSize="xs" color="gray.600">
{t('challenge.admin.users.stats.attempts')} {taskStat.totalAttempts}
</Text>
<Badge
colorPalette={
taskStat.status === 'completed'
? 'green'
: taskStat.status === 'needs_revision'
? 'red'
: 'gray'
}
>
{taskStat.status === 'completed'
? 'Завершено'
: taskStat.status === 'needs_revision'
? 'Доработка'
: taskStat.status === 'in_progress'
? 'В процессе'
: 'Не начато'}
</Badge>
</HStack>
<Text fontSize="xs" color="gray.600">
Попыток: {taskStat.totalAttempts}
</Text>
</Box>
))}
</Box>
)
})}
</VStack>
</Box>
)}
@@ -258,10 +258,10 @@ const UserStatsModal: React.FC<UserStatsModalProps> = ({ userId, isOpen, onClose
{/* Average Check Time */}
<Box p={3} bg="purple.50" borderRadius="md">
<Text fontSize="sm" color="gray.700" mb={1}>
Среднее время проверки
{t('challenge.admin.users.stats.avg.check.time')}
</Text>
<Text fontSize="lg" fontWeight="bold" color="purple.700">
{(stats.averageCheckTimeMs / 1000).toFixed(2)} сек
{t('challenge.admin.dashboard.check.time.value', { time: (stats.averageCheckTimeMs / 1000).toFixed(2) })}
</Text>
</Box>
</VStack>