Add admin panel

This commit is contained in:
2025-12-19 02:07:25 +07:00
parent 8e634994bd
commit 481bdabaa8
40 changed files with 3526 additions and 112 deletions

View File

@@ -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>