Add 3 roles, settings for marathons

This commit is contained in:
2025-12-14 20:21:56 +07:00
parent bb9e9a6e1d
commit d0b8eca600
28 changed files with 1679 additions and 290 deletions

View File

@@ -1,16 +1,20 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useNavigate, Link } 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'
import { Globe, Lock, Users, UserCog, ArrowLeft } from 'lucide-react'
import type { GameProposalMode } from '@/types'
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),
is_public: z.boolean().default(false),
game_proposal_mode: z.enum(['all_participants', 'organizer_only']).default('all_participants'),
})
type CreateForm = z.infer<typeof createSchema>
@@ -23,21 +27,32 @@ export function CreateMarathonPage() {
const {
register,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<CreateForm>({
resolver: zodResolver(createSchema),
defaultValues: {
duration_days: 30,
is_public: false,
game_proposal_mode: 'all_participants',
},
})
const isPublic = watch('is_public')
const gameProposalMode = watch('game_proposal_mode')
const onSubmit = async (data: CreateForm) => {
setIsLoading(true)
setError(null)
try {
const marathon = await marathonsApi.create({
...data,
title: data.title,
description: data.description,
start_date: new Date(data.start_date).toISOString(),
duration_days: data.duration_days,
is_public: data.is_public,
game_proposal_mode: data.game_proposal_mode as GameProposalMode,
})
navigate(`/marathons/${marathon.id}/lobby`)
} catch (err: unknown) {
@@ -50,6 +65,12 @@ export function CreateMarathonPage() {
return (
<div className="max-w-lg mx-auto">
{/* Back button */}
<Link to="/marathons" className="inline-flex items-center gap-2 text-gray-400 hover:text-white mb-4 transition-colors">
<ArrowLeft className="w-4 h-4" />
К списку марафонов
</Link>
<Card>
<CardHeader>
<CardTitle>Создать марафон</CardTitle>
@@ -94,6 +115,92 @@ export function CreateMarathonPage() {
{...register('duration_days', { valueAsNumber: true })}
/>
{/* Тип марафона */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Тип марафона
</label>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => setValue('is_public', false)}
className={`p-3 rounded-lg border-2 transition-all ${
!isPublic
? 'border-primary-500 bg-primary-500/10'
: 'border-gray-700 bg-gray-800 hover:border-gray-600'
}`}
>
<Lock className={`w-5 h-5 mx-auto mb-1 ${!isPublic ? 'text-primary-400' : 'text-gray-400'}`} />
<div className={`text-sm font-medium ${!isPublic ? 'text-white' : 'text-gray-300'}`}>
Закрытый
</div>
<div className="text-xs text-gray-500 mt-1">
Вход по коду
</div>
</button>
<button
type="button"
onClick={() => setValue('is_public', true)}
className={`p-3 rounded-lg border-2 transition-all ${
isPublic
? 'border-primary-500 bg-primary-500/10'
: 'border-gray-700 bg-gray-800 hover:border-gray-600'
}`}
>
<Globe className={`w-5 h-5 mx-auto mb-1 ${isPublic ? 'text-primary-400' : 'text-gray-400'}`} />
<div className={`text-sm font-medium ${isPublic ? 'text-white' : 'text-gray-300'}`}>
Открытый
</div>
<div className="text-xs text-gray-500 mt-1">
Виден всем
</div>
</button>
</div>
</div>
{/* Кто может предлагать игры */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Кто может предлагать игры
</label>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => setValue('game_proposal_mode', 'all_participants')}
className={`p-3 rounded-lg border-2 transition-all ${
gameProposalMode === 'all_participants'
? 'border-primary-500 bg-primary-500/10'
: 'border-gray-700 bg-gray-800 hover:border-gray-600'
}`}
>
<Users className={`w-5 h-5 mx-auto mb-1 ${gameProposalMode === 'all_participants' ? 'text-primary-400' : 'text-gray-400'}`} />
<div className={`text-sm font-medium ${gameProposalMode === 'all_participants' ? 'text-white' : 'text-gray-300'}`}>
Все участники
</div>
<div className="text-xs text-gray-500 mt-1">
С модерацией
</div>
</button>
<button
type="button"
onClick={() => setValue('game_proposal_mode', 'organizer_only')}
className={`p-3 rounded-lg border-2 transition-all ${
gameProposalMode === 'organizer_only'
? 'border-primary-500 bg-primary-500/10'
: 'border-gray-700 bg-gray-800 hover:border-gray-600'
}`}
>
<UserCog className={`w-5 h-5 mx-auto mb-1 ${gameProposalMode === 'organizer_only' ? 'text-primary-400' : 'text-gray-400'}`} />
<div className={`text-sm font-medium ${gameProposalMode === 'organizer_only' ? 'text-white' : 'text-gray-300'}`}>
Только организатор
</div>
<div className="text-xs text-gray-500 mt-1">
Без модерации
</div>
</button>
</div>
</div>
<div className="flex gap-3 pt-4">
<Button
type="button"