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,
|
||||
@@ -27,6 +28,7 @@ import { StatusBadge } from '../../components/StatusBadge'
|
||||
import type { ChallengeSubmission, SubmissionStatus, ChallengeTask, ChallengeUser } from '../../types/challenge'
|
||||
|
||||
export const SubmissionsPage: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { data: submissions, isLoading, error, refetch } = useGetAllSubmissionsQuery()
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
@@ -34,11 +36,11 @@ export const SubmissionsPage: React.FC = () => {
|
||||
const [selectedSubmission, setSelectedSubmission] = useState<ChallengeSubmission | null>(null)
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner message="Загрузка попыток..." />
|
||||
return <LoadingSpinner message={t('challenge.admin.submissions.loading')} />
|
||||
}
|
||||
|
||||
if (error || !submissions) {
|
||||
return <ErrorAlert message="Не удалось загрузить список попыток" onRetry={refetch} />
|
||||
return <ErrorAlert message={t('challenge.admin.submissions.load.error')} onRetry={refetch} />
|
||||
}
|
||||
|
||||
const filteredSubmissions = submissions.filter((submission) => {
|
||||
@@ -69,28 +71,28 @@ export const SubmissionsPage: React.FC = () => {
|
||||
const submitted = new Date(submission.submittedAt).getTime()
|
||||
const checked = new Date(submission.checkedAt).getTime()
|
||||
const diff = Math.round((checked - submitted) / 1000)
|
||||
return `${diff} сек`
|
||||
return t('challenge.admin.submissions.check.time', { time: diff })
|
||||
}
|
||||
|
||||
const statusOptions = createListCollection({
|
||||
items: [
|
||||
{ label: 'Все статусы', value: 'all' },
|
||||
{ label: 'Принято', value: 'accepted' },
|
||||
{ label: 'Доработка', value: 'needs_revision' },
|
||||
{ label: 'Проверяется', value: 'in_progress' },
|
||||
{ label: 'Ожидает', value: 'pending' },
|
||||
{ label: t('challenge.admin.submissions.status.all'), value: 'all' },
|
||||
{ label: t('challenge.admin.submissions.status.accepted'), value: 'accepted' },
|
||||
{ label: t('challenge.admin.submissions.status.needs.revision'), value: 'needs_revision' },
|
||||
{ label: t('challenge.admin.submissions.status.in.progress'), value: 'in_progress' },
|
||||
{ label: t('challenge.admin.submissions.status.pending'), value: 'pending' },
|
||||
],
|
||||
})
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading mb={6}>Попытки решений</Heading>
|
||||
<Heading mb={6}>{t('challenge.admin.submissions.title')}</Heading>
|
||||
|
||||
{/* Filters */}
|
||||
{submissions.length > 0 && (
|
||||
<HStack mb={4} gap={4}>
|
||||
<Input
|
||||
placeholder="Поиск по пользователю или заданию..."
|
||||
placeholder={t('challenge.admin.submissions.search.placeholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
maxW="400px"
|
||||
@@ -102,7 +104,7 @@ export const SubmissionsPage: React.FC = () => {
|
||||
maxW="200px"
|
||||
>
|
||||
<Select.Trigger>
|
||||
<Select.ValueText placeholder="Статус" />
|
||||
<Select.ValueText placeholder={t('challenge.admin.submissions.filter.status')} />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{statusOptions.items.map((option) => (
|
||||
@@ -116,21 +118,21 @@ export const SubmissionsPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{filteredSubmissions.length === 0 && submissions.length === 0 ? (
|
||||
<EmptyState title="Нет попыток" description="Попытки появятся после отправки решений" />
|
||||
<EmptyState title={t('challenge.admin.submissions.empty.title')} description={t('challenge.admin.submissions.empty.description')} />
|
||||
) : filteredSubmissions.length === 0 ? (
|
||||
<EmptyState title="Ничего не найдено" description="Попробуйте изменить фильтры" />
|
||||
<EmptyState title={t('challenge.admin.submissions.search.empty.title')} description={t('challenge.admin.submissions.search.empty.description')} />
|
||||
) : (
|
||||
<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>Попытка</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>Дата отправки</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>Время проверки</Table.ColumnHeader>
|
||||
<Table.ColumnHeader textAlign="right">Действия</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>{t('challenge.admin.submissions.table.user')}</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>{t('challenge.admin.submissions.table.task')}</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>{t('challenge.admin.submissions.table.status')}</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>{t('challenge.admin.submissions.table.attempt')}</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>{t('challenge.admin.submissions.table.submitted')}</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>{t('challenge.admin.submissions.table.check.time')}</Table.ColumnHeader>
|
||||
<Table.ColumnHeader textAlign="right">{t('challenge.admin.submissions.table.actions')}</Table.ColumnHeader>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
@@ -167,7 +169,7 @@ export const SubmissionsPage: React.FC = () => {
|
||||
colorPalette="teal"
|
||||
onClick={() => setSelectedSubmission(submission)}
|
||||
>
|
||||
Детали
|
||||
{t('challenge.admin.submissions.button.details')}
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
@@ -199,6 +201,8 @@ const SubmissionDetailsModal: React.FC<SubmissionDetailsModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!submission) return null
|
||||
|
||||
const user = submission.user as ChallengeUser
|
||||
@@ -215,7 +219,7 @@ const SubmissionDetailsModal: React.FC<SubmissionDetailsModalProps> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const getCheckTime = () => {
|
||||
const getCheckTimeValue = () => {
|
||||
if (!submission.checkedAt) return null
|
||||
const submitted = new Date(submission.submittedAt).getTime()
|
||||
const checked = new Date(submission.checkedAt).getTime()
|
||||
@@ -226,7 +230,7 @@ const SubmissionDetailsModal: React.FC<SubmissionDetailsModalProps> = ({
|
||||
<DialogRoot open={isOpen} onOpenChange={(e) => !e.open && onClose()} size="xl">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Детали попытки #{submission.attemptNumber}</DialogTitle>
|
||||
<DialogTitle>{t('challenge.admin.submissions.details.title')} #{submission.attemptNumber}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<VStack gap={6} align="stretch">
|
||||
@@ -235,13 +239,13 @@ const SubmissionDetailsModal: React.FC<SubmissionDetailsModalProps> = ({
|
||||
<HStack mb={4} justify="space-between">
|
||||
<Box>
|
||||
<Text fontSize="sm" color="gray.600" mb={1}>
|
||||
Пользователь
|
||||
{t('challenge.admin.submissions.details.user')}
|
||||
</Text>
|
||||
<Text fontWeight="bold">{user.nickname}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fontSize="sm" color="gray.600" mb={1}>
|
||||
Статус
|
||||
{t('challenge.admin.submissions.details.status')}
|
||||
</Text>
|
||||
<StatusBadge status={submission.status} />
|
||||
</Box>
|
||||
@@ -249,15 +253,15 @@ const SubmissionDetailsModal: React.FC<SubmissionDetailsModalProps> = ({
|
||||
|
||||
<VStack align="stretch" gap={2}>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Отправлено:</strong> {formatDate(submission.submittedAt)}
|
||||
<strong>{t('challenge.admin.submissions.details.submitted')}</strong> {formatDate(submission.submittedAt)}
|
||||
</Text>
|
||||
{submission.checkedAt && (
|
||||
<>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Проверено:</strong> {formatDate(submission.checkedAt)}
|
||||
<strong>{t('challenge.admin.submissions.details.checked')}</strong> {formatDate(submission.checkedAt)}
|
||||
</Text>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Время проверки:</strong> {getCheckTime()} сек
|
||||
<strong>{t('challenge.admin.submissions.details.check.time')}</strong> {t('challenge.admin.submissions.check.time', { time: getCheckTimeValue() })}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
@@ -267,7 +271,7 @@ const SubmissionDetailsModal: React.FC<SubmissionDetailsModalProps> = ({
|
||||
{/* Task */}
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2}>
|
||||
Задание: {task.title}
|
||||
{t('challenge.admin.submissions.details.task')} {task.title}
|
||||
</Text>
|
||||
<Box
|
||||
p={4}
|
||||
@@ -285,7 +289,7 @@ const SubmissionDetailsModal: React.FC<SubmissionDetailsModalProps> = ({
|
||||
{/* Solution */}
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2}>
|
||||
Решение пользователя:
|
||||
{t('challenge.admin.submissions.details.solution')}
|
||||
</Text>
|
||||
<Box
|
||||
p={4}
|
||||
@@ -311,7 +315,7 @@ const SubmissionDetailsModal: React.FC<SubmissionDetailsModalProps> = ({
|
||||
{submission.feedback && (
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2}>
|
||||
Обратная связь от LLM:
|
||||
{t('challenge.admin.submissions.details.feedback')}
|
||||
</Text>
|
||||
<Box
|
||||
p={4}
|
||||
@@ -329,7 +333,7 @@ const SubmissionDetailsModal: React.FC<SubmissionDetailsModalProps> = ({
|
||||
<DialogFooter>
|
||||
<DialogActionTrigger asChild>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Закрыть
|
||||
{t('challenge.admin.submissions.details.close')}
|
||||
</Button>
|
||||
</DialogActionTrigger>
|
||||
</DialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user