2025-12-19 02:07:25 +07:00
|
|
|
|
import { Outlet, NavLink, Navigate, Link } from 'react-router-dom'
|
|
|
|
|
|
import { useAuthStore } from '@/store/auth'
|
|
|
|
|
|
import { NeonButton } from '@/components/ui'
|
|
|
|
|
|
import {
|
|
|
|
|
|
LayoutDashboard,
|
|
|
|
|
|
Users,
|
|
|
|
|
|
Trophy,
|
|
|
|
|
|
ScrollText,
|
|
|
|
|
|
Send,
|
|
|
|
|
|
FileText,
|
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
|
Shield,
|
|
|
|
|
|
MessageCircle,
|
|
|
|
|
|
Sparkles,
|
2025-12-29 22:23:34 +03:00
|
|
|
|
Lock,
|
|
|
|
|
|
AlertTriangle
|
2025-12-19 02:07:25 +07:00
|
|
|
|
} from 'lucide-react'
|
|
|
|
|
|
|
|
|
|
|
|
const navItems = [
|
|
|
|
|
|
{ to: '/admin', icon: LayoutDashboard, label: 'Дашборд', end: true },
|
|
|
|
|
|
{ to: '/admin/users', icon: Users, label: 'Пользователи' },
|
|
|
|
|
|
{ to: '/admin/marathons', icon: Trophy, label: 'Марафоны' },
|
2025-12-29 22:23:34 +03:00
|
|
|
|
{ to: '/admin/disputes', icon: AlertTriangle, label: 'Оспаривания' },
|
2025-12-19 02:07:25 +07:00
|
|
|
|
{ to: '/admin/logs', icon: ScrollText, label: 'Логи' },
|
|
|
|
|
|
{ to: '/admin/broadcast', icon: Send, label: 'Рассылка' },
|
|
|
|
|
|
{ to: '/admin/content', icon: FileText, label: 'Контент' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
export function AdminLayout() {
|
|
|
|
|
|
const user = useAuthStore((state) => state.user)
|
|
|
|
|
|
|
|
|
|
|
|
// Only admins can access
|
|
|
|
|
|
if (!user || user.role !== 'admin') {
|
|
|
|
|
|
return <Navigate to="/" replace />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Admin without Telegram - show warning
|
|
|
|
|
|
if (!user.telegram_id) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-[70vh] flex flex-col items-center justify-center text-center px-4">
|
|
|
|
|
|
{/* Background effects */}
|
|
|
|
|
|
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
|
|
|
|
|
<div className="absolute top-1/3 -left-32 w-96 h-96 bg-amber-500/5 rounded-full blur-[100px]" />
|
|
|
|
|
|
<div className="absolute bottom-1/3 -right-32 w-96 h-96 bg-accent-500/5 rounded-full blur-[100px]" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Icon */}
|
|
|
|
|
|
<div className="relative mb-8 animate-float">
|
|
|
|
|
|
<div className="w-32 h-32 rounded-3xl bg-dark-700/50 border border-amber-500/30 flex items-center justify-center shadow-[0_0_30px_rgba(245,158,11,0.15)]">
|
|
|
|
|
|
<Lock className="w-16 h-16 text-amber-400" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="absolute -bottom-2 -right-2 w-12 h-12 rounded-xl bg-accent-500/20 border border-accent-500/30 flex items-center justify-center">
|
|
|
|
|
|
<Shield className="w-6 h-6 text-accent-400" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/* Decorative dots */}
|
|
|
|
|
|
<div className="absolute -top-2 -left-2 w-3 h-3 rounded-full bg-amber-500/50 animate-pulse" />
|
|
|
|
|
|
<div className="absolute top-4 -right-4 w-2 h-2 rounded-full bg-accent-500/50 animate-pulse" style={{ animationDelay: '0.5s' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Title with glow */}
|
|
|
|
|
|
<div className="relative mb-4">
|
|
|
|
|
|
<h1 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-400 via-orange-400 to-accent-400">
|
|
|
|
|
|
Требуется Telegram
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<div className="absolute inset-0 text-3xl font-bold text-amber-500/20 blur-xl">
|
|
|
|
|
|
Требуется Telegram
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p className="text-gray-400 mb-2 max-w-md">
|
|
|
|
|
|
Для доступа к админ-панели необходимо привязать Telegram-аккаунт.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-gray-500 text-sm mb-8 max-w-md">
|
|
|
|
|
|
Это требуется для двухфакторной аутентификации при входе.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Info card */}
|
|
|
|
|
|
<div className="glass rounded-xl p-4 mb-8 max-w-md border border-amber-500/20">
|
|
|
|
|
|
<div className="flex items-center gap-2 text-amber-400 mb-2">
|
|
|
|
|
|
<Shield className="w-4 h-4" />
|
|
|
|
|
|
<span className="text-sm font-semibold">Двухфакторная аутентификация</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-gray-400 text-sm">
|
|
|
|
|
|
После привязки Telegram при входе в админ-панель вам будет отправляться код подтверждения.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Buttons */}
|
|
|
|
|
|
<div className="flex gap-4">
|
|
|
|
|
|
<Link to="/profile">
|
|
|
|
|
|
<NeonButton size="lg" color="purple" icon={<MessageCircle className="w-5 h-5" />}>
|
|
|
|
|
|
Привязать Telegram
|
|
|
|
|
|
</NeonButton>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
<Link to="/marathons">
|
|
|
|
|
|
<NeonButton size="lg" variant="secondary" icon={<ArrowLeft className="w-5 h-5" />}>
|
|
|
|
|
|
На сайт
|
|
|
|
|
|
</NeonButton>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Decorative sparkles */}
|
|
|
|
|
|
<div className="absolute top-1/4 left-1/4 opacity-20">
|
|
|
|
|
|
<Sparkles className="w-6 h-6 text-amber-400 animate-pulse" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="absolute bottom-1/3 right-1/4 opacity-20">
|
|
|
|
|
|
<Sparkles className="w-4 h-4 text-accent-400 animate-pulse" style={{ animationDelay: '1s' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full min-h-[calc(100vh-64px)]">
|
|
|
|
|
|
{/* Background effects */}
|
|
|
|
|
|
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
|
|
|
|
|
<div className="absolute top-0 -left-32 w-96 h-96 bg-accent-500/5 rounded-full blur-[100px]" />
|
|
|
|
|
|
<div className="absolute bottom-0 -right-32 w-96 h-96 bg-neon-500/5 rounded-full blur-[100px]" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Sidebar */}
|
|
|
|
|
|
<aside className="w-64 glass border-r border-dark-600 flex flex-col relative z-10">
|
|
|
|
|
|
<div className="p-4 border-b border-dark-600">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-accent-500 to-pink-500 flex items-center justify-center">
|
|
|
|
|
|
<Shield className="w-4 h-4 text-white" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h2 className="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent-400 to-pink-400">
|
|
|
|
|
|
Админ-панель
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<nav className="flex-1 p-4 space-y-1">
|
|
|
|
|
|
{navItems.map((item) => (
|
|
|
|
|
|
<NavLink
|
|
|
|
|
|
key={item.to}
|
|
|
|
|
|
to={item.to}
|
|
|
|
|
|
end={item.end}
|
|
|
|
|
|
className={({ isActive }) =>
|
|
|
|
|
|
`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200 ${
|
|
|
|
|
|
isActive
|
|
|
|
|
|
? 'bg-accent-500/20 text-accent-400 border border-accent-500/30 shadow-[0_0_10px_rgba(139,92,246,0.15)]'
|
|
|
|
|
|
: 'text-gray-400 hover:bg-dark-600/50 hover:text-white border border-transparent'
|
|
|
|
|
|
}`
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<item.icon className="w-5 h-5" />
|
|
|
|
|
|
<span className="font-medium">{item.label}</span>
|
|
|
|
|
|
</NavLink>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="p-4 border-t border-dark-600">
|
|
|
|
|
|
<NavLink
|
|
|
|
|
|
to="/marathons"
|
|
|
|
|
|
className="flex items-center gap-3 px-3 py-2.5 text-gray-400 hover:text-neon-400 transition-colors rounded-lg hover:bg-dark-600/50"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ArrowLeft className="w-5 h-5" />
|
|
|
|
|
|
<span className="font-medium">Вернуться на сайт</span>
|
|
|
|
|
|
</NavLink>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Main content */}
|
|
|
|
|
|
<main className="flex-1 p-6 overflow-auto relative z-10">
|
|
|
|
|
|
<Outlet />
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|