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,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>
|
||||
|
||||
Reference in New Issue
Block a user