Add optional learningMaterial field to ChallengeTask model for additional educational content; update API endpoints, TypeScript interfaces, and frontend forms to support this feature. Enhance localization for English and Russian to include new field descriptions and placeholders.

This commit is contained in:
2025-12-14 15:02:43 +03:00
parent e93de750fc
commit 4aae3c154e
6 changed files with 339 additions and 2 deletions

View File

@@ -3,6 +3,6 @@ import Keycloak from 'keycloak-js'
export const keycloak = new Keycloak({
url: KC_URL,
realm: KC_REALM,
clientId: KC_CLIENT_ID,
});
clientId: KC_CLIENT_ID
})

View File

@@ -40,6 +40,7 @@ export const TaskFormPage: React.FC = () => {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [learningMaterial, setLearningMaterial] = useState('')
const [hiddenInstructions, setHiddenInstructions] = useState('')
const [showDescPreview, setShowDescPreview] = useState(false)
const [testAnswer, setTestAnswer] = useState('')
@@ -50,6 +51,7 @@ export const TaskFormPage: React.FC = () => {
if (task) {
setTitle(task.title)
setDescription(task.description)
setLearningMaterial(task.learningMaterial || '')
setHiddenInstructions(task.hiddenInstructions || '')
}
}, [task])
@@ -106,6 +108,7 @@ export const TaskFormPage: React.FC = () => {
data: {
title: title.trim(),
description: description.trim(),
learningMaterial: learningMaterial.trim() || undefined,
hiddenInstructions: hiddenInstructions.trim() || undefined,
},
}).unwrap()
@@ -118,6 +121,7 @@ export const TaskFormPage: React.FC = () => {
await createTask({
title: title.trim(),
description: description.trim(),
learningMaterial: learningMaterial.trim() || undefined,
hiddenInstructions: hiddenInstructions.trim() || undefined,
}).unwrap()
toaster.create({
@@ -355,6 +359,128 @@ export const TaskFormPage: React.FC = () => {
<Field.HelperText>{t('challenge.admin.tasks.field.description.helper')}</Field.HelperText>
</Field.Root>
{/* Learning Material */}
<Field.Root>
<Field.Label>{t('challenge.admin.tasks.field.learning.material')}</Field.Label>
<Box display={{ base: 'block', lg: 'none' }}>
{/* Табы для мобильных */}
<Tabs.Root
value={showDescPreview ? 'preview' : 'editor'}
onValueChange={(e) => setShowDescPreview(e.value === 'preview')}
>
<Tabs.List>
<Tabs.Trigger value="editor">{t('challenge.admin.tasks.tab.editor')}</Tabs.Trigger>
<Tabs.Trigger value="preview">{t('challenge.admin.tasks.tab.preview')}</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="editor" pt={4}>
<Textarea
value={learningMaterial}
onChange={(e) => setLearningMaterial(e.target.value)}
placeholder={t('challenge.admin.tasks.field.learning.material.placeholder')}
rows={12}
fontFamily="monospace"
disabled={isLoading}
/>
</Tabs.Content>
<Tabs.Content value="preview" pt={4}>
<Box
p={4}
borderWidth="1px"
borderColor="gray.200"
borderRadius="md"
minH="250px"
maxH="400px"
bg="gray.50"
overflowY="auto"
>
{learningMaterial ? (
<Box
className="markdown-preview"
css={{
'& a': {
color: '#0f766e',
textDecoration: 'underline',
cursor: 'pointer',
'&:hover': {
color: '#115e59',
}
}
}}
>
<ReactMarkdown>{learningMaterial}</ReactMarkdown>
</Box>
) : (
<Text color="gray.400" fontStyle="italic">
{t('challenge.admin.tasks.preview.empty')}
</Text>
)}
</Box>
</Tabs.Content>
</Tabs.Root>
</Box>
{/* Две колонки для десктопа */}
<Grid
display={{ base: 'none', lg: 'grid' }}
templateColumns="1fr 1fr"
gap={4}
mt={2}
>
<Box>
<Text fontSize="sm" fontWeight="medium" mb={2} color="gray.700">
{t('challenge.admin.tasks.tab.editor')}
</Text>
<Textarea
value={learningMaterial}
onChange={(e) => setLearningMaterial(e.target.value)}
placeholder={t('challenge.admin.tasks.field.learning.material.placeholder')}
rows={15}
fontFamily="monospace"
disabled={isLoading}
/>
</Box>
<Box>
<Text fontSize="sm" fontWeight="medium" mb={2} color="gray.700">
{t('challenge.admin.tasks.tab.preview')}
</Text>
<Box
p={4}
borderWidth="1px"
borderColor="gray.200"
borderRadius="md"
minH="250px"
maxH="400px"
bg="gray.50"
overflowY="auto"
height="100%"
>
{learningMaterial ? (
<Box
className="markdown-preview"
css={{
'& a': {
color: '#0f766e',
textDecoration: 'underline',
cursor: 'pointer',
'&:hover': {
color: '#115e59',
}
}
}}
>
<ReactMarkdown>{learningMaterial}</ReactMarkdown>
</Box>
) : (
<Text color="gray.400" fontStyle="italic">
{t('challenge.admin.tasks.preview.empty')}
</Text>
)}
</Box>
</Box>
</Grid>
<Field.HelperText>{t('challenge.admin.tasks.field.learning.material.helper')}</Field.HelperText>
</Field.Root>
{/* Hidden Instructions */}
<Field.Root>
<Box bg="purple.50" p={4} borderRadius="md" borderWidth="1px" borderColor="purple.200">

View File

@@ -12,6 +12,7 @@ export interface ChallengeTask {
id: string
title: string
description: string // Markdown
learningMaterial?: string // Дополнительный учебный материал в Markdown
hiddenInstructions?: string // Только для преподавателей
creator?: {
sub: string
@@ -121,12 +122,14 @@ export interface APIResponse<T> {
export interface CreateTaskRequest {
title: string
description: string
learningMaterial?: string
hiddenInstructions?: string
}
export interface UpdateTaskRequest {
title?: string
description?: string
learningMaterial?: string
hiddenInstructions?: string
}