init + api use

This commit is contained in:
Primakov Alexandr Alexandrovich
2025-11-03 17:59:08 +03:00
commit e777b57991
52 changed files with 20725 additions and 0 deletions

View File

@@ -0,0 +1,320 @@
import React, { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import {
Box,
Heading,
Button,
Input,
VStack,
HStack,
Text,
Field,
Badge,
IconButton,
Flex,
} from '@chakra-ui/react'
import {
useGetChainQuery,
useGetTasksQuery,
useCreateChainMutation,
useUpdateChainMutation,
} 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'
import type { ChallengeTask } from '../../types/challenge'
export const ChainFormPage: React.FC = () => {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const isEdit = !!id
const { data: chain, isLoading: isLoadingChain, error: loadError } = useGetChainQuery(id!, {
skip: !id,
})
const { data: allTasks, isLoading: isLoadingTasks } = useGetTasksQuery()
const [createChain, { isLoading: isCreating }] = useCreateChainMutation()
const [updateChain, { isLoading: isUpdating }] = useUpdateChainMutation()
const [name, setName] = useState('')
const [selectedTasks, setSelectedTasks] = useState<ChallengeTask[]>([])
const [searchQuery, setSearchQuery] = useState('')
useEffect(() => {
if (chain) {
setName(chain.name)
setSelectedTasks(chain.tasks)
}
}, [chain])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) {
toaster.create({
title: 'Ошибка валидации',
description: 'Введите название цепочки',
type: 'error',
})
return
}
if (selectedTasks.length === 0) {
toaster.create({
title: 'Ошибка валидации',
description: 'Добавьте хотя бы одно задание',
type: 'error',
})
return
}
try {
const taskIds = selectedTasks.map((task) => task.id)
if (isEdit && id) {
await updateChain({
id,
data: {
name: name.trim(),
tasks: taskIds,
},
}).unwrap()
toaster.create({
title: 'Успешно',
description: 'Цепочка обновлена',
type: 'success',
})
} else {
await createChain({
name: name.trim(),
tasks: taskIds,
}).unwrap()
toaster.create({
title: 'Успешно',
description: 'Цепочка создана',
type: 'success',
})
}
navigate(URLs.chains)
} catch (err: any) {
toaster.create({
title: 'Ошибка',
description: err?.data?.error?.message || 'Не удалось сохранить цепочку',
type: 'error',
})
}
}
const handleAddTask = (task: ChallengeTask) => {
if (!selectedTasks.find((t) => t.id === task.id)) {
setSelectedTasks([...selectedTasks, task])
}
}
const handleRemoveTask = (taskId: string) => {
setSelectedTasks(selectedTasks.filter((t) => t.id !== taskId))
}
const handleMoveUp = (index: number) => {
if (index === 0) return
const newTasks = [...selectedTasks]
;[newTasks[index - 1], newTasks[index]] = [newTasks[index], newTasks[index - 1]]
setSelectedTasks(newTasks)
}
const handleMoveDown = (index: number) => {
if (index === selectedTasks.length - 1) return
const newTasks = [...selectedTasks]
;[newTasks[index], newTasks[index + 1]] = [newTasks[index + 1], newTasks[index]]
setSelectedTasks(newTasks)
}
if (isEdit && isLoadingChain) {
return <LoadingSpinner message="Загрузка цепочки..." />
}
if (isEdit && loadError) {
return <ErrorAlert message="Не удалось загрузить цепочку" />
}
if (isLoadingTasks) {
return <LoadingSpinner message="Загрузка заданий..." />
}
if (!allTasks) {
return <ErrorAlert message="Не удалось загрузить список заданий" />
}
const isLoading = isCreating || isUpdating
const availableTasks = allTasks.filter(
(task) =>
!selectedTasks.find((t) => t.id === task.id) &&
task.title.toLowerCase().includes(searchQuery.toLowerCase())
)
return (
<Box>
<Heading mb={6}>{isEdit ? 'Редактировать цепочку' : 'Создать цепочку'}</Heading>
<Box
as="form"
onSubmit={handleSubmit}
bg="white"
p={6}
borderRadius="lg"
boxShadow="sm"
borderWidth="1px"
borderColor="gray.200"
>
<VStack gap={6} align="stretch">
{/* Name */}
<Field.Root required>
<Field.Label>Название цепочки</Field.Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Введите название цепочки"
maxLength={255}
disabled={isLoading}
/>
</Field.Root>
{/* Selected Tasks */}
<Box>
<Text fontWeight="bold" mb={3}>
Задания в цепочке ({selectedTasks.length})
</Text>
{selectedTasks.length === 0 ? (
<Box
p={6}
borderWidth="2px"
borderStyle="dashed"
borderColor="gray.200"
borderRadius="md"
textAlign="center"
>
<Text color="gray.500">Добавьте задания из списка ниже</Text>
</Box>
) : (
<VStack gap={2} align="stretch">
{selectedTasks.map((task, index) => (
<Flex
key={task.id}
p={3}
bg="teal.50"
borderWidth="1px"
borderColor="teal.200"
borderRadius="md"
align="center"
justify="space-between"
>
<HStack gap={3} flex={1}>
<Badge colorPalette="teal" variant="solid">
#{index + 1}
</Badge>
<Text fontWeight="medium">{task.title}</Text>
</HStack>
<HStack gap={1}>
<IconButton
size="sm"
variant="ghost"
onClick={() => handleMoveUp(index)}
disabled={index === 0 || isLoading}
aria-label="Move up"
>
</IconButton>
<IconButton
size="sm"
variant="ghost"
onClick={() => handleMoveDown(index)}
disabled={index === selectedTasks.length - 1 || isLoading}
aria-label="Move down"
>
</IconButton>
<IconButton
size="sm"
variant="ghost"
colorPalette="red"
onClick={() => handleRemoveTask(task.id)}
disabled={isLoading}
aria-label="Remove"
>
</IconButton>
</HStack>
</Flex>
))}
</VStack>
)}
</Box>
{/* Available Tasks */}
<Box>
<Text fontWeight="bold" mb={3}>
Доступные задания
</Text>
<Input
placeholder="Поиск заданий..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
mb={3}
/>
{availableTasks.length === 0 ? (
<Box
p={6}
borderWidth="1px"
borderColor="gray.200"
borderRadius="md"
textAlign="center"
>
<Text color="gray.500">
{allTasks.length === selectedTasks.length
? 'Все задания уже добавлены'
: 'Ничего не найдено'}
</Text>
</Box>
) : (
<VStack gap={2} align="stretch" maxH="400px" overflowY="auto">
{availableTasks.map((task) => (
<Flex
key={task.id}
p={3}
bg="gray.50"
borderWidth="1px"
borderColor="gray.200"
borderRadius="md"
align="center"
justify="space-between"
cursor="pointer"
_hover={{ bg: 'gray.100' }}
onClick={() => handleAddTask(task)}
>
<Text>{task.title}</Text>
<Button size="sm" colorPalette="teal" variant="ghost">
+ Добавить
</Button>
</Flex>
))}
</VStack>
)}
</Box>
{/* Actions */}
<HStack gap={3} justify="flex-end">
<Button variant="outline" onClick={() => navigate(URLs.chains)} disabled={isLoading}>
Отмена
</Button>
<Button type="submit" colorPalette="teal" loading={isLoading}>
{isEdit ? 'Сохранить изменения' : 'Создать цепочку'}
</Button>
</HStack>
</VStack>
</Box>
</Box>
)
}