This commit is contained in:
2025-12-14 02:38:35 +07:00
commit 5343a8f2c3
84 changed files with 7406 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { marathonsApi } from '@/api'
import { Button, Input, Card, CardHeader, CardTitle, CardContent } from '@/components/ui'
const createSchema = z.object({
title: z.string().min(1, 'Название обязательно').max(100),
description: z.string().optional(),
start_date: z.string().min(1, 'Дата начала обязательна'),
duration_days: z.number().min(1).max(365).default(30),
})
type CreateForm = z.infer<typeof createSchema>
export function CreateMarathonPage() {
const navigate = useNavigate()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const {
register,
handleSubmit,
formState: { errors },
} = useForm<CreateForm>({
resolver: zodResolver(createSchema),
defaultValues: {
duration_days: 30,
},
})
const onSubmit = async (data: CreateForm) => {
setIsLoading(true)
setError(null)
try {
const marathon = await marathonsApi.create({
...data,
start_date: new Date(data.start_date).toISOString(),
})
navigate(`/marathons/${marathon.id}/lobby`)
} catch (err: unknown) {
const apiError = err as { response?: { data?: { detail?: string } } }
setError(apiError.response?.data?.detail || 'Не удалось создать марафон')
} finally {
setIsLoading(false)
}
}
return (
<div className="max-w-lg mx-auto">
<Card>
<CardHeader>
<CardTitle>Создать марафон</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{error && (
<div className="p-3 bg-red-500/20 border border-red-500 rounded-lg text-red-500 text-sm">
{error}
</div>
)}
<Input
label="Название"
placeholder="Введите название марафона"
error={errors.title?.message}
{...register('title')}
/>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">
Описание (необязательно)
</label>
<textarea
className="input min-h-[100px] resize-none"
placeholder="Введите описание"
{...register('description')}
/>
</div>
<Input
label="Дата начала"
type="datetime-local"
error={errors.start_date?.message}
{...register('start_date')}
/>
<Input
label="Длительность (дней)"
type="number"
error={errors.duration_days?.message}
{...register('duration_days', { valueAsNumber: true })}
/>
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="secondary"
className="flex-1"
onClick={() => navigate('/marathons')}
>
Отмена
</Button>
<Button type="submit" className="flex-1" isLoading={isLoading}>
Создать
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}