251 lines
8.7 KiB
TypeScript
251 lines
8.7 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import { useNavigate, useParams } from 'react-router-dom'
|
|
import { useTranslation } from 'react-i18next'
|
|
import {
|
|
Box,
|
|
Heading,
|
|
Button,
|
|
Input,
|
|
Textarea,
|
|
VStack,
|
|
HStack,
|
|
Text,
|
|
Field,
|
|
Tabs,
|
|
} from '@chakra-ui/react'
|
|
import ReactMarkdown from 'react-markdown'
|
|
import {
|
|
useGetTaskQuery,
|
|
useCreateTaskMutation,
|
|
useUpdateTaskMutation,
|
|
} from '../../__data__/api/api'
|
|
import { URLs } from '../../__data__/urls'
|
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
|
import { ErrorAlert } from '../../components/ErrorAlert'
|
|
import { toaster } from '../../components/ui/toaster'
|
|
|
|
export const TaskFormPage: React.FC = () => {
|
|
const { id } = useParams<{ id: string }>()
|
|
const navigate = useNavigate()
|
|
const isEdit = !!id
|
|
const { t } = useTranslation()
|
|
const { data: task, isLoading: isLoadingTask, error: loadError } = useGetTaskQuery(id!, {
|
|
skip: !id,
|
|
})
|
|
const [createTask, { isLoading: isCreating }] = useCreateTaskMutation()
|
|
const [updateTask, { isLoading: isUpdating }] = useUpdateTaskMutation()
|
|
|
|
const [title, setTitle] = useState('')
|
|
const [description, setDescription] = useState('')
|
|
const [hiddenInstructions, setHiddenInstructions] = useState('')
|
|
const [showDescPreview, setShowDescPreview] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (task) {
|
|
setTitle(task.title)
|
|
setDescription(task.description)
|
|
setHiddenInstructions(task.hiddenInstructions || '')
|
|
}
|
|
}, [task])
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
if (!title.trim() || !description.trim()) {
|
|
toaster.create({
|
|
title: t('challenge.admin.common.validation.error'),
|
|
description: t('challenge.admin.tasks.validation.fill.required.fields'),
|
|
type: 'error',
|
|
})
|
|
return
|
|
}
|
|
|
|
try {
|
|
if (isEdit && id) {
|
|
await updateTask({
|
|
id,
|
|
data: {
|
|
title: title.trim(),
|
|
description: description.trim(),
|
|
hiddenInstructions: hiddenInstructions.trim() || undefined,
|
|
},
|
|
}).unwrap()
|
|
toaster.create({
|
|
title: t('challenge.admin.common.success'),
|
|
description: t('challenge.admin.tasks.updated'),
|
|
type: 'success',
|
|
})
|
|
} else {
|
|
await createTask({
|
|
title: title.trim(),
|
|
description: description.trim(),
|
|
hiddenInstructions: hiddenInstructions.trim() || undefined,
|
|
}).unwrap()
|
|
toaster.create({
|
|
title: t('challenge.admin.common.success'),
|
|
description: t('challenge.admin.tasks.created'),
|
|
type: 'success',
|
|
})
|
|
}
|
|
navigate(URLs.tasks)
|
|
} catch (err: unknown) {
|
|
const errorMessage =
|
|
(err && typeof err === 'object' && 'data' in err &&
|
|
err.data && typeof err.data === 'object' && 'error' in err.data &&
|
|
err.data.error && typeof err.data.error === 'object' && 'message' in err.data.error &&
|
|
typeof err.data.error.message === 'string')
|
|
? err.data.error.message
|
|
: t('challenge.admin.tasks.save.error')
|
|
|
|
toaster.create({
|
|
title: t('challenge.admin.common.error'),
|
|
description: errorMessage,
|
|
type: 'error',
|
|
})
|
|
}
|
|
}
|
|
|
|
if (isEdit && isLoadingTask) {
|
|
return <LoadingSpinner message={t('challenge.admin.tasks.loading')} />
|
|
}
|
|
|
|
if (isEdit && loadError) {
|
|
return <ErrorAlert message={t('challenge.admin.tasks.load.error')} />
|
|
}
|
|
|
|
const isLoading = isCreating || isUpdating
|
|
|
|
return (
|
|
<Box>
|
|
<Heading mb={6}>{isEdit ? t('challenge.admin.tasks.edit.title') : t('challenge.admin.tasks.create.title')}</Heading>
|
|
|
|
<Box
|
|
as="form"
|
|
onSubmit={handleSubmit}
|
|
bg="white"
|
|
p={6}
|
|
borderRadius="lg"
|
|
boxShadow="sm"
|
|
borderWidth="1px"
|
|
borderColor="gray.200"
|
|
>
|
|
<VStack gap={6} align="stretch">
|
|
{/* Title */}
|
|
<Field.Root required>
|
|
<Field.Label>{t('challenge.admin.tasks.field.title')}</Field.Label>
|
|
<Input
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder={t('challenge.admin.tasks.field.title.placeholder')}
|
|
maxLength={255}
|
|
disabled={isLoading}
|
|
/>
|
|
<Field.HelperText>{t('challenge.admin.tasks.field.title.helper')}</Field.HelperText>
|
|
</Field.Root>
|
|
|
|
{/* Description with Markdown */}
|
|
<Field.Root required>
|
|
<Field.Label>{t('challenge.admin.tasks.field.description')}</Field.Label>
|
|
<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={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder={t('challenge.admin.tasks.field.description.placeholder')}
|
|
rows={15}
|
|
fontFamily="monospace"
|
|
disabled={isLoading}
|
|
/>
|
|
</Tabs.Content>
|
|
<Tabs.Content value="preview" pt={4}>
|
|
<Box
|
|
p={4}
|
|
borderWidth="1px"
|
|
borderColor="gray.200"
|
|
borderRadius="md"
|
|
minH="300px"
|
|
bg="gray.50"
|
|
>
|
|
{description ? (
|
|
<Box className="markdown-preview">
|
|
<ReactMarkdown>{description}</ReactMarkdown>
|
|
</Box>
|
|
) : (
|
|
<Text color="gray.400" fontStyle="italic">
|
|
{t('challenge.admin.tasks.preview.empty')}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
</Tabs.Content>
|
|
</Tabs.Root>
|
|
<Field.HelperText>{t('challenge.admin.tasks.field.description.helper')}</Field.HelperText>
|
|
</Field.Root>
|
|
|
|
{/* Hidden Instructions */}
|
|
<Field.Root>
|
|
<Box bg="purple.50" p={4} borderRadius="md" borderWidth="1px" borderColor="purple.200">
|
|
<HStack mb={2}>
|
|
<Text fontWeight="bold" color="purple.800">
|
|
{t('challenge.admin.tasks.field.hidden.instructions')}
|
|
</Text>
|
|
</HStack>
|
|
<Text fontSize="sm" color="purple.700" mb={3}>
|
|
{t('challenge.admin.tasks.field.hidden.instructions.description')}
|
|
</Text>
|
|
<Textarea
|
|
value={hiddenInstructions}
|
|
onChange={(e) => setHiddenInstructions(e.target.value)}
|
|
placeholder={t('challenge.admin.tasks.field.hidden.instructions.placeholder')}
|
|
rows={6}
|
|
disabled={isLoading}
|
|
/>
|
|
<Field.HelperText>
|
|
{t('challenge.admin.tasks.field.hidden.instructions.helper')}
|
|
</Field.HelperText>
|
|
</Box>
|
|
</Field.Root>
|
|
|
|
{/* Meta info for edit mode */}
|
|
{isEdit && task && (
|
|
<Box p={4} bg="gray.50" borderRadius="md">
|
|
<Text fontSize="sm" color="gray.600">
|
|
<strong>{t('challenge.admin.tasks.meta.created')}</strong>{' '}
|
|
{new Date(task.createdAt).toLocaleString('ru-RU')}
|
|
</Text>
|
|
{task.creator && (
|
|
<Text fontSize="sm" color="gray.600">
|
|
<strong>{t('challenge.admin.tasks.meta.author')}</strong> {task.creator.preferred_username}
|
|
</Text>
|
|
)}
|
|
{task.updatedAt !== task.createdAt && (
|
|
<Text fontSize="sm" color="gray.600">
|
|
<strong>{t('challenge.admin.tasks.meta.updated')}</strong>{' '}
|
|
{new Date(task.updatedAt).toLocaleString('ru-RU')}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<HStack gap={3} justify="flex-end">
|
|
<Button variant="outline" onClick={() => navigate(URLs.tasks)} disabled={isLoading}>
|
|
{t('challenge.admin.common.cancel')}
|
|
</Button>
|
|
<Button type="submit" colorPalette="teal" disabled={isLoading}>
|
|
{isEdit ? t('challenge.admin.tasks.button.save') : t('challenge.admin.tasks.button.create')}
|
|
</Button>
|
|
</HStack>
|
|
</VStack>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|