Files
challenge-admin-pl/src/pages/tasks/TaskFormPage.tsx
T

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>
)
}