/
foult080
/
hackaton-app
Обзор
Документация
Войти
/
foult080
/
hackaton-app
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
web-app/src/pages/AddHack.tsx
185 строк
8 KB
foult080
feat(): improvments from react-doctor
20 июл 2026, 16:34
20 июл 2026, 16:34
b59277e
Код
Авторство
О чём код?
import { useEffect } from 'react' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { useNavigate, useParams, Link } from 'react-router-dom' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { ArrowLeft, FileText, Save, Trophy } from 'lucide-react' import { hackatonsApi } from '../api/hackatons' import { tasksApi } from '../api/tasks' import Input from '../components/ui/Input' import Button from '../components/ui/Button' import Spinner from '../components/ui/Spinner' import Card from '../components/ui/Card' import { useAlertStore } from '../store/alertStore' const schema = z.object({ name: z.string().min(1, 'Укажите название конкурса'), description: z.string().optional(), period: z.string().min(1, 'Укажите период'), selectedTasks: z.array(z.string()).min(1, 'Выберите хотя бы одну задачу'), }) type FormData = z.infer<typeof schema> export default function AddHack() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const { addAlert } = useAlertStore() const queryClient = useQueryClient() const isEdit = !!id const { data: hackData, isLoading: hackLoading } = useQuery({ queryKey: ['hackaton', id], queryFn: () => hackatonsApi.getById(id!), enabled: isEdit, }) const { data: tasksData, isLoading: tasksLoading } = useQuery({ queryKey: ['tasks'], queryFn: () => tasksApi.getAll(), }) const hack = hackData?.data?.data const allTasks = tasksData?.data?.data || [] const { register, handleSubmit, setValue, watch, formState: { errors } } = useForm<FormData>({ resolver: zodResolver(schema), defaultValues: { name: '', description: '', period: '', selectedTasks: [] }, }) const selectedTasks = watch('selectedTasks') useEffect(() => { if (hack) { setValue('name', hack.name) setValue('description', hack.description || '') setValue('period', hack.period || '') setValue('selectedTasks', hack.tasks.map(t => t.id)) } }, [hack, setValue]) const toggleTask = (taskId: string) => { const current = selectedTasks || [] if (current.includes(taskId)) { setValue('selectedTasks', current.filter(t => t !== taskId)) } else { setValue('selectedTasks', [...current, taskId]) } } const mutation = useMutation({ mutationFn: (data: { name: string; period: string; tasks: string[] }) => isEdit ? hackatonsApi.update(id!, data) : hackatonsApi.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['hackatons'] }) queryClient.invalidateQueries({ queryKey: ['hackaton', 'current'] }) addAlert({ type: 'success', msg: isEdit ? 'Конкурс обновлён' : 'Конкурс создан' }) navigate('/dashboard') }, onError: () => addAlert({ type: 'error', msg: 'Ошибка при сохранении конкурса' }), }) const onSubmit = (data: FormData) => { mutation.mutate({ name: data.name, description: data.description || undefined, period: data.period, tasks: data.selectedTasks }) } if (hackLoading || tasksLoading) return <Spinner /> return ( <div className="min-h-[calc(100vh-4rem)] flex items-center justify-center px-4 py-8"> <div className="absolute inset-0 bg-gradient-to-br from-purple-500/10 via-white to-teal-500/10 pointer-events-none" /> <div className="relative w-full max-w-lg"> <div className="flex items-center gap-3 mb-8"> <div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-purple-400 to-teal-500 flex items-center justify-center shadow-sm"> <Trophy size={24} className="text-white" /> </div> <div> <h2 className="text-2xl font-bold text-gray-900"> {isEdit ? 'Редактировать конкурс' : 'Создать конкурс'} </h2> <p className="text-sm text-gray-500"> {isEdit ? 'Измените данные конкурса' : 'Настройте новый конкурс'} </p> </div> </div> <Card accent="purple"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-5"> <Input label="Название конкурса" placeholder="Укажите название" icon={<Trophy size={16} />} error={errors.name?.message} {...register('name')} /> <Input label="Период проведения" placeholder="например: 2026-07" error={errors.period?.message} {...register('period')} /> <div className="space-y-2"> <label className="block text-sm font-medium text-gray-700"> Описание конкурса <textarea className="mt-1 w-full rounded-xl border bg-white/70 backdrop-blur-sm px-3 py-2.5 text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 border-gray-200 hover:border-gray-300" rows={4} placeholder="Краткое описание хакатона" {...register('description')} /> </label> </div> <div className="space-y-2"> <span className="block text-sm font-medium text-gray-700">Задачи конкурса</span> {allTasks.length === 0 ? ( <p className="text-sm text-gray-500">Нет доступных задач. Сначала создайте задачи во вкладке «Задачи».</p> ) : ( <div className="space-y-2 max-h-60 overflow-y-auto"> {(() => { const selectedSet = new Set(selectedTasks); return allTasks.map((task) => ( <label key={task.id} className={`flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all ${ selectedSet.has(task.id) ? 'bg-purple-50 border-purple-300' : 'bg-white/70 border-gray-200 hover:border-gray-300' }`} > <input type="checkbox" checked={selectedSet.has(task.id)} onChange={() => toggleTask(task.id)} className="w-4 h-4 text-purple-600 rounded focus:ring-purple-500" /> <div> <p className="text-sm font-medium text-gray-900">{task.title}</p> <p className="text-xs text-gray-500 line-clamp-1">{task.description}</p> </div> </label> )); })()} </div> )} {errors.selectedTasks && ( <p className="mt-1.5 text-sm text-red-600">{errors.selectedTasks.message}</p> )} </div> <div className="flex flex-col sm:flex-row gap-3 pt-2"> <Button type="submit" variant="glass-primary" className="flex-1" icon={<Save size={16} />} loading={mutation.isPending}> {isEdit ? 'Сохранить' : 'Создать'} </Button> <Link to="/dashboard" className="flex-1"> <Button type="button" variant="glass" className="w-full" icon={<ArrowLeft size={16} />}> Назад </Button> </Link> </div> </form> </Card> </div> </div> ) }