Some checks failed
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
271 lines
7.7 KiB
TypeScript
271 lines
7.7 KiB
TypeScript
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>
|
||
)
|
||
} |