Files
game-marathon/frontend/src/pages/LoginPage.tsx

265 lines
10 KiB
TypeScript
Raw Normal View History

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 14:53:56 +07:00
import { NeonButton, Input, GlassCard } from '@/components/ui'
2025-12-19 02:07:25 +07:00
import { Gamepad2, LogIn, AlertCircle, Trophy, Users, Zap, Target, Shield, ArrowLeft } 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-19 02:07:25 +07:00
const { login, verify2FA, cancel2FA, pending2FA, isLoading, error, clearError, consumePendingInviteCode } = useAuthStore()
2025-12-14 02:38:35 +07:00
const [submitError, setSubmitError] = useState<string | null>(null)
2025-12-19 02:07:25 +07:00
const [twoFACode, setTwoFACode] = useState('')
2025-12-14 02:38:35 +07:00
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
})
const onSubmit = async (data: LoginForm) => {
setSubmitError(null)
clearError()
try {
2025-12-19 02:07:25 +07:00
const result = await login(data)
// If 2FA required, don't navigate
if (result.requires2FA) {
return
}
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 {
2025-12-20 23:59:13 +07:00
// Error is already set in store by login function
// Ban case is handled separately via banInfo state
2025-12-14 02:38:35 +07:00
}
}
2025-12-19 02:07:25 +07:00
const handle2FASubmit = async (e: React.FormEvent) => {
e.preventDefault()
setSubmitError(null)
clearError()
try {
await verify2FA(twoFACode)
navigate('/marathons')
} catch {
setSubmitError(error || 'Неверный код')
}
}
const handleCancel2FA = () => {
cancel2FA()
setTwoFACode('')
setSubmitError(null)
}
2025-12-17 14:53:56 +07:00
const features = [
{ icon: <Trophy className="w-5 h-5" />, text: 'Соревнуйтесь с друзьями' },
{ icon: <Target className="w-5 h-5" />, text: 'Выполняйте челленджи' },
{ icon: <Zap className="w-5 h-5" />, text: 'Зарабатывайте очки' },
{ icon: <Users className="w-5 h-5" />, text: 'Создавайте марафоны' },
]
2025-12-14 02:38:35 +07:00
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>
2025-12-17 14:53:56 +07:00
{/* Bento Grid */}
<div className="relative w-full max-w-4xl">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 animate-scale-in">
{/* Branding Block (left) */}
<GlassCard className="p-8 flex flex-col justify-center relative overflow-hidden" variant="neon">
{/* Background decoration */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-20 -left-20 w-48 h-48 bg-neon-500/20 rounded-full blur-[60px]" />
<div className="absolute -bottom-20 -right-20 w-48 h-48 bg-accent-500/20 rounded-full blur-[60px]" />
2025-12-17 02:03:33 +07:00
</div>
2025-12-17 14:53:56 +07:00
<div className="relative">
{/* Logo */}
<div className="flex justify-center md:justify-start mb-6">
<div className="w-20 h-20 rounded-2xl bg-neon-500/10 border border-neon-500/30 flex items-center justify-center shadow-[0_0_24px_rgba(34,211,238,0.25)]">
<Gamepad2 className="w-10 h-10 text-neon-500" />
</div>
</div>
{/* Title */}
<h1 className="text-3xl md:text-4xl font-bold text-white mb-2 text-center md:text-left">
Game Marathon
</h1>
<p className="text-gray-400 mb-8 text-center md:text-left">
Платформа для игровых соревнований
</p>
{/* Features */}
<div className="grid grid-cols-2 gap-3">
{features.map((feature, index) => (
<div
key={index}
className="flex items-center gap-2 p-3 rounded-xl bg-dark-700/50 border border-dark-600"
>
<div className="w-8 h-8 rounded-lg bg-neon-500/20 flex items-center justify-center text-neon-400">
{feature.icon}
</div>
<span className="text-sm text-gray-300">{feature.text}</span>
</div>
))}
2025-12-14 02:38:35 +07:00
</div>
2025-12-17 14:53:56 +07:00
</div>
</GlassCard>
{/* Form Block (right) */}
<GlassCard className="p-8">
2025-12-19 02:07:25 +07:00
{pending2FA ? (
// 2FA Form
<>
{/* Header */}
<div className="text-center mb-8">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-neon-500/10 border border-neon-500/30 flex items-center justify-center">
<Shield className="w-8 h-8 text-neon-500" />
</div>
<h2 className="text-2xl font-bold text-white mb-2">Двухфакторная аутентификация</h2>
<p className="text-gray-400">Введите код из Telegram</p>
</div>
2025-12-17 14:53:56 +07:00
2025-12-19 02:07:25 +07:00
{/* 2FA Form */}
<form onSubmit={handle2FASubmit} className="space-y-5">
{(submitError || 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>{submitError || error}</span>
</div>
)}
<Input
label="Код подтверждения"
placeholder="000000"
value={twoFACode}
onChange={(e) => setTwoFACode(e.target.value.replace(/\D/g, '').slice(0, 6))}
maxLength={6}
className="text-center text-2xl tracking-widest font-mono"
autoFocus
/>
<NeonButton
type="submit"
className="w-full"
size="lg"
isLoading={isLoading}
disabled={twoFACode.length !== 6}
icon={<Shield className="w-5 h-5" />}
>
Подтвердить
</NeonButton>
</form>
{/* Back button */}
<div className="mt-6 pt-6 border-t border-dark-600 text-center">
<button
onClick={handleCancel2FA}
className="text-gray-400 hover:text-white transition-colors text-sm flex items-center justify-center gap-2 mx-auto"
>
<ArrowLeft className="w-4 h-4" />
Вернуться к входу
</button>
2025-12-17 14:53:56 +07:00
</div>
2025-12-19 02:07:25 +07:00
</>
) : (
// Regular Login Form
<>
{/* Header */}
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-white mb-2">Добро пожаловать!</h2>
<p className="text-gray-400">Войдите, чтобы продолжить</p>
</div>
{/* Form */}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
{(submitError || 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>{submitError || error}</span>
</div>
)}
<Input
label="Логин"
placeholder="Введите логин"
error={errors.login?.message}
autoComplete="username"
{...register('login')}
/>
<Input
label="Пароль"
type="password"
placeholder="Введите пароль"
error={errors.password?.message}
autoComplete="current-password"
{...register('password')}
/>
<NeonButton
type="submit"
className="w-full"
size="lg"
isLoading={isLoading}
icon={<LogIn className="w-5 h-5" />}
>
Войти
</NeonButton>
</form>
{/* Footer */}
<div className="mt-6 pt-6 border-t border-dark-600 text-center">
<p className="text-gray-400 text-sm">
Нет аккаунта?{' '}
<Link
to="/register"
className="text-neon-400 hover:text-neon-300 transition-colors font-medium"
>
Зарегистрироваться
</Link>
</p>
</div>
</>
)}
2025-12-17 14:53:56 +07:00
</GlassCard>
2025-12-17 02:03:33 +07:00
</div>
{/* Decorative elements */}
2025-12-17 14:53:56 +07:00
<div className="absolute -top-4 -right-4 w-24 h-24 border border-neon-500/20 rounded-2xl -z-10 hidden md:block" />
<div className="absolute -bottom-4 -left-4 w-32 h-32 border border-accent-500/20 rounded-2xl -z-10 hidden md:block" />
2025-12-17 02:03:33 +07:00
</div>
2025-12-14 02:38:35 +07:00
</div>
)
}