Add Submission Details Page and Update Localization for Submissions Overview
This commit is contained in:
235
src/pages/submissions/SubmissionDetailsPage.tsx
Normal file
235
src/pages/submissions/SubmissionDetailsPage.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Box, Heading, Text, Button, HStack, VStack } from '@chakra-ui/react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { useGetUserSubmissionsQuery } from '../../__data__/api/api'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { ErrorAlert } from '../../components/ErrorAlert'
|
||||
import { StatusBadge } from '../../components/StatusBadge'
|
||||
import type { ChallengeTask, ChallengeUser } from '../../types/challenge'
|
||||
import { URLs } from '../../__data__/urls'
|
||||
|
||||
export const SubmissionDetailsPage: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { userId, submissionId } = useParams<{ userId: string; submissionId: string }>()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Получаем submissions для конкретного пользователя
|
||||
const { data: submissions, isLoading, error } = useGetUserSubmissionsQuery(
|
||||
{ userId: userId!, taskId: undefined },
|
||||
{ skip: !userId }
|
||||
)
|
||||
|
||||
const submission = submissions?.find((s) => s.id === submissionId)
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(URLs.submissions)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner message={t('challenge.admin.submissions.loading')} />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorAlert message={t('challenge.admin.submissions.load.error')} onRetry={handleBack} />
|
||||
}
|
||||
|
||||
if (!submission) {
|
||||
return (
|
||||
<Box>
|
||||
<Button variant="ghost" onClick={handleBack} mb={4}>
|
||||
← {t('challenge.admin.common.close')}
|
||||
</Button>
|
||||
<ErrorAlert
|
||||
message={t('challenge.admin.submissions.details.not.found')}
|
||||
onRetry={handleBack}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const rawUser = submission.user as ChallengeUser | string | undefined
|
||||
const rawTask = submission.task as ChallengeTask | string | undefined
|
||||
|
||||
const userNickname =
|
||||
rawUser && typeof rawUser === 'object' && 'nickname' in rawUser
|
||||
? rawUser.nickname ?? ''
|
||||
: typeof rawUser === 'string'
|
||||
? rawUser
|
||||
: ''
|
||||
|
||||
const taskTitle =
|
||||
rawTask && typeof rawTask === 'object' && 'title' in rawTask
|
||||
? rawTask.title ?? ''
|
||||
: typeof rawTask === 'string'
|
||||
? rawTask
|
||||
: ''
|
||||
|
||||
const taskDescription =
|
||||
rawTask && typeof rawTask === 'object' && 'description' in rawTask
|
||||
? rawTask.description ?? ''
|
||||
: ''
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
const getCheckTimeValue = () => {
|
||||
if (!submission.checkedAt) return null
|
||||
const submitted = new Date(submission.submittedAt).getTime()
|
||||
const checked = new Date(submission.checkedAt).getTime()
|
||||
return ((checked - submitted) / 1000).toFixed(2)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header with back button */}
|
||||
<HStack mb={6}>
|
||||
<Button variant="ghost" onClick={handleBack}>
|
||||
← {t('challenge.admin.common.close')}
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
<Heading mb={6}>
|
||||
{t('challenge.admin.submissions.details.title')} #{submission.attemptNumber}
|
||||
</Heading>
|
||||
|
||||
<VStack gap={6} align="stretch">
|
||||
{/* Meta */}
|
||||
<Box
|
||||
p={6}
|
||||
bg="white"
|
||||
borderRadius="lg"
|
||||
boxShadow="sm"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.200"
|
||||
>
|
||||
<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">{userNickname}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fontSize="sm" color="gray.600" mb={1}>
|
||||
{t('challenge.admin.submissions.details.status')}
|
||||
</Text>
|
||||
<StatusBadge status={submission.status} />
|
||||
</Box>
|
||||
</HStack>
|
||||
|
||||
<VStack align="stretch" gap={2}>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>{t('challenge.admin.submissions.details.submitted')}</strong>{' '}
|
||||
{formatDate(submission.submittedAt)}
|
||||
</Text>
|
||||
{submission.checkedAt && (
|
||||
<>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>{t('challenge.admin.submissions.details.checked')}</strong>{' '}
|
||||
{formatDate(submission.checkedAt)}
|
||||
</Text>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>{t('challenge.admin.submissions.details.check.time')}</strong>{' '}
|
||||
{t('challenge.admin.submissions.check.time', { time: getCheckTimeValue() })}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* Task */}
|
||||
<Box
|
||||
p={6}
|
||||
bg="white"
|
||||
borderRadius="lg"
|
||||
boxShadow="sm"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.200"
|
||||
>
|
||||
<Text fontWeight="bold" mb={4}>
|
||||
{t('challenge.admin.submissions.details.task')} {taskTitle}
|
||||
</Text>
|
||||
<Box
|
||||
p={4}
|
||||
bg="gray.50"
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.200"
|
||||
maxH="400px"
|
||||
overflowY="auto"
|
||||
>
|
||||
<ReactMarkdown>{taskDescription}</ReactMarkdown>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Solution */}
|
||||
<Box
|
||||
p={6}
|
||||
bg="white"
|
||||
borderRadius="lg"
|
||||
boxShadow="sm"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.200"
|
||||
>
|
||||
<Text fontWeight="bold" mb={4}>
|
||||
{t('challenge.admin.submissions.details.solution')}
|
||||
</Text>
|
||||
<Box
|
||||
p={4}
|
||||
bg="blue.50"
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor="blue.200"
|
||||
maxH="500px"
|
||||
overflowY="auto"
|
||||
>
|
||||
<Text
|
||||
fontFamily="monospace"
|
||||
fontSize="sm"
|
||||
whiteSpace="pre-wrap"
|
||||
wordBreak="break-word"
|
||||
>
|
||||
{submission.result}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Feedback */}
|
||||
{submission.feedback && (
|
||||
<Box
|
||||
p={6}
|
||||
bg="white"
|
||||
borderRadius="lg"
|
||||
boxShadow="sm"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.200"
|
||||
>
|
||||
<Text fontWeight="bold" mb={4}>
|
||||
{t('challenge.admin.submissions.details.feedback')}
|
||||
</Text>
|
||||
<Box
|
||||
p={4}
|
||||
bg={submission.status === 'accepted' ? 'green.50' : 'red.50'}
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor={submission.status === 'accepted' ? 'green.200' : 'red.200'}
|
||||
>
|
||||
<Text>{submission.feedback}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user