Redesign p1
This commit is contained in:
@@ -4,8 +4,8 @@ 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 { NeonButton, Input, GlassCard } from '@/components/ui'
|
||||
import { Globe, Lock, Users, UserCog, ArrowLeft, Gamepad2, AlertCircle, Sparkles, Calendar, Clock } from 'lucide-react'
|
||||
import type { GameProposalMode } from '@/types'
|
||||
|
||||
const createSchema = z.object({
|
||||
@@ -64,25 +64,38 @@ export function CreateMarathonPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-lg mx-auto">
|
||||
<div className="max-w-xl 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
|
||||
to="/marathons"
|
||||
className="inline-flex items-center gap-2 text-gray-400 hover:text-neon-400 mb-6 transition-colors group"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
К списку марафонов
|
||||
</Link>
|
||||
|
||||
<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>
|
||||
)}
|
||||
<GlassCard variant="neon">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-neon-500/20 to-accent-500/20 flex items-center justify-center border border-neon-500/30">
|
||||
<Gamepad2 className="w-7 h-7 text-neon-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Создать марафон</h1>
|
||||
<p className="text-gray-400 text-sm">Настройте свой игровой марафон</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
{error && (
|
||||
<div className="flex items-center gap-3 p-4 bg-red-500/10 border border-red-500/30 rounded-xl text-red-400 text-sm animate-shake">
|
||||
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Basic info */}
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Название"
|
||||
placeholder="Введите название марафона"
|
||||
@@ -91,132 +104,209 @@ export function CreateMarathonPage() {
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Описание (необязательно)
|
||||
</label>
|
||||
<textarea
|
||||
className="input min-h-[100px] resize-none"
|
||||
placeholder="Введите описание"
|
||||
placeholder="Расскажите о вашем марафоне..."
|
||||
{...register('description')}
|
||||
/>
|
||||
</div>
|
||||
</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 })}
|
||||
/>
|
||||
|
||||
{/* Тип марафона */}
|
||||
{/* Date and duration */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Тип марафона
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-gray-300 mb-2">
|
||||
<Calendar className="w-4 h-4 text-neon-400" />
|
||||
Дата начала
|
||||
</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>
|
||||
<input
|
||||
type="datetime-local"
|
||||
className="input w-full"
|
||||
{...register('start_date')}
|
||||
/>
|
||||
{errors.start_date && (
|
||||
<p className="text-red-400 text-xs mt-1">{errors.start_date.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Кто может предлагать игры */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Кто может предлагать игры
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-gray-300 mb-2">
|
||||
<Clock className="w-4 h-4 text-accent-400" />
|
||||
Длительность (дней)
|
||||
</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>
|
||||
<input
|
||||
type="number"
|
||||
className="input w-full"
|
||||
min={1}
|
||||
max={365}
|
||||
{...register('duration_days', { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.duration_days && (
|
||||
<p className="text-red-400 text-xs mt-1">{errors.duration_days.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button
|
||||
{/* Marathon type */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-3">
|
||||
Тип марафона
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
onClick={() => navigate('/marathons')}
|
||||
onClick={() => setValue('is_public', false)}
|
||||
className={`
|
||||
relative p-4 rounded-xl border-2 transition-all duration-300 text-left group
|
||||
${!isPublic
|
||||
? 'border-neon-500/50 bg-neon-500/10 shadow-[0_0_20px_rgba(0,240,255,0.1)]'
|
||||
: 'border-dark-600 bg-dark-700/50 hover:border-dark-500'
|
||||
}
|
||||
`}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button type="submit" className="flex-1" isLoading={isLoading}>
|
||||
Создать
|
||||
</Button>
|
||||
<div className={`
|
||||
w-10 h-10 rounded-xl mb-3 flex items-center justify-center transition-colors
|
||||
${!isPublic ? 'bg-neon-500/20' : 'bg-dark-600'}
|
||||
`}>
|
||||
<Lock className={`w-5 h-5 ${!isPublic ? 'text-neon-400' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div className={`font-semibold mb-1 ${!isPublic ? 'text-white' : 'text-gray-300'}`}>
|
||||
Закрытый
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Вход только по коду приглашения
|
||||
</div>
|
||||
{!isPublic && (
|
||||
<div className="absolute top-3 right-3">
|
||||
<Sparkles className="w-4 h-4 text-neon-400" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setValue('is_public', true)}
|
||||
className={`
|
||||
relative p-4 rounded-xl border-2 transition-all duration-300 text-left group
|
||||
${isPublic
|
||||
? 'border-accent-500/50 bg-accent-500/10 shadow-[0_0_20px_rgba(168,85,247,0.1)]'
|
||||
: 'border-dark-600 bg-dark-700/50 hover:border-dark-500'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className={`
|
||||
w-10 h-10 rounded-xl mb-3 flex items-center justify-center transition-colors
|
||||
${isPublic ? 'bg-accent-500/20' : 'bg-dark-600'}
|
||||
`}>
|
||||
<Globe className={`w-5 h-5 ${isPublic ? 'text-accent-400' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div className={`font-semibold mb-1 ${isPublic ? 'text-white' : 'text-gray-300'}`}>
|
||||
Открытый
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Виден всем пользователям
|
||||
</div>
|
||||
{isPublic && (
|
||||
<div className="absolute top-3 right-3">
|
||||
<Sparkles className="w-4 h-4 text-accent-400" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Game proposal mode */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-3">
|
||||
Кто может предлагать игры
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setValue('game_proposal_mode', 'all_participants')}
|
||||
className={`
|
||||
relative p-4 rounded-xl border-2 transition-all duration-300 text-left
|
||||
${gameProposalMode === 'all_participants'
|
||||
? 'border-neon-500/50 bg-neon-500/10 shadow-[0_0_20px_rgba(0,240,255,0.1)]'
|
||||
: 'border-dark-600 bg-dark-700/50 hover:border-dark-500'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className={`
|
||||
w-10 h-10 rounded-xl mb-3 flex items-center justify-center transition-colors
|
||||
${gameProposalMode === 'all_participants' ? 'bg-neon-500/20' : 'bg-dark-600'}
|
||||
`}>
|
||||
<Users className={`w-5 h-5 ${gameProposalMode === 'all_participants' ? 'text-neon-400' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div className={`font-semibold mb-1 ${gameProposalMode === 'all_participants' ? 'text-white' : 'text-gray-300'}`}>
|
||||
Все участники
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
С модерацией организатором
|
||||
</div>
|
||||
{gameProposalMode === 'all_participants' && (
|
||||
<div className="absolute top-3 right-3">
|
||||
<Sparkles className="w-4 h-4 text-neon-400" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setValue('game_proposal_mode', 'organizer_only')}
|
||||
className={`
|
||||
relative p-4 rounded-xl border-2 transition-all duration-300 text-left
|
||||
${gameProposalMode === 'organizer_only'
|
||||
? 'border-accent-500/50 bg-accent-500/10 shadow-[0_0_20px_rgba(168,85,247,0.1)]'
|
||||
: 'border-dark-600 bg-dark-700/50 hover:border-dark-500'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className={`
|
||||
w-10 h-10 rounded-xl mb-3 flex items-center justify-center transition-colors
|
||||
${gameProposalMode === 'organizer_only' ? 'bg-accent-500/20' : 'bg-dark-600'}
|
||||
`}>
|
||||
<UserCog className={`w-5 h-5 ${gameProposalMode === 'organizer_only' ? 'text-accent-400' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div className={`font-semibold mb-1 ${gameProposalMode === 'organizer_only' ? 'text-white' : 'text-gray-300'}`}>
|
||||
Только организатор
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Без модерации
|
||||
</div>
|
||||
{gameProposalMode === 'organizer_only' && (
|
||||
<div className="absolute top-3 right-3">
|
||||
<Sparkles className="w-4 h-4 text-accent-400" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 pt-4 border-t border-dark-600">
|
||||
<NeonButton
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => navigate('/marathons')}
|
||||
>
|
||||
Отмена
|
||||
</NeonButton>
|
||||
<NeonButton
|
||||
type="submit"
|
||||
className="flex-1"
|
||||
isLoading={isLoading}
|
||||
icon={<Sparkles className="w-4 h-4" />}
|
||||
>
|
||||
Создать
|
||||
</NeonButton>
|
||||
</div>
|
||||
</form>
|
||||
</GlassCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user