- RADAR: dashboard → /terminal/radar - MARKET: auctions + marketplace → /terminal/market - INTEL: pricing → /terminal/intel - WATCHLIST: watchlist + portfolio → /terminal/watchlist - LISTING: listings → /terminal/listing All redirects configured for backwards compatibility. Updated sidebar navigation with new module names.
401 lines
14 KiB
TypeScript
401 lines
14 KiB
TypeScript
'use client'
|
|
|
|
import { ReactNode, useState, useEffect } from 'react'
|
|
import Link from 'next/link'
|
|
import Image from 'next/image'
|
|
import { usePathname, useRouter } from 'next/navigation'
|
|
import { useStore } from '@/lib/store'
|
|
import { KeyboardShortcutsProvider, useAdminShortcuts, ShortcutHint } from '@/hooks/useKeyboardShortcuts'
|
|
import {
|
|
Activity,
|
|
Users,
|
|
Bell,
|
|
Mail,
|
|
Globe,
|
|
Gavel,
|
|
BookOpen,
|
|
Database,
|
|
History,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
LogOut,
|
|
Shield,
|
|
LayoutDashboard,
|
|
Menu,
|
|
X,
|
|
Command,
|
|
Settings,
|
|
} from 'lucide-react'
|
|
import clsx from 'clsx'
|
|
|
|
// ============================================================================
|
|
// ADMIN LAYOUT
|
|
// ============================================================================
|
|
|
|
interface AdminLayoutProps {
|
|
children: ReactNode
|
|
title?: string
|
|
subtitle?: string
|
|
actions?: ReactNode
|
|
activeTab?: string
|
|
onTabChange?: (tab: string) => void
|
|
}
|
|
|
|
export function AdminLayout({
|
|
children,
|
|
title = 'Admin Panel',
|
|
subtitle,
|
|
actions,
|
|
activeTab,
|
|
onTabChange,
|
|
}: AdminLayoutProps) {
|
|
const router = useRouter()
|
|
const { user, isAuthenticated, isLoading, checkAuth, logout } = useStore()
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
|
const [mobileOpen, setMobileOpen] = useState(false)
|
|
|
|
useEffect(() => {
|
|
checkAuth()
|
|
}, [checkAuth])
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && !isAuthenticated) {
|
|
router.push('/login')
|
|
}
|
|
}, [isLoading, isAuthenticated, router])
|
|
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem('admin-sidebar-collapsed')
|
|
if (saved) setSidebarCollapsed(saved === 'true')
|
|
}, [])
|
|
|
|
const toggleCollapsed = () => {
|
|
const newState = !sidebarCollapsed
|
|
setSidebarCollapsed(newState)
|
|
localStorage.setItem('admin-sidebar-collapsed', String(newState))
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!isAuthenticated || !user?.is_admin) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
<div className="text-center">
|
|
<Shield className="w-16 h-16 text-red-400 mx-auto mb-4" />
|
|
<h1 className="text-xl font-semibold text-foreground mb-2">Access Denied</h1>
|
|
<p className="text-foreground-muted mb-4">Admin privileges required</p>
|
|
<button
|
|
onClick={() => router.push('/terminal/radar')}
|
|
className="px-4 py-2 bg-accent text-background rounded-lg font-medium"
|
|
>
|
|
Go to Dashboard
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<KeyboardShortcutsProvider>
|
|
<AdminShortcutsWrapper />
|
|
<div className="min-h-screen bg-background relative overflow-hidden">
|
|
{/* Background Effects */}
|
|
<div className="fixed inset-0 pointer-events-none">
|
|
<div className="absolute top-[-30%] left-[-10%] w-[800px] h-[800px] bg-red-500/[0.02] rounded-full blur-[120px]" />
|
|
<div className="absolute bottom-[-20%] right-[-10%] w-[600px] h-[600px] bg-accent/[0.02] rounded-full blur-[100px]" />
|
|
</div>
|
|
|
|
{/* Admin Sidebar */}
|
|
<AdminSidebar
|
|
collapsed={sidebarCollapsed}
|
|
onCollapse={toggleCollapsed}
|
|
mobileOpen={mobileOpen}
|
|
onMobileClose={() => setMobileOpen(false)}
|
|
user={user}
|
|
onLogout={logout}
|
|
activeTab={activeTab}
|
|
onTabChange={onTabChange}
|
|
/>
|
|
|
|
{/* Mobile Menu Button */}
|
|
<button
|
|
onClick={() => setMobileOpen(true)}
|
|
className="lg:hidden fixed top-4 left-4 z-50 w-11 h-11 bg-background/80 backdrop-blur-xl border border-border
|
|
rounded-xl flex items-center justify-center text-foreground-muted hover:text-foreground
|
|
transition-all shadow-lg hover:shadow-xl hover:border-red-500/30"
|
|
>
|
|
<Menu className="w-5 h-5" />
|
|
</button>
|
|
|
|
{/* Main Content */}
|
|
<div
|
|
className={clsx(
|
|
"relative min-h-screen transition-all duration-300",
|
|
"lg:ml-[280px]",
|
|
sidebarCollapsed && "lg:ml-[80px]",
|
|
"ml-0 pt-16 lg:pt-0"
|
|
)}
|
|
>
|
|
{/* Top Bar */}
|
|
<header className="sticky top-0 z-30 h-16 sm:h-20 bg-gradient-to-r from-background/90 via-background/80 to-background/90 backdrop-blur-xl border-b border-border/30">
|
|
<div className="h-full px-4 sm:px-6 lg:px-8 flex items-center justify-between">
|
|
<div className="ml-10 lg:ml-0">
|
|
<h1 className="text-lg sm:text-xl lg:text-2xl font-semibold tracking-tight text-foreground">{title}</h1>
|
|
{subtitle && <p className="text-xs sm:text-sm text-foreground-muted mt-0.5">{subtitle}</p>}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{actions}
|
|
<button
|
|
onClick={() => {}}
|
|
className="hidden sm:flex items-center gap-2 px-3 py-1.5 text-xs text-foreground-subtle hover:text-foreground
|
|
bg-foreground/5 rounded-lg border border-border/50 hover:border-border transition-all"
|
|
title="Keyboard shortcuts"
|
|
>
|
|
<Command className="w-3 h-3" />
|
|
<span>?</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page Content */}
|
|
<main className="p-4 sm:p-6 lg:p-8">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</KeyboardShortcutsProvider>
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// ADMIN SIDEBAR
|
|
// ============================================================================
|
|
|
|
interface AdminSidebarProps {
|
|
collapsed: boolean
|
|
onCollapse: () => void
|
|
mobileOpen: boolean
|
|
onMobileClose: () => void
|
|
user: any
|
|
onLogout: () => void
|
|
activeTab?: string
|
|
onTabChange?: (tab: string) => void
|
|
}
|
|
|
|
function AdminSidebar({
|
|
collapsed,
|
|
onCollapse,
|
|
mobileOpen,
|
|
onMobileClose,
|
|
user,
|
|
onLogout,
|
|
activeTab,
|
|
onTabChange,
|
|
}: AdminSidebarProps) {
|
|
const pathname = usePathname()
|
|
|
|
const navItems = [
|
|
{ id: 'overview', label: 'Overview', icon: Activity, shortcut: 'O' },
|
|
{ id: 'users', label: 'Users', icon: Users, shortcut: 'U' },
|
|
{ id: 'alerts', label: 'Price Alerts', icon: Bell },
|
|
{ id: 'newsletter', label: 'Newsletter', icon: Mail },
|
|
{ id: 'tld', label: 'TLD Data', icon: Globe },
|
|
{ id: 'auctions', label: 'Auctions', icon: Gavel },
|
|
{ id: 'blog', label: 'Blog', icon: BookOpen, shortcut: 'B' },
|
|
{ id: 'system', label: 'System', icon: Database, shortcut: 'Y' },
|
|
{ id: 'activity', label: 'Activity Log', icon: History },
|
|
]
|
|
|
|
const SidebarContent = () => (
|
|
<>
|
|
{/* Logo */}
|
|
<div className={clsx(
|
|
"h-20 flex items-center border-b border-red-500/20",
|
|
collapsed ? "justify-center px-2" : "px-5"
|
|
)}>
|
|
<Link href="/admin" className="flex items-center gap-3 group">
|
|
<div className={clsx(
|
|
"relative flex items-center justify-center",
|
|
collapsed ? "w-10 h-10" : "w-11 h-11"
|
|
)}>
|
|
<div className="absolute inset-0 bg-red-500/20 blur-xl rounded-full scale-150 opacity-50 group-hover:opacity-80 transition-opacity" />
|
|
<div className="relative w-full h-full bg-gradient-to-br from-red-500 to-red-600 rounded-xl flex items-center justify-center shadow-lg shadow-red-500/20">
|
|
<Shield className="w-5 h-5 text-white" />
|
|
</div>
|
|
</div>
|
|
{!collapsed && (
|
|
<div>
|
|
<span className="text-lg font-bold tracking-wide text-foreground">Admin</span>
|
|
<span className="block text-[10px] text-red-400 uppercase tracking-wider">Control Panel</span>
|
|
</div>
|
|
)}
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 py-6 px-3 space-y-1 overflow-y-auto">
|
|
{!collapsed && (
|
|
<p className="px-3 mb-3 text-[10px] font-semibold text-foreground-subtle/60 uppercase tracking-[0.15em]">
|
|
Management
|
|
</p>
|
|
)}
|
|
|
|
{navItems.map((item) => {
|
|
const isActive = activeTab === item.id
|
|
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => onTabChange?.(item.id)}
|
|
className={clsx(
|
|
"w-full group relative flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
|
|
isActive
|
|
? "bg-gradient-to-r from-red-500/20 to-red-500/5 text-foreground border border-red-500/20"
|
|
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5 border border-transparent"
|
|
)}
|
|
title={collapsed ? item.label : undefined}
|
|
>
|
|
{isActive && (
|
|
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-red-500 rounded-r-full" />
|
|
)}
|
|
|
|
<item.icon className={clsx(
|
|
"w-5 h-5 transition-colors",
|
|
isActive ? "text-red-400" : "group-hover:text-foreground"
|
|
)} />
|
|
|
|
{!collapsed && (
|
|
<>
|
|
<span className="flex-1 text-left text-sm font-medium">{item.label}</span>
|
|
{item.shortcut && <ShortcutHint shortcut={item.shortcut} />}
|
|
</>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
{/* Bottom Section */}
|
|
<div className="border-t border-border/30 py-4 px-3 space-y-2">
|
|
{/* Back to User Dashboard */}
|
|
<Link
|
|
href="/terminal/radar"
|
|
className={clsx(
|
|
"flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
|
|
"text-accent hover:bg-accent/10 border border-transparent hover:border-accent/20"
|
|
)}
|
|
title={collapsed ? "Back to Dashboard" : undefined}
|
|
>
|
|
<LayoutDashboard className="w-5 h-5" />
|
|
{!collapsed && (
|
|
<>
|
|
<span className="flex-1 text-sm font-medium">User Dashboard</span>
|
|
<ShortcutHint shortcut="D" />
|
|
</>
|
|
)}
|
|
</Link>
|
|
|
|
{/* User Info */}
|
|
{!collapsed && (
|
|
<div className="p-4 bg-red-500/5 border border-red-500/20 rounded-xl">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<div className="w-9 h-9 bg-red-500/20 rounded-lg flex items-center justify-center">
|
|
<Shield className="w-4 h-4 text-red-400" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-foreground truncate">{user?.name || user?.email?.split('@')[0]}</p>
|
|
<p className="text-xs text-red-400">Administrator</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Logout */}
|
|
<button
|
|
onClick={onLogout}
|
|
className={clsx(
|
|
"w-full flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
|
|
"text-foreground-muted hover:text-foreground hover:bg-foreground/5"
|
|
)}
|
|
title={collapsed ? "Sign out" : undefined}
|
|
>
|
|
<LogOut className="w-5 h-5" />
|
|
{!collapsed && <span className="text-sm font-medium">Sign out</span>}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Collapse Toggle */}
|
|
<button
|
|
onClick={onCollapse}
|
|
className={clsx(
|
|
"hidden lg:flex absolute -right-3 top-24 w-6 h-6 bg-background border border-border rounded-full",
|
|
"items-center justify-center text-foreground-muted hover:text-foreground",
|
|
"hover:bg-red-500/10 hover:border-red-500/30 transition-all duration-300 shadow-lg"
|
|
)}
|
|
>
|
|
{collapsed ? <ChevronRight className="w-3.5 h-3.5" /> : <ChevronLeft className="w-3.5 h-3.5" />}
|
|
</button>
|
|
</>
|
|
)
|
|
|
|
return (
|
|
<>
|
|
{/* Mobile Overlay */}
|
|
{mobileOpen && (
|
|
<div
|
|
className="lg:hidden fixed inset-0 z-40 bg-background/80 backdrop-blur-sm"
|
|
onClick={onMobileClose}
|
|
/>
|
|
)}
|
|
|
|
{/* Mobile Sidebar */}
|
|
<aside
|
|
className={clsx(
|
|
"lg:hidden fixed left-0 top-0 bottom-0 z-50 w-[280px] flex flex-col",
|
|
"bg-background/95 backdrop-blur-xl border-r border-red-500/20",
|
|
"transition-transform duration-300 ease-out",
|
|
mobileOpen ? "translate-x-0" : "-translate-x-full"
|
|
)}
|
|
>
|
|
<button
|
|
onClick={onMobileClose}
|
|
className="absolute top-5 right-4 w-8 h-8 flex items-center justify-center text-foreground-muted hover:text-foreground"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
<SidebarContent />
|
|
</aside>
|
|
|
|
{/* Desktop Sidebar */}
|
|
<aside
|
|
className={clsx(
|
|
"hidden lg:flex fixed left-0 top-0 bottom-0 z-40 flex-col",
|
|
"bg-gradient-to-b from-background/95 via-background/90 to-background/95 backdrop-blur-xl",
|
|
"border-r border-red-500/20",
|
|
"transition-all duration-300 ease-out",
|
|
collapsed ? "w-[80px]" : "w-[280px]"
|
|
)}
|
|
>
|
|
<SidebarContent />
|
|
</aside>
|
|
</>
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// SHORTCUTS WRAPPER
|
|
// ============================================================================
|
|
|
|
function AdminShortcutsWrapper() {
|
|
useAdminShortcuts()
|
|
return null
|
|
}
|
|
|