2025-12-14 02:38:35 +07:00
|
|
|
|
import { useState } from 'react'
|
|
|
|
|
|
import { Link, useNavigate } from 'react-router-dom'
|
|
|
|
|
|
import { useForm } from 'react-hook-form'
|
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
|
|
|
|
import { z } from 'zod'
|
|
|
|
|
|
import { useAuthStore } from '@/store/auth'
|
2025-12-14 20:39:26 +07:00
|
|
|
|
import { marathonsApi } from '@/api'
|
2025-12-17 02:03:33 +07:00
|
|
|
|
import { NeonButton, Input } from '@/components/ui'
|
|
|
|
|
|
import { Gamepad2, LogIn, AlertCircle } from 'lucide-react'
|
2025-12-14 02:38:35 +07:00
|
|
|
|
|
|
|
|
|
|
const loginSchema = z.object({
|
|
|
|
|
|
login: z.string().min(3, 'Логин должен быть не менее 3 символов'),
|
|
|
|
|
|
password: z.string().min(6, 'Пароль должен быть не менее 6 символов'),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
type LoginForm = z.infer<typeof loginSchema>
|
|
|
|
|
|
|
|
|
|
|
|
export function LoginPage() {
|
|
|
|
|
|
const navigate = useNavigate()
|
2025-12-14 20:39:26 +07:00
|
|
|
|
const { login, isLoading, error, clearError, consumePendingInviteCode } = useAuthStore()
|
2025-12-14 02:38:35 +07:00
|
|
|
|
const [submitError, setSubmitError] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
register,
|
|
|
|
|
|
handleSubmit,
|
|
|
|
|
|
formState: { errors },
|
|
|
|
|
|
} = useForm<LoginForm>({
|
|
|
|
|
|
resolver: zodResolver(loginSchema),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const onSubmit = async (data: LoginForm) => {
|
|
|
|
|
|
setSubmitError(null)
|
|
|
|
|
|
clearError()
|
|
|
|
|
|
try {
|
|
|
|
|
|
await login(data)
|
2025-12-14 20:39:26 +07:00
|
|
|
|
|
|
|
|
|
|
// Check for pending invite code
|
|
|
|
|
|
const pendingCode = consumePendingInviteCode()
|
|
|
|
|
|
if (pendingCode) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const marathon = await marathonsApi.join(pendingCode)
|
|
|
|
|
|
navigate(`/marathons/${marathon.id}`)
|
|
|
|
|
|
return
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// If join fails (already member, etc), just go to marathons
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-14 02:38:35 +07:00
|
|
|
|
navigate('/marathons')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setSubmitError(error || 'Ошибка входа')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-12-17 02:03:33 +07:00
|
|
|
|
<div className="min-h-[80vh] flex items-center justify-center px-4 -mt-8">
|
|
|
|
|
|
{/* Background effects */}
|
|
|
|
|
|
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
|
|
|
|
|
<div className="absolute top-1/4 -left-32 w-96 h-96 bg-neon-500/10 rounded-full blur-[100px]" />
|
|
|
|
|
|
<div className="absolute bottom-1/4 -right-32 w-96 h-96 bg-accent-500/10 rounded-full blur-[100px]" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="relative w-full max-w-md">
|
|
|
|
|
|
{/* Card */}
|
|
|
|
|
|
<div className="glass-neon rounded-2xl p-8 animate-scale-in">
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<div className="text-center mb-8">
|
|
|
|
|
|
<div className="flex justify-center mb-4">
|
|
|
|
|
|
<div className="w-16 h-16 rounded-2xl bg-neon-500/10 border border-neon-500/30 flex items-center justify-center">
|
|
|
|
|
|
<Gamepad2 className="w-8 h-8 text-neon-500" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h1 className="text-2xl font-bold text-white mb-2">Добро пожаловать!</h1>
|
|
|
|
|
|
<p className="text-gray-400">Войдите, чтобы продолжить</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Form */}
|
|
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
2025-12-14 02:38:35 +07:00
|
|
|
|
{(submitError || error) && (
|
2025-12-17 02:03:33 +07:00
|
|
|
|
<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>{submitError || error}</span>
|
2025-12-14 02:38:35 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<Input
|
|
|
|
|
|
label="Логин"
|
|
|
|
|
|
placeholder="Введите логин"
|
|
|
|
|
|
error={errors.login?.message}
|
2025-12-17 02:03:33 +07:00
|
|
|
|
autoComplete="username"
|
2025-12-14 02:38:35 +07:00
|
|
|
|
{...register('login')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Input
|
|
|
|
|
|
label="Пароль"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
placeholder="Введите пароль"
|
|
|
|
|
|
error={errors.password?.message}
|
2025-12-17 02:03:33 +07:00
|
|
|
|
autoComplete="current-password"
|
2025-12-14 02:38:35 +07:00
|
|
|
|
{...register('password')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-12-17 02:03:33 +07:00
|
|
|
|
<NeonButton
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
className="w-full"
|
|
|
|
|
|
size="lg"
|
|
|
|
|
|
isLoading={isLoading}
|
|
|
|
|
|
icon={<LogIn className="w-5 h-5" />}
|
|
|
|
|
|
>
|
2025-12-14 02:38:35 +07:00
|
|
|
|
Войти
|
2025-12-17 02:03:33 +07:00
|
|
|
|
</NeonButton>
|
|
|
|
|
|
</form>
|
2025-12-14 02:38:35 +07:00
|
|
|
|
|
2025-12-17 02:03:33 +07:00
|
|
|
|
{/* Footer */}
|
|
|
|
|
|
<div className="mt-6 pt-6 border-t border-dark-600 text-center">
|
|
|
|
|
|
<p className="text-gray-400 text-sm">
|
2025-12-14 02:38:35 +07:00
|
|
|
|
Нет аккаунта?{' '}
|
2025-12-17 02:03:33 +07:00
|
|
|
|
<Link
|
|
|
|
|
|
to="/register"
|
|
|
|
|
|
className="text-neon-400 hover:text-neon-300 transition-colors font-medium"
|
|
|
|
|
|
>
|
2025-12-14 02:38:35 +07:00
|
|
|
|
Зарегистрироваться
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</p>
|
2025-12-17 02:03:33 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Decorative elements */}
|
|
|
|
|
|
<div className="absolute -top-4 -right-4 w-24 h-24 border border-neon-500/20 rounded-2xl -z-10" />
|
|
|
|
|
|
<div className="absolute -bottom-4 -left-4 w-32 h-32 border border-accent-500/20 rounded-2xl -z-10" />
|
|
|
|
|
|
</div>
|
2025-12-14 02:38:35 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|