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.
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
This commit is contained in:
1 parent
08b654bd4d
commit
0092e55b65
5 files changed
+505
No files matched your search
@@ -0,0 +1,202 @@
|
|||||||
|
# Добавление поля learningMaterial в задачу челленджа
|
||||||
|
|
||||||
|
## Описание изменений
|
||||||
|
|
||||||
|
В модель задачи челленджа (`ChallengeTask`) добавлено новое необязательное текстовое поле `learningMaterial` для хранения дополнительной обучающей информации в формате Markdown.
|
||||||
|
|
||||||
|
## Структура данных
|
||||||
|
|
||||||
|
### Модель ChallengeTask
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
title: string, // Заголовок задания (обязательное)
|
||||||
|
description: string, // Основное описание в Markdown (обязательное, видно студентам)
|
||||||
|
learningMaterial: string, // Дополнительный учебный материал в Markdown (необязательное, видно студентам)
|
||||||
|
hiddenInstructions: string, // Скрытые инструкции для LLM (необязательное, только для преподавателей)
|
||||||
|
createdAt: Date, // Дата создания
|
||||||
|
updatedAt: Date, // Дата последнего обновления
|
||||||
|
creator: Object // Данные создателя из Keycloak
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Изменения в API
|
||||||
|
|
||||||
|
### 1. Создание задания (POST /challenge/task)
|
||||||
|
|
||||||
|
**Добавлено поле в тело запроса:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Название задания",
|
||||||
|
"description": "Основное описание в Markdown",
|
||||||
|
"learningMaterial": "Дополнительный учебный материал в Markdown",
|
||||||
|
"hiddenInstructions": "Скрытые инструкции для преподавателей"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
```bash
|
||||||
|
POST /challenge/task
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Реализация алгоритма сортировки",
|
||||||
|
"description": "Напишите функцию сортировки массива методом пузырька",
|
||||||
|
"learningMaterial": "## Теория\n\nМетод пузырьковой сортировки работает путем...\n\n## Полезные ссылки\n- [Википедия](https://ru.wikipedia.org/wiki/Сортировка_пузырьком)\n- [Видео объяснение](https://example.com/video)",
|
||||||
|
"hiddenInstructions": "Оценить эффективность алгоритма и стиль кода"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Обновление задания (PUT /challenge/task/:taskId)
|
||||||
|
|
||||||
|
**Добавлено поле в тело запроса:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Новое название",
|
||||||
|
"description": "Обновленное описание",
|
||||||
|
"learningMaterial": "Обновленный учебный материал",
|
||||||
|
"hiddenInstructions": "Обновленные инструкции"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Получение данных
|
||||||
|
|
||||||
|
### Получение задания (GET /challenge/task/:taskId)
|
||||||
|
|
||||||
|
**Ответ содержит новое поле:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "task_id",
|
||||||
|
"title": "Название задания",
|
||||||
|
"description": "Основное описание в Markdown",
|
||||||
|
"learningMaterial": "Дополнительный учебный материал в Markdown",
|
||||||
|
"createdAt": "2025-01-15T10:00:00.000Z",
|
||||||
|
"updatedAt": "2025-01-15T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важно:** Поле `learningMaterial` видно всем пользователям (студентам и преподавателям), в отличие от `hiddenInstructions`, которое скрывается от студентов.
|
||||||
|
|
||||||
|
### Получение всех заданий (GET /challenge/tasks)
|
||||||
|
|
||||||
|
Возвращает массив заданий с новым полем `learningMaterial`.
|
||||||
|
|
||||||
|
### Получение цепочек (GET /challenge/chains, GET /challenge/chain/:chainId)
|
||||||
|
|
||||||
|
При получении цепочек с populate заданий, поле `learningMaterial` будет доступно в каждом задании цепочки.
|
||||||
|
|
||||||
|
## Frontend изменения
|
||||||
|
|
||||||
|
### Интерфейсы TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ChallengeTask {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string; // Markdown
|
||||||
|
learningMaterial?: string; // Новое поле - дополнительный материал в Markdown
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Формы создания/редактирования заданий
|
||||||
|
|
||||||
|
В формах создания и редактирования заданий необходимо добавить поле для ввода `learningMaterial`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Пример компонента формы
|
||||||
|
const TaskForm = () => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
learningMaterial: '', // Новое поле
|
||||||
|
hiddenInstructions: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Визуальный редактор или textarea для learningMaterial
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<input name="title" value={formData.title} />
|
||||||
|
<textarea name="description" value={formData.description} />
|
||||||
|
|
||||||
|
{/* Новое поле для дополнительного материала */}
|
||||||
|
<label>Дополнительный учебный материал (Markdown)</label>
|
||||||
|
<textarea
|
||||||
|
name="learningMaterial"
|
||||||
|
value={formData.learningMaterial}
|
||||||
|
placeholder="Дополнительные объяснения, ссылки, примеры..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Только для преподавателей */}
|
||||||
|
<textarea name="hiddenInstructions" value={formData.hiddenInstructions} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Отображение заданий
|
||||||
|
|
||||||
|
При отображении задания студентам показывать `learningMaterial` как дополнительную информацию:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const TaskView = ({ task }: { task: ChallengeTask }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>{task.title}</h1>
|
||||||
|
|
||||||
|
{/* Основное описание */}
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: marked(task.description) }} />
|
||||||
|
|
||||||
|
{/* Дополнительный учебный материал */}
|
||||||
|
{task.learningMaterial && (
|
||||||
|
<div className="learning-material">
|
||||||
|
<h2>Дополнительные материалы</h2>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: marked(task.learningMaterial) }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Миграция данных
|
||||||
|
|
||||||
|
Поле `learningMaterial` добавлено как необязательное с значением по умолчанию `''`, поэтому:
|
||||||
|
- Существующие задания будут работать без изменений
|
||||||
|
- Новое поле будет пустым для старых заданий
|
||||||
|
- Можно постепенно добавлять учебный материал к существующим заданиям
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
### Создание задания с учебным материалом
|
||||||
|
```bash
|
||||||
|
# Создать задание с дополнительным материалом
|
||||||
|
POST /challenge/task
|
||||||
|
{
|
||||||
|
"title": "Тестовое задание",
|
||||||
|
"description": "Основное задание",
|
||||||
|
"learningMaterial": "# Полезная информация\n\nЭто дополнительный материал для студентов"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Получение задания
|
||||||
|
```bash
|
||||||
|
GET /challenge/task/{taskId}
|
||||||
|
# Проверить, что learningMaterial присутствует в ответе
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обновление учебного материала
|
||||||
|
```bash
|
||||||
|
PUT /challenge/task/{taskId}
|
||||||
|
{
|
||||||
|
"learningMaterial": "# Обновленная информация\n\nНовые полезные материалы..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Влияние на существующий код
|
||||||
|
|
||||||
|
- Все существующие эндпоинты получения данных автоматически возвращают новое поле
|
||||||
|
- Создание заданий без указания `learningMaterial` работает как прежде
|
||||||
|
- Фильтрация и валидация не затрагиваются
|
||||||
|
- Поле индексируется MongoDB автоматически
|
||||||
@@ -12,6 +12,7 @@ export interface ChallengeTask {
|
|||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
|
learningMaterial?: string
|
||||||
hiddenInstructions?: string
|
hiddenInstructions?: string
|
||||||
creator?: Record<string, unknown>
|
creator?: Record<string, unknown>
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import remarkGfm from 'remark-gfm'
|
|||||||
import type { ChallengeTask } from '../../__data__/types'
|
import type { ChallengeTask } from '../../__data__/types'
|
||||||
import { useChallenge } from '../../context/ChallengeContext'
|
import { useChallenge } from '../../context/ChallengeContext'
|
||||||
import { useSubmission } from '../../hooks/useSubmission'
|
import { useSubmission } from '../../hooks/useSubmission'
|
||||||
|
import { LearningMaterialViewer } from './LearningMaterialViewer'
|
||||||
|
|
||||||
interface TaskWorkspaceProps {
|
interface TaskWorkspaceProps {
|
||||||
task: ChallengeTask
|
task: ChallengeTask
|
||||||
@@ -27,6 +28,8 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
|
|||||||
|
|
||||||
// Сохраняем последний результат, чтобы блок не исчезал
|
// Сохраняем последний результат, чтобы блок не исчезал
|
||||||
const [lastResult, setLastResult] = useState<typeof finalSubmission>(null)
|
const [lastResult, setLastResult] = useState<typeof finalSubmission>(null)
|
||||||
|
// Состояние для показа дополнительного материала
|
||||||
|
const [showLearningMaterial, setShowLearningMaterial] = useState(false)
|
||||||
|
|
||||||
const isChecking = !!queueStatus || isSubmitting
|
const isChecking = !!queueStatus || isSubmitting
|
||||||
const isAccepted = finalSubmission?.status === 'accepted'
|
const isAccepted = finalSubmission?.status === 'accepted'
|
||||||
@@ -50,6 +53,7 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
|
|||||||
// Сбрасываем состояние при смене задания
|
// Сбрасываем состояние при смене задания
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLastResult(null)
|
setLastResult(null)
|
||||||
|
setShowLearningMaterial(false)
|
||||||
}, [task.id])
|
}, [task.id])
|
||||||
|
|
||||||
// Обновляем сохраненный результат только когда получаем новый
|
// Обновляем сохраненный результат только когда получаем новый
|
||||||
@@ -76,6 +80,7 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
|
|||||||
<Text fontSize="lg" fontWeight="bold" mb={3} color="gray.800">
|
<Text fontSize="lg" fontWeight="bold" mb={3} color="gray.800">
|
||||||
{task.title}
|
{task.title}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
color="gray.700"
|
color="gray.700"
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
@@ -244,8 +249,32 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
|
|||||||
>
|
>
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{task.description}</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{task.description}</ReactMarkdown>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Кнопка для показа дополнительного материала */}
|
||||||
|
{task.learningMaterial && !showLearningMaterial && (
|
||||||
|
<HStack justify="center" mt={4}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={() => setShowLearningMaterial(true)}
|
||||||
|
>
|
||||||
|
📚 Прочитать доп. материал
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Дополнительные материалы - показываются отдельно */}
|
||||||
|
{task.learningMaterial && showLearningMaterial && (
|
||||||
|
<LearningMaterialViewer
|
||||||
|
content={task.learningMaterial}
|
||||||
|
linesPerPage={30}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Статус проверки и результат - фиксированное место */}
|
{/* Статус проверки и результат - фиксированное место */}
|
||||||
<Box minH="80px">
|
<Box minH="80px">
|
||||||
{queueStatus && !finalSubmission ? (
|
{queueStatus && !finalSubmission ? (
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"_id": "task-html-intro",
|
"_id": "task-html-intro",
|
||||||
"title": "HTML старт",
|
"title": "HTML старт",
|
||||||
"description": "# HTML старт\n\nСоздайте базовую HTML-страницу с заголовком и абзацем.",
|
"description": "# HTML старт\n\nСоздайте базовую HTML-страницу с заголовком и абзацем.",
|
||||||
|
"learningMaterial": "# Дополнительные материалы: HTML основы\n\n## Что такое HTML?\n\n**HTML (HyperText Markup Language)** — это язык разметки гипертекста, который является основой всех веб-страниц. HTML позволяет создавать структуру документа и определять его содержимое.\n\n## История HTML\n\nHTML был создан Тимом Бернерс-Ли в 1989 году. Первая версия HTML появилась в 1991 году. С тех пор язык эволюционировал:\n\n- **HTML 2.0** (1995) — первая стандартизированная версия\n- **HTML 3.2** (1997) — добавлены таблицы и апплеты\n- **HTML 4.01** (1999) — последняя версия HTML 4\n- **HTML5** (2014) — современная версия с множеством новых возможностей\n\n## Базовая структура HTML-документа\n\nКаждый HTML-документ имеет стандартную структуру:\n\n```html\n<!DOCTYPE html>\n<html lang=\"ru\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Заголовок страницы</title>\n</head>\n<body>\n <!-- Основное содержимое страницы -->\n</body>\n</html>\n```\n\n### Детальное объяснение структуры:\n\n- `<!DOCTYPE html>` — сообщает браузеру, что это HTML5 документ\n- `<html lang=\"ru\">` — корневой элемент с указанием языка\n- `<head>` — содержит мета-информацию\n- `<meta charset=\"UTF-8\">` — кодировка символов\n- `<meta name=\"viewport\"...>` — адаптивность для мобильных устройств\n- `<title>` — заголовок вкладки браузера\n- `<body>` — видимая часть страницы\n\n## Семантические элементы HTML5\n\nHTML5 ввел множество семантических элементов для лучшей структуры:\n\n```html\n<header> <!-- Шапка сайта -->\n<nav> <!-- Навигация -->\n<main> <!-- Основное содержимое -->\n<section> <!-- Раздел -->\n<article> <!-- Статья -->\n<aside> <!-- Боковая панель -->\n<footer> <!-- Подвал -->\n```\n\n## Работа с текстом\n\n### Заголовки и параграфы\n\n```html\n<h1>Главный заголовок</h1>\n<h2>Подзаголовок</h2>\n<p>Абзац текста с <strong>выделением</strong> и <em>курсивом</em>.</p>\n```\n\n### Списки\n\n```html\n<!-- Нумерованный список -->\n<ol>\n <li>Первый пункт</li>\n <li>Второй пункт</li>\n</ol>\n\n<!-- Маркированный список -->\n<ul>\n <li>Элемент списка</li>\n <li>Еще один элемент</li>\n</ul>\n```\n\n## Мультимедиа\n\n### Изображения\n\n```html\n<img src=\"image.jpg\" alt=\"Описание изображения\" width=\"300\" height=\"200\">\n```\n\n### Ссылки\n\n```html\n<!-- Внешняя ссылка -->\n<a href=\"https://example.com\" target=\"_blank\">Ссылка</a>\n\n<!-- Внутренняя ссылка -->\n<a href=\"#section1\">Перейти к разделу</a>\n```\n\n## Формы и интерактивность\n\n```html\n<form action=\"/submit\" method=\"post\">\n <label for=\"name\">Имя:</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">Отправить</button>\n</form>\n```\n\n## Атрибуты и их типы\n\n### Глобальные атрибуты\n\n- `id` — уникальный идентификатор\n- `class` — CSS классы\n- `title` — всплывающая подсказка\n- `data-*` — пользовательские данные\n\n### Атрибуты специального назначения\n\n- `src` — источник (для изображений, скриптов)\n- `href` — ссылка (для ссылок)\n- `alt` — альтернативный текст (для изображений)\n- `type` — тип (для input элементов)\n\n## Валидация HTML\n\nДля проверки корректности HTML-кода используйте:\n\n- [W3C HTML Validator](https://validator.w3.org/)\n- [HTML5 Validator](https://html5.validator.nu/)\n\n## Лучшие практики\n\n1. **ИспоLine truncated
|
||||||
"createdAt": "2024-09-01T08:05:00.000Z",
|
"createdAt": "2024-09-01T08:05:00.000Z",
|
||||||
"updatedAt": "2024-09-10T12:00:00.000Z"
|
"updatedAt": "2024-09-10T12:00:00.000Z"
|
||||||
},
|
},
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"_id": "task-react-component",
|
"_id": "task-react-component",
|
||||||
"title": "React компонент",
|
"title": "React компонент",
|
||||||
"description": "# React компонент\n\nСоздайте компонент `StatCard` с пропсами `title` и `value`.",
|
"description": "# React компонент\n\nСоздайте компонент `StatCard` с пропсами `title` и `value`.",
|
||||||
|
"learningMaterial": "# Дополнительные материалы: React компоненты\n\n## Основы компонентов\n\nКомпоненты — это строительные блоки React приложений. Они позволяют разбить интерфейс на независимые, переиспользуемые части.\n\n### Функциональные компоненты\n\nФункциональные компоненты — это простые функции JavaScript, которые принимают props и возвращают JSX:\n\n```jsx\nfunction Welcome(props) {\n return <h1>Привет, {props.name}</h1>;\n}\n```\n\n### Классовые компоненты\n\nКлассовые компоненты наследуются от React.Component:\n\n```jsx\nclass Welcome extends React.Component {\n render() {\n return <h1>Привет, {this.props.name}</h1>;\n }\n}\n```\n\n## Props\n\nProps (свойства) — это механизм передачи данных от родительского компонента к дочернему.\n\n```jsx\nfunction Greeting({ name, age }) {\n return (\n <div>\n <h1>Привет, {name}!</h1>\n <p>Тебе {age} лет.</p>\n </div>\n );\n}\n```\n\n## Состояние компонента\n\nСостояние позволяет компонентам изменяться со временем:\n\n```jsx\nimport { useState } from 'react';\n\nfunction Counter() {\n const [count, setCount] = useState(0);\n \n return (\n <div>\n <p>Ты нажал {count} раз</p>\n <button onClick={() => setCount(count + 1)}>\n Нажми меня\n </button>\n </div>\n );\n}\n```\n\n## Жизненный цикл компонентов\n\nКомпоненты имеют жизненный цикл с различными этапами:\n\n- **Mounting** (монтирование)\n- **Updating** (обновление)\n- **Unmounting** (размонтирование)\n\n## Хуки\n\nХуки позволяют использовать состояние и другие возможности React в функциональных компонентах:\n\n- `useState` — управление состоянием\n- `useEffect` — побочные эффекты\n- `useContext` — контекст\n- `useReducer` — сложное состояние\n\n## Лучшие практики\n\n1. **Единая ответственность** — каждый компонент должен делать только одну вещь\n2. **Переиспользуемость** — компоненты должны быть переиспользуемыми\n3. **Читаемость** — код должен быть понятным и читаемым\n4. **Производительность** — оптимизировать рендеринг с помощью React.memo, useMemo, useCallback\n\n## Полезные ссылки\n\n- [Официальная документация React](https://react.dev)\n- [React Hooks](https://react.dev/reference/react)\n- [Создание React приложения](https://create-react-app.dev)\n\n## Примеры компонентов\n\n### Простой компонент\n\n```jsx\nfunction Button({ children, onClick }) {\n return (\n <button \n onClick={onClick}\n style={{ \n padding: '10px 20px', \n backgroundColor: '#007bff', \n color: 'white', \n border: 'none', \n borderRadius: '4px' \n }}\n >\n {children}\n </button>\n );\n}\n```\n\n### Компонент с состоянием\n\n```jsx\nimport { useState } from 'react';\n\nfunction TodoList() {\n const [todos, setTodos] = useState([]);\n const [input, setInput] = useState('');\n \n const addTodo = () => {\n setTodos([...todos, input]);\n setInput('');\n };\n \n return (\n <div>\n <input \n value={input} \n onChange={e => setInput(e.target.value)} \n placeholder=\"Добавить задачу\"\n />\n <button onClick={addTodo}>Добавить</button>\n <ul>\n {todos.map((todo, index) => (\n <li key={index}>{todo}</li>\n ))}\n </ul>\n </div>\n );\n}\n```\n\n### Компонент с эффектами\n\n```jsx\nimport { useState, useEffect } from 'react';\n\nfunction UserProfile({ userId }) {\n const [user, setUser] = useState(null);\n \n useEffect(() => {\n fetch(`/api/users/${userId}`)\n .then(res => res.json())\n .then(setUser);\n }, [userId]);\n \n if (!user) return <div>Загрузка...</div>;\n \n return (\n <div>\n <h1>{user.name}</h1>\n <p>{user.email}</p>\n </div>\n );\n}\n```\n\n## Заключение\n\nReact компоненты — это мощный инструмент для создания пользоваLine truncated
|
||||||
"createdAt": "2024-09-05T11:30:00.000Z",
|
"createdAt": "2024-09-05T11:30:00.000Z",
|
||||||
"updatedAt": "2024-10-01T09:45:00.000Z"
|
"updatedAt": "2024-10-01T09:45:00.000Z"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user