Add optional learningMaterial field to ChallengeTask model and update API endpoints. Introduce LearningMaterialViewer component for displaying additional educational content in Markdown format. Enhance TaskWorkspace to conditionally render learning materials, improving user experience with task-related resources.
Some checks failed
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
Some checks failed
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
This commit is contained in:
271
src/components/personal/LearningMaterialViewer.tsx
Normal file
271
src/components/personal/LearningMaterialViewer.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
HStack,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@chakra-ui/react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
interface LearningMaterialViewerProps {
|
||||
content: string
|
||||
linesPerPage?: number
|
||||
}
|
||||
|
||||
export const LearningMaterialViewer = ({
|
||||
content,
|
||||
linesPerPage = 30
|
||||
}: LearningMaterialViewerProps) => {
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
|
||||
// Разделяем контент на страницы по linesPerPage строк
|
||||
const pages = useMemo(() => {
|
||||
const lines = content.split('\n')
|
||||
const pagesArray: string[] = []
|
||||
|
||||
for (let i = 0; i < lines.length; i += linesPerPage) {
|
||||
const pageLines = lines.slice(i, i + linesPerPage)
|
||||
pagesArray.push(pageLines.join('\n'))
|
||||
}
|
||||
|
||||
return pagesArray
|
||||
}, [content, linesPerPage])
|
||||
|
||||
const totalPages = pages.length
|
||||
|
||||
if (totalPages === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const goToPrevious = () => {
|
||||
setCurrentPage(prev => Math.max(0, prev - 1))
|
||||
}
|
||||
|
||||
const goToNext = () => {
|
||||
setCurrentPage(prev => Math.min(totalPages - 1, prev + 1))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderWidth="1px"
|
||||
borderRadius="md"
|
||||
borderColor="blue.200"
|
||||
p={4}
|
||||
bg="blue.50"
|
||||
shadow="sm"
|
||||
>
|
||||
<VStack align="stretch" gap={3}>
|
||||
<HStack justify="space-between" align="center">
|
||||
<Text fontSize="lg" fontWeight="bold" color="blue.800">
|
||||
Дополнительные материалы
|
||||
</Text>
|
||||
{totalPages > 1 && (
|
||||
<Text fontSize="sm" color="blue.600">
|
||||
Страница {currentPage + 1} из {totalPages}
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<Box
|
||||
color="gray.700"
|
||||
fontSize="sm"
|
||||
lineHeight="1.7"
|
||||
css={{
|
||||
// Заголовки
|
||||
'& h1': {
|
||||
fontSize: '1.75em',
|
||||
fontWeight: '700',
|
||||
marginTop: '1.2em',
|
||||
marginBottom: '0.6em',
|
||||
color: '#2D3748',
|
||||
borderBottom: '2px solid #E2E8F0',
|
||||
paddingBottom: '0.3em'
|
||||
},
|
||||
'& h2': {
|
||||
fontSize: '1.5em',
|
||||
fontWeight: '600',
|
||||
marginTop: '1em',
|
||||
marginBottom: '0.5em',
|
||||
color: '#2D3748'
|
||||
},
|
||||
'& h3': {
|
||||
fontSize: '1.25em',
|
||||
fontWeight: '600',
|
||||
marginTop: '0.8em',
|
||||
marginBottom: '0.4em',
|
||||
color: '#2D3748'
|
||||
},
|
||||
'& h4': {
|
||||
fontSize: '1.1em',
|
||||
fontWeight: '600',
|
||||
marginTop: '0.6em',
|
||||
marginBottom: '0.3em',
|
||||
color: '#4A5568'
|
||||
},
|
||||
// Параграфы
|
||||
'& p': {
|
||||
marginTop: '0.75em',
|
||||
marginBottom: '0.75em',
|
||||
lineHeight: '1.8'
|
||||
},
|
||||
// Списки
|
||||
'& ul, & ol': {
|
||||
marginLeft: '1.5em',
|
||||
marginTop: '0.75em',
|
||||
marginBottom: '0.75em',
|
||||
paddingLeft: '0.5em'
|
||||
},
|
||||
'& li': {
|
||||
marginTop: '0.4em',
|
||||
marginBottom: '0.4em',
|
||||
lineHeight: '1.7'
|
||||
},
|
||||
'& li > p': {
|
||||
marginTop: '0.25em',
|
||||
marginBottom: '0.25em'
|
||||
},
|
||||
// Инлайн-код
|
||||
'& code': {
|
||||
backgroundColor: '#EDF2F7',
|
||||
color: '#C53030',
|
||||
padding: '0.15em 0.4em',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.9em',
|
||||
fontFamily: 'Monaco, Consolas, "Courier New", monospace',
|
||||
fontWeight: '500'
|
||||
},
|
||||
// Блоки кода
|
||||
'& pre': {
|
||||
backgroundColor: '#1A202C',
|
||||
color: '#E2E8F0',
|
||||
padding: '1em 1.2em',
|
||||
borderRadius: '8px',
|
||||
overflowX: 'auto',
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
border: '1px solid #2D3748',
|
||||
fontSize: '0.9em',
|
||||
lineHeight: '1.6'
|
||||
},
|
||||
'& pre code': {
|
||||
backgroundColor: 'transparent',
|
||||
color: '#E2E8F0',
|
||||
padding: '0',
|
||||
fontFamily: 'Monaco, Consolas, "Courier New", monospace'
|
||||
},
|
||||
// Цитаты
|
||||
'& blockquote': {
|
||||
borderLeft: '4px solid #4299E1',
|
||||
paddingLeft: '1em',
|
||||
paddingTop: '0.5em',
|
||||
paddingBottom: '0.5em',
|
||||
marginLeft: '0',
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
fontStyle: 'italic',
|
||||
color: '#4A5568',
|
||||
backgroundColor: '#EBF8FF',
|
||||
borderRadius: '0 4px 4px 0'
|
||||
},
|
||||
'& blockquote p': {
|
||||
marginTop: '0.25em',
|
||||
marginBottom: '0.25em'
|
||||
},
|
||||
// Ссылки
|
||||
'& a': {
|
||||
color: '#3182CE',
|
||||
textDecoration: 'underline',
|
||||
fontWeight: '500',
|
||||
transition: 'color 0.2s',
|
||||
'&:hover': {
|
||||
color: '#2C5282'
|
||||
}
|
||||
},
|
||||
// Горизонтальная линия
|
||||
'& hr': {
|
||||
border: 'none',
|
||||
borderTop: '2px solid #E2E8F0',
|
||||
marginTop: '1.5em',
|
||||
marginBottom: '1.5em'
|
||||
},
|
||||
// Таблицы
|
||||
'& table': {
|
||||
borderCollapse: 'collapse',
|
||||
width: '100%',
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
fontSize: '0.95em'
|
||||
},
|
||||
'& table thead': {
|
||||
backgroundColor: '#F7FAFC'
|
||||
},
|
||||
'& table th': {
|
||||
border: '1px solid #E2E8F0',
|
||||
padding: '0.75em 1em',
|
||||
textAlign: 'left',
|
||||
fontWeight: '600',
|
||||
color: '#2D3748'
|
||||
},
|
||||
'& table td': {
|
||||
border: '1px solid #E2E8F0',
|
||||
padding: '0.75em 1em',
|
||||
textAlign: 'left'
|
||||
},
|
||||
'& table tr:nth-of-type(even)': {
|
||||
backgroundColor: '#F7FAFC'
|
||||
},
|
||||
// Выделение (strong, em)
|
||||
'& strong': {
|
||||
fontWeight: '600',
|
||||
color: '#2D3748'
|
||||
},
|
||||
'& em': {
|
||||
fontStyle: 'italic'
|
||||
},
|
||||
// Изображения
|
||||
'& img': {
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
borderRadius: '8px',
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{pages[currentPage]}
|
||||
</ReactMarkdown>
|
||||
</Box>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<HStack justify="center" gap={2}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
onClick={goToPrevious}
|
||||
// @ts-expect-error Chakra UI v2 uses isDisabled
|
||||
isDisabled={currentPage === 0}
|
||||
leftIcon={<Text>←</Text>}
|
||||
>
|
||||
Предыдущая
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
onClick={goToNext}
|
||||
// @ts-expect-error Chakra UI v2 uses isDisabled
|
||||
isDisabled={currentPage === totalPages - 1}
|
||||
rightIcon={<Text>→</Text>}
|
||||
>
|
||||
Следующая
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user