diff --git a/src/locales/en.json b/src/locales/en.json index 641de93..a8e63da 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1117,6 +1117,17 @@ "panel": { "title": "Admin Panel", "subtitle": "System management", + "statsUptime": "Uptime", + "statsBot": "Bot", + "statsCabinet": "Cabinet", + "statsTrials": "Trials", + "statsPaid": "Paid", + "statsOnline": "Online", + "statsToday": "today", + "searchPlaceholder": "Search...", + "searchEmpty": "Nothing found", + "searchEmptyHint": "Try a different query", + "searchClear": "Clear search", "dashboardDesc": "Statistics and system monitoring", "ticketsDesc": "Handle user support tickets", "settingsDesc": "System settings and parameters", diff --git a/src/locales/fa.json b/src/locales/fa.json index 029c69d..43a2a44 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -951,6 +951,17 @@ "panel": { "title": "پنل مدیریت", "subtitle": "مدیریت سیستم", + "statsUptime": "آپتایم", + "statsBot": "ربات", + "statsCabinet": "کابینت", + "statsTrials": "آزمایشی", + "statsPaid": "پولی", + "statsOnline": "آنلاین", + "statsToday": "امروز", + "searchPlaceholder": "جستجو...", + "searchEmpty": "چیزی پیدا نشد", + "searchEmptyHint": "عبارت دیگری امتحان کنید", + "searchClear": "پاک کردن جستجو", "dashboardDesc": "آمار و مانیتورینگ سیستم", "ticketsDesc": "مدیریت تیکت‌های کاربران", "settingsDesc": "تنظیمات و پارامترهای سیستم", diff --git a/src/locales/ru.json b/src/locales/ru.json index 2f2edc2..aaa9a5c 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1138,6 +1138,17 @@ "panel": { "title": "Панель администратора", "subtitle": "Управление системой", + "statsUptime": "Аптайм", + "statsBot": "Бот", + "statsCabinet": "Кабинет", + "statsTrials": "Триалы", + "statsPaid": "Платные", + "statsOnline": "Онлайн", + "statsToday": "сегодня", + "searchPlaceholder": "Поиск...", + "searchEmpty": "Ничего не найдено", + "searchEmptyHint": "Попробуйте другой запрос", + "searchClear": "Очистить поиск", "dashboardDesc": "Статистика и мониторинг системы", "ticketsDesc": "Обработка обращений пользователей", "settingsDesc": "Настройки системы и параметры", diff --git a/src/locales/zh.json b/src/locales/zh.json index 001db8d..b436038 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -951,6 +951,17 @@ "panel": { "title": "管理面板", "subtitle": "系统管理", + "statsUptime": "运行时间", + "statsBot": "机器人", + "statsCabinet": "控制台", + "statsTrials": "试用", + "statsPaid": "付费", + "statsOnline": "在线", + "statsToday": "今日", + "searchPlaceholder": "搜索...", + "searchEmpty": "未找到结果", + "searchEmptyHint": "请尝试其他关键词", + "searchClear": "清除搜索", "dashboardDesc": "统计和系统监控", "ticketsDesc": "处理用户工单", "settingsDesc": "系统设置和参数", diff --git a/src/pages/AdminPanel.tsx b/src/pages/AdminPanel.tsx index 83753c4..def35e1 100644 --- a/src/pages/AdminPanel.tsx +++ b/src/pages/AdminPanel.tsx @@ -1,725 +1,1129 @@ +import { useState, useEffect, useRef, useCallback, useMemo, memo } from 'react'; import { Link } from 'react-router'; import { useTranslation } from 'react-i18next'; -import { RemnawaveIcon, ArrowPathIcon } from '../components/icons'; import { usePermissionStore } from '@/store/permissions'; +import { statsApi, type SystemInfo, type DashboardStats } from '@/api/admin'; +import { useAnimatedNumber } from '@/hooks/useAnimatedNumber'; +import { useTelegramSDK } from '@/hooks/useTelegramSDK'; +import { cn } from '@/lib/utils'; -// Group header icons -const AnalyticsGroupIcon = () => ( - - +const CABINET_VERSION = __APP_VERSION__; +const IS_MAC = /Mac|iPhone|iPod|iPad/i.test(navigator.userAgent); + +// ─── Inline SVG Icons (lightweight, no external deps) ─── + +const SvgIcon = ({ + children, + className, + ...props +}: React.SVGProps & { children: React.ReactNode }) => ( + ); -const UsersGroupIcon = () => ( - - +// Stats bar icons (16x16 viewBox) +const StatUptimeIcon = () => ( + +); +const StatBotIcon = () => ( + +); +const StatCabinetIcon = () => ( + +); +const StatTrialIcon = () => ( + +); +const StatPaidIcon = () => ( + ); -const TariffsGroupIcon = () => ( - - - -); +// Section nav icons (24x24 viewBox) +const icons = { + 'bar-chart': ( + + + + + + + + ), + 'credit-card': ( + + + + + ), + activity: ( + + + + ), + trending: ( + + + + + ), + users: ( + + + + + + + ), + ticket: ( + + + + + ), + 'shield-alert': ( + + + + + + ), + tag: ( + + + + + ), + gift: ( + + + + + + ), + percent: ( + + + + + + ), + sparkle: ( + + + + ), + wallet: ( + + + + + + ), + layout: ( + + + + + + ), + newspaper: ( + + + + + + ), + megaphone: ( + + + + + ), + send: ( + + + + + ), + pin: ( + + + + + ), + 'circle-dot': ( + + + + + ), + handshake: ( + + + + + + + ), + 'arrow-up': ( + + + + + + ), + network: ( + + + + + + + + ), + radio: ( + + + + + + + + ), + settings: ( + + + + + ), + app: ( + + + + + ), + server: ( + + + + + + + ), + remnawave: ( + + ), + mail: ( + + + + + ), + refresh: ( + + + + + + + ), + shield: ( + + + + ), + 'user-check': ( + + + + + + ), + lock: ( + + + + + ), + scroll: ( + + + + + + ), + search: ( + + + + + ), + chevron: ( + + + + ), + x: ( + + + + + ), +} as const; -const MarketingGroupIcon = () => ( - - - -); +type IconName = keyof typeof icons; -const SystemGroupIcon = () => ( - - - - -); +// ─── Section Data ─── -// Modern icons with consistent styling -const ChartBarIcon = () => ( - - - -); - -const BanknotesIcon = () => ( - - - -); - -const UsersIcon = () => ( - - - -); - -const ChatBubbleIcon = () => ( - - - -); - -const NoSymbolIcon = () => ( - - - -); - -const CreditCardIcon = () => ( - - - -); - -const TicketIcon = () => ( - - - -); - -const GiftIcon = () => ( - - - -); - -const UserGroupIcon = () => ( - - - -); - -const MegaphoneIcon = () => ( - - - -); - -const PinnedMessageIcon = () => ( - - - - -); - -const PaperAirplaneIcon = () => ( - - - -); - -const SparklesIcon = () => ( - - - -); - -const HandshakeIcon = () => ( - - - -); - -const CogIcon = () => ( - - - - -); - -const DevicePhoneMobileIcon = () => ( - - - -); - -const ServerStackIcon = () => ( - - - -); - -const EnvelopeIcon = () => ( - - - -); - -const ArrowsUpDownIcon = () => ( - - - -); - -const NetworkGraphIcon = () => ( - - - - -); - -const NewspaperIcon = () => ( - - - -); - -const ChevronRightIcon = () => ( - - - -); - -const SecurityGroupIcon = () => ( - - - -); - -const ShieldIcon = () => ( - - - -); - -const UserPlusIcon = () => ( - - - -); - -const DocumentCheckIcon = () => ( - - - -); - -const ClipboardDocumentListIcon = () => ( - - - -); - -const RectangleGroupIcon = () => ( - - - -); - -interface AdminItem { +interface AdminNavItem { + name: string; + icon: IconName; to: string; - icon: React.ReactNode; - title: string; - description: string; permission: string; } -interface AdminGroup { +interface AdminSection { id: string; - title: string; - icon: React.ReactNode; + titleKey: string; + accent: string; gradient: string; - borderColor: string; - iconBg: string; - iconColor: string; - items: AdminItem[]; + items: AdminNavItem[]; } -function AdminCard({ - to, - icon, - title, - description, - iconBg, - iconColor, -}: AdminItem & { iconBg: string; iconColor: string }) { +const sections: AdminSection[] = [ + { + id: 'analytics', + titleKey: 'admin.groups.analytics', + accent: 'rgb(var(--color-success-400))', + gradient: + 'linear-gradient(135deg, rgb(var(--color-success-400)), rgb(var(--color-accent-500)))', + items: [ + { + name: 'admin.nav.dashboard', + icon: 'bar-chart', + to: '/admin/dashboard', + permission: 'stats:read', + }, + { + name: 'admin.nav.payments', + icon: 'credit-card', + to: '/admin/payments', + permission: 'payments:read', + }, + { + name: 'admin.nav.trafficUsage', + icon: 'activity', + to: '/admin/traffic-usage', + permission: 'traffic:read', + }, + { + name: 'admin.nav.salesStats', + icon: 'trending', + to: '/admin/sales-stats', + permission: 'sales_stats:read', + }, + ], + }, + { + id: 'users', + titleKey: 'admin.groups.users', + accent: 'rgb(var(--color-accent-400))', + gradient: 'linear-gradient(135deg, rgb(var(--color-accent-400)), rgb(var(--color-error-400)))', + items: [ + { name: 'admin.nav.users', icon: 'users', to: '/admin/users', permission: 'users:read' }, + { + name: 'admin.nav.tickets', + icon: 'ticket', + to: '/admin/tickets', + permission: 'tickets:read', + }, + { + name: 'admin.nav.banSystem', + icon: 'shield-alert', + to: '/admin/ban-system', + permission: 'ban_system:read', + }, + ], + }, + { + id: 'tariffs', + titleKey: 'admin.groups.tariffs', + accent: 'rgb(var(--color-warning-400))', + gradient: 'linear-gradient(135deg, rgb(var(--color-warning-400)), rgb(var(--color-error-300)))', + items: [ + { name: 'admin.nav.tariffs', icon: 'tag', to: '/admin/tariffs', permission: 'tariffs:read' }, + { + name: 'admin.nav.promocodes', + icon: 'gift', + to: '/admin/promocodes', + permission: 'promocodes:read', + }, + { + name: 'admin.nav.promoGroups', + icon: 'percent', + to: '/admin/promo-groups', + permission: 'promo_groups:read', + }, + { + name: 'admin.nav.promoOffers', + icon: 'sparkle', + to: '/admin/promo-offers', + permission: 'promo_offers:read', + }, + { + name: 'admin.nav.paymentMethods', + icon: 'wallet', + to: '/admin/payment-methods', + permission: 'payment_methods:read', + }, + { + name: 'admin.nav.landings', + icon: 'layout', + to: '/admin/landings', + permission: 'landings:read', + }, + ], + }, + { + id: 'marketing', + titleKey: 'admin.groups.marketing', + accent: 'rgb(var(--color-accent-300))', + gradient: 'linear-gradient(135deg, rgb(var(--color-accent-300)), rgb(var(--color-accent-600)))', + items: [ + { name: 'admin.nav.news', icon: 'newspaper', to: '/admin/news', permission: 'news:read' }, + { + name: 'admin.nav.campaigns', + icon: 'megaphone', + to: '/admin/campaigns', + permission: 'campaigns:read', + }, + { + name: 'admin.nav.broadcasts', + icon: 'send', + to: '/admin/broadcasts', + permission: 'broadcasts:read', + }, + { + name: 'admin.nav.pinnedMessages', + icon: 'pin', + to: '/admin/pinned-messages', + permission: 'pinned_messages:read', + }, + { name: 'admin.nav.wheel', icon: 'circle-dot', to: '/admin/wheel', permission: 'wheel:read' }, + { + name: 'admin.nav.partners', + icon: 'handshake', + to: '/admin/partners', + permission: 'partners:read', + }, + { + name: 'admin.nav.withdrawals', + icon: 'arrow-up', + to: '/admin/withdrawals', + permission: 'withdrawals:read', + }, + { + name: 'admin.nav.referralNetwork', + icon: 'network', + to: '/admin/referral-network', + permission: 'stats:read', + }, + ], + }, + { + id: 'system', + titleKey: 'admin.groups.system', + accent: 'rgb(var(--color-accent-500))', + gradient: + 'linear-gradient(135deg, rgb(var(--color-accent-500)), rgb(var(--color-success-500)))', + items: [ + { + name: 'admin.nav.channelSubscriptions', + icon: 'radio', + to: '/admin/channel-subscriptions', + permission: 'channels:read', + }, + { + name: 'admin.nav.settings', + icon: 'settings', + to: '/admin/settings', + permission: 'settings:read', + }, + { name: 'admin.nav.apps', icon: 'app', to: '/admin/apps', permission: 'apps:read' }, + { + name: 'admin.nav.servers', + icon: 'server', + to: '/admin/servers', + permission: 'servers:read', + }, + { + name: 'admin.nav.remnawave', + icon: 'remnawave', + to: '/admin/remnawave', + permission: 'remnawave:read', + }, + { + name: 'admin.nav.emailTemplates', + icon: 'mail', + to: '/admin/email-templates', + permission: 'email_templates:read', + }, + { + name: 'admin.nav.updates', + icon: 'refresh', + to: '/admin/updates', + permission: 'updates:read', + }, + ], + }, + { + id: 'security', + titleKey: 'admin.groups.security', + accent: 'rgb(var(--color-error-400))', + gradient: 'linear-gradient(135deg, rgb(var(--color-error-400)), rgb(var(--color-accent-600)))', + items: [ + { name: 'admin.nav.roles', icon: 'shield', to: '/admin/roles', permission: 'roles:read' }, + { + name: 'admin.nav.roleAssign', + icon: 'user-check', + to: '/admin/roles/assign', + permission: 'roles:assign', + }, + { name: 'admin.nav.policies', icon: 'lock', to: '/admin/policies', permission: 'roles:read' }, + { + name: 'admin.nav.auditLog', + icon: 'scroll', + to: '/admin/audit-log', + permission: 'audit_log:read', + }, + ], + }, +]; + +// ─── Helpers ─── + +function formatUptime(seconds: number): string { + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (d > 0) return `${d}d ${h}h ${m}m`; + if (h > 0) return `${h}h ${m}m`; + return `${m}m`; +} + +// ─── Animated Stat Number ─── + +function AnimatedStat({ value, suffix }: { value: number; suffix?: string }) { + const animated = useAnimatedNumber(value); return ( - -
- {icon} -
-
-

- {title} -

-

{description}

-
-
- -
- + + {Math.round(animated).toLocaleString()} + {suffix} + ); } -function GroupSection({ group }: { group: AdminGroup }) { +// ─── Stats Bar ─── + +interface StatsBarProps { + systemInfo: SystemInfo | null; + dashboardStats: DashboardStats | null; + loading: boolean; +} + +const StatsBar = memo(function StatsBar({ systemInfo, dashboardStats, loading }: StatsBarProps) { + const { t } = useTranslation(); + + const stats = useMemo(() => { + const uptime = systemInfo?.uptime_seconds ?? 0; + const trial = dashboardStats?.subscriptions.trial ?? 0; + const paid = dashboardStats?.subscriptions.paid ?? 0; + const purchasedToday = dashboardStats?.subscriptions.purchased_today ?? 0; + + return [ + { + icon: , + label: t('admin.panel.statsUptime'), + value: uptime > 0 ? formatUptime(uptime) : '--', + colorClass: 'text-success-400 bg-success-400/10 border-success-400/20', + }, + { + icon: , + label: t('admin.panel.statsBot'), + value: systemInfo?.bot_version ?? '--', + colorClass: 'text-accent-400 bg-accent-400/10 border-accent-400/20', + }, + { + icon: , + label: t('admin.panel.statsCabinet'), + value: `v${CABINET_VERSION}`, + colorClass: 'text-accent-300 bg-accent-300/10 border-accent-300/20', + }, + { + icon: , + label: t('admin.panel.statsTrials'), + numericValue: trial, + colorClass: 'text-warning-400 bg-warning-400/10 border-warning-400/20', + }, + { + icon: , + label: t('admin.panel.statsPaid'), + numericValue: paid, + delta: purchasedToday > 0 ? `+${purchasedToday}` : undefined, + colorClass: 'text-success-400 bg-success-400/10 border-success-400/20', + }, + ]; + }, [systemInfo, dashboardStats, t]); + + return ( +
+ {stats.map((s, i) => ( +
+
+ {s.icon} +
+
+ + {'numericValue' in s && s.numericValue !== undefined ? ( + + ) : ( + {s.value} + )} + {s.delta && ( + + {s.delta} + + )} + + + {s.label} + {s.delta && ` · ${t('admin.panel.statsToday')}`} + +
+
+ ))} +
+ ); +}); + +// ─── Glass Card (Section) ─── + +interface GlassCardProps { + section: AdminSection; + index: number; + searchTerm: string; +} + +const GlassCard = memo(function GlassCard({ section, index, searchTerm }: GlassCardProps) { + const { t } = useTranslation(); const hasPermission = usePermissionStore((state) => state.hasPermission); - const visibleItems = group.items.filter((item) => hasPermission(item.permission)); + const [hoveredItem, setHoveredItem] = useState(null); + const cardRef = useRef(null); + const [tilt, setTilt] = useState({ x: 0, y: 0 }); + const tiltRef = useRef({ x: 0, y: 0 }); + const rafRef = useRef(0); + + useEffect(() => () => cancelAnimationFrame(rafRef.current), []); + + const onMouseMove = useCallback((e: React.MouseEvent) => { + if (!cardRef.current) return; + const rect = cardRef.current.getBoundingClientRect(); + tiltRef.current = { + x: ((e.clientY - rect.top) / rect.height - 0.5) * -2.5, + y: ((e.clientX - rect.left) / rect.width - 0.5) * 2.5, + }; + cancelAnimationFrame(rafRef.current); + rafRef.current = requestAnimationFrame(() => { + setTilt({ ...tiltRef.current }); + }); + }, []); + + const onMouseLeave = useCallback(() => { + cancelAnimationFrame(rafRef.current); + setTilt({ x: 0, y: 0 }); + }, []); + + const visibleItems = useMemo( + () => + section.items.filter((item) => { + if (!hasPermission(item.permission)) return false; + if (!searchTerm) return true; + return t(item.name).toLowerCase().includes(searchTerm.toLowerCase()); + }), + [section.items, hasPermission, searchTerm, t], + ); + + const highlightMatch = useCallback( + (text: string) => { + if (!searchTerm) return text; + const idx = text.toLowerCase().indexOf(searchTerm.toLowerCase()); + if (idx === -1) return text; + return ( + <> + {text.slice(0, idx)} + + {text.slice(idx, idx + searchTerm.length)} + + {text.slice(idx + searchTerm.length)} + + ); + }, + [searchTerm], + ); if (visibleItems.length === 0) return null; return ( -
- {/* Group Header */} -
-
-
{group.icon}
-

{group.title}

+
+ {/* Top glow line */} +
+ + {/* Header */} +
+
+ {/* Shine overlay */} +
+
+

+ {t(section.titleKey)} +

- {/* Group Items */} -
- {visibleItems.map((item) => ( - + {/* Items */} +
+ {visibleItems.map((item, i) => ( + setHoveredItem(i)} + onMouseLeave={() => setHoveredItem(null)} + style={{ + animation: `adminItemEnter 0.35s cubic-bezier(0.22, 1, 0.36, 1) ${index * 60 + i * 20}ms both`, + }} + > + {/* Icon */} +
+ {icons[item.icon]} +
+ + {/* Label */} + + {highlightMatch(t(item.name))} + + + {/* Chevron */} +
+ {icons.chevron} +
+ ))}
); -} +}); + +// ─── Floating Orbs Background ─── + +const CosmosBackground = memo(function CosmosBackground() { + return ( +