Add admin panel
This commit is contained in:
@@ -6,7 +6,7 @@ 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'
|
||||
import { Gamepad2, LogIn, AlertCircle, Trophy, Users, Zap, Target, Shield, ArrowLeft } from 'lucide-react'
|
||||
|
||||
const loginSchema = z.object({
|
||||
login: z.string().min(3, 'Логин должен быть не менее 3 символов'),
|
||||
@@ -17,8 +17,9 @@ type LoginForm = z.infer<typeof loginSchema>
|
||||
|
||||
export function LoginPage() {
|
||||
const navigate = useNavigate()
|
||||
const { login, isLoading, error, clearError, consumePendingInviteCode } = useAuthStore()
|
||||
const { login, verify2FA, cancel2FA, pending2FA, isLoading, error, clearError, consumePendingInviteCode } = useAuthStore()
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const [twoFACode, setTwoFACode] = useState('')
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -32,7 +33,12 @@ export function LoginPage() {
|
||||
setSubmitError(null)
|
||||
clearError()
|
||||
try {
|
||||
await login(data)
|
||||
const result = await login(data)
|
||||
|
||||
// If 2FA required, don't navigate
|
||||
if (result.requires2FA) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check for pending invite code
|
||||
const pendingCode = consumePendingInviteCode()
|
||||
@@ -52,6 +58,24 @@ export function LoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const features = [
|
||||
{ icon: <Trophy className="w-5 h-5" />, text: 'Соревнуйтесь с друзьями' },
|
||||
{ icon: <Target className="w-5 h-5" />, text: 'Выполняйте челленджи' },
|
||||
@@ -113,61 +137,120 @@ export function LoginPage() {
|
||||
|
||||
{/* 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>
|
||||
{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>
|
||||
)}
|
||||
|
||||
<Input
|
||||
label="Логин"
|
||||
placeholder="Введите логин"
|
||||
error={errors.login?.message}
|
||||
autoComplete="username"
|
||||
{...register('login')}
|
||||
/>
|
||||
{/* 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="Пароль"
|
||||
type="password"
|
||||
placeholder="Введите пароль"
|
||||
error={errors.password?.message}
|
||||
autoComplete="current-password"
|
||||
{...register('password')}
|
||||
/>
|
||||
<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}
|
||||
icon={<LogIn className="w-5 h-5" />}
|
||||
>
|
||||
Войти
|
||||
</NeonButton>
|
||||
</form>
|
||||
<NeonButton
|
||||
type="submit"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
disabled={twoFACode.length !== 6}
|
||||
icon={<Shield 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>
|
||||
{/* 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>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// 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>
|
||||
</>
|
||||
)}
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user