Finish
This commit is contained in:
@@ -1,434 +0,0 @@
|
|||||||
// ============================================
|
|
||||||
// AUTH PAGES BACKUP - Bento Style
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// LOGIN PAGE (LoginPage.tsx)
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
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'
|
|
||||||
import { marathonsApi } from '@/api'
|
|
||||||
import { NeonButton, Input, GlassCard } from '@/components/ui'
|
|
||||||
import { Gamepad2, LogIn, AlertCircle, Trophy, Users, Zap, Target } from 'lucide-react'
|
|
||||||
|
|
||||||
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()
|
|
||||||
const { login, isLoading, error, clearError, consumePendingInviteCode } = useAuthStore()
|
|
||||||
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)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate('/marathons')
|
|
||||||
} catch {
|
|
||||||
setSubmitError(error || 'Ошибка входа')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: 'Создавайте марафоны' },
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Bento Grid */}
|
|
||||||
<div className="relative w-full max-w-4xl">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{/* 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]" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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_40px_rgba(0,240,255,0.3)]">
|
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
|
|
||||||
{/* Form Block (right) */}
|
|
||||||
<GlassCard className="p-8">
|
|
||||||
{/* 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>
|
|
||||||
</GlassCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Decorative elements */}
|
|
||||||
<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" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// REGISTER PAGE (RegisterPage.tsx)
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
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'
|
|
||||||
import { marathonsApi } from '@/api'
|
|
||||||
import { NeonButton, Input, GlassCard } from '@/components/ui'
|
|
||||||
import { Gamepad2, UserPlus, AlertCircle, Trophy, Users, Zap, Target, Sparkles } from 'lucide-react'
|
|
||||||
|
|
||||||
const registerSchema = z.object({
|
|
||||||
login: z
|
|
||||||
.string()
|
|
||||||
.min(3, 'Логин должен быть не менее 3 символов')
|
|
||||||
.max(50, 'Логин должен быть не более 50 символов')
|
|
||||||
.regex(/^[a-zA-Z0-9_]+$/, 'Логин может содержать только буквы, цифры и подчёркивания'),
|
|
||||||
nickname: z
|
|
||||||
.string()
|
|
||||||
.min(2, 'Никнейм должен быть не менее 2 символов')
|
|
||||||
.max(50, 'Никнейм должен быть не более 50 символов'),
|
|
||||||
password: z.string().min(6, 'Пароль должен быть не менее 6 символов'),
|
|
||||||
confirmPassword: z.string(),
|
|
||||||
}).refine((data) => data.password === data.confirmPassword, {
|
|
||||||
message: 'Пароли не совпадают',
|
|
||||||
path: ['confirmPassword'],
|
|
||||||
})
|
|
||||||
|
|
||||||
type RegisterForm = z.infer<typeof registerSchema>
|
|
||||||
|
|
||||||
export function RegisterPage() {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const { register: registerUser, isLoading, error, clearError, consumePendingInviteCode } = useAuthStore()
|
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<RegisterForm>({
|
|
||||||
resolver: zodResolver(registerSchema),
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit = async (data: RegisterForm) => {
|
|
||||||
setSubmitError(null)
|
|
||||||
clearError()
|
|
||||||
try {
|
|
||||||
await registerUser({
|
|
||||||
login: data.login,
|
|
||||||
password: data.password,
|
|
||||||
nickname: data.nickname,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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, just go to marathons
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate('/marathons')
|
|
||||||
} catch {
|
|
||||||
setSubmitError(error || 'Ошибка регистрации')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: 'Создавайте марафоны' },
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-[80vh] flex items-center justify-center px-4 py-8">
|
|
||||||
{/* Background effects */}
|
|
||||||
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
|
||||||
<div className="absolute top-1/3 -right-32 w-96 h-96 bg-accent-500/10 rounded-full blur-[100px]" />
|
|
||||||
<div className="absolute bottom-1/3 -left-32 w-96 h-96 bg-neon-500/10 rounded-full blur-[100px]" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bento Grid */}
|
|
||||||
<div className="relative w-full max-w-4xl">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{/* Branding Block (left) */}
|
|
||||||
<GlassCard className="p-8 flex flex-col justify-center relative overflow-hidden order-2 md:order-1" 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-accent-500/20 rounded-full blur-[60px]" />
|
|
||||||
<div className="absolute -bottom-20 -right-20 w-48 h-48 bg-neon-500/20 rounded-full blur-[60px]" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
{/* Logo */}
|
|
||||||
<div className="flex justify-center md:justify-start mb-6">
|
|
||||||
<div className="w-20 h-20 rounded-2xl bg-accent-500/10 border border-accent-500/30 flex items-center justify-center shadow-[0_0_40px_rgba(147,51,234,0.3)]">
|
|
||||||
<Gamepad2 className="w-10 h-10 text-accent-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-6 text-center md:text-left">
|
|
||||||
Присоединяйтесь к игровому сообществу
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Benefits */}
|
|
||||||
<div className="p-4 rounded-xl bg-dark-700/50 border border-dark-600 mb-6">
|
|
||||||
<div className="flex items-center gap-2 mb-3">
|
|
||||||
<Sparkles className="w-5 h-5 text-accent-400" />
|
|
||||||
<span className="text-white font-semibold">Что вас ждет:</span>
|
|
||||||
</div>
|
|
||||||
<ul className="space-y-2 text-sm text-gray-400">
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-neon-500" />
|
|
||||||
Создавайте игровые марафоны
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-accent-500" />
|
|
||||||
Выполняйте уникальные челленджи
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-pink-500" />
|
|
||||||
Соревнуйтесь за первое место
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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-accent-500/20 flex items-center justify-center text-accent-400">
|
|
||||||
{feature.icon}
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-300">{feature.text}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
|
|
||||||
{/* Form Block (right) */}
|
|
||||||
<GlassCard className="p-8 order-1 md:order-2">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<div className="flex justify-center mb-4 md:hidden">
|
|
||||||
<div className="w-16 h-16 rounded-2xl bg-accent-500/10 border border-accent-500/30 flex items-center justify-center">
|
|
||||||
<Gamepad2 className="w-8 h-8 text-accent-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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-4">
|
|
||||||
{(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="Никнейм"
|
|
||||||
placeholder="Как вас называть?"
|
|
||||||
error={errors.nickname?.message}
|
|
||||||
{...register('nickname')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Пароль"
|
|
||||||
type="password"
|
|
||||||
placeholder="Придумайте пароль"
|
|
||||||
error={errors.password?.message}
|
|
||||||
autoComplete="new-password"
|
|
||||||
{...register('password')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Подтвердите пароль"
|
|
||||||
type="password"
|
|
||||||
placeholder="Повторите пароль"
|
|
||||||
error={errors.confirmPassword?.message}
|
|
||||||
autoComplete="new-password"
|
|
||||||
{...register('confirmPassword')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<NeonButton
|
|
||||||
type="submit"
|
|
||||||
className="w-full"
|
|
||||||
size="lg"
|
|
||||||
color="purple"
|
|
||||||
isLoading={isLoading}
|
|
||||||
icon={<UserPlus 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="/login"
|
|
||||||
className="text-accent-400 hover:text-accent-300 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
Войти
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Decorative elements */}
|
|
||||||
<div className="absolute -top-4 -left-4 w-24 h-24 border border-accent-500/20 rounded-2xl -z-10 hidden md:block" />
|
|
||||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 border border-neon-500/20 rounded-2xl -z-10 hidden md:block" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user