Files
infohliadka/components/AdminLayout.tsx
Lukas Davidovic 249a672cd7 transform admin panel with comprehensive professional UI
- migrate from SQLite to PostgreSQL with Drizzle ORM
- implement comprehensive AdminLayout with expandable sidebar navigation
- create professional dashboard with real-time charts and metrics
- add advanced monitoring, reporting, and export functionality
- fix menu alignment and remove non-existent pages
- eliminate duplicate headers and improve UI consistency
- add Tailwind CSS v3 for professional styling
- expand database schema from 6 to 15 tables
- implement role-based access control and API key management
- create comprehensive settings, monitoring, and system info pages
2025-09-06 15:14:20 +02:00

305 lines
11 KiB
TypeScript

import { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import {
ChevronDownIcon,
HomeIcon,
ShieldExclamationIcon,
DocumentTextIcon,
UsersIcon,
Cog6ToothIcon,
ChartBarIcon,
KeyIcon,
BellIcon,
EyeIcon,
ExclamationTriangleIcon,
CloudArrowUpIcon,
ServerStackIcon,
CpuChipIcon,
ClockIcon,
CircleStackIcon,
FolderIcon,
TagIcon,
DocumentDuplicateIcon,
ArrowTrendingUpIcon,
ShieldCheckIcon,
GlobeAltIcon,
MagnifyingGlassIcon,
AdjustmentsHorizontalIcon,
BugAntIcon
} from '@heroicons/react/24/outline'
interface AdminLayoutProps {
children: React.ReactNode
title?: string
}
const navigation = [
{
name: 'Dashboard',
href: '/admin',
icon: HomeIcon,
current: false,
},
{
name: 'Správa zdrojov',
icon: ShieldExclamationIcon,
current: false,
children: [
{ name: 'Všetky zdroje', href: '/admin/sources' },
{ name: 'Bulk import', href: '/admin/bulk-import' },
],
},
{
name: 'Hlásenia',
icon: DocumentTextIcon,
current: false,
children: [
{ name: 'Všetky hlásenia', href: '/admin/reports' },
],
},
{
name: 'Kategórie',
href: '/admin/categories',
icon: TagIcon,
current: false,
},
{
name: 'Používatelia',
icon: UsersIcon,
current: false,
children: [
{ name: 'Správa používateľov', href: '/admin/users' },
],
},
{
name: 'API & Integrácie',
icon: KeyIcon,
current: false,
children: [
{ name: 'API kľúče', href: '/admin/api-keys' },
{ name: 'Webhooks', href: '/admin/webhooks' },
],
},
{
name: 'Monitoring',
icon: ChartBarIcon,
current: false,
children: [
{ name: 'Prehľad výkonnosti', href: '/admin/monitoring' },
{ name: 'Real-time monitor', href: '/admin/monitoring/realtime' },
],
},
{
name: 'Exporty',
icon: DocumentDuplicateIcon,
current: false,
children: [
{ name: 'Export dát', href: '/admin/export' },
],
},
{
name: 'Nastavenia',
icon: Cog6ToothIcon,
current: false,
children: [
{ name: 'Všeobecné', href: '/admin/settings/general' },
],
},
{
name: 'Systém',
icon: ServerStackIcon,
current: false,
children: [
{ name: 'Stav systému', href: '/admin/system/status' },
],
},
]
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ')
}
export default function AdminLayout({ children, title = 'Administrácia' }: AdminLayoutProps) {
const router = useRouter()
const [sidebarOpen, setSidebarOpen] = useState(false)
const [expandedMenus, setExpandedMenus] = useState<string[]>([])
const toggleMenu = (menuName: string) => {
setExpandedMenus(prev =>
prev.includes(menuName)
? prev.filter(name => name !== menuName)
: [...prev, menuName]
)
}
const isCurrentPath = (href: string) => {
if (href === '/admin') {
return router.pathname === '/admin'
}
return router.pathname.startsWith(href)
}
return (
<div className="min-h-screen bg-gray-100">
{/* Static sidebar for desktop */}
<div className="hidden lg:flex lg:w-72 lg:flex-col lg:fixed lg:inset-y-0">
<div className="flex flex-col flex-grow bg-primary-700 pt-5 pb-4 overflow-y-auto">
<div className="flex items-center flex-shrink-0 px-4">
<ShieldCheckIcon className="h-8 w-8 text-white" />
<h1 className="ml-3 text-xl font-bold text-white">Hliadka.sk Admin</h1>
</div>
<nav className="mt-8 flex-1 flex flex-col divide-y divide-primary-800" aria-label="Sidebar">
<div className="px-2 space-y-1">
{navigation.map((item) =>
!item.children ? (
<Link
key={item.name}
href={item.href || '#'}
className={classNames(
isCurrentPath(item.href || '')
? 'bg-primary-800 text-white'
: 'text-primary-100 hover:text-white hover:bg-primary-600',
'group flex items-center px-2 py-2 text-sm leading-6 font-medium rounded-md transition-colors duration-200'
)}
>
<item.icon className="mr-3 flex-shrink-0 h-6 w-6" aria-hidden="true" />
{item.name}
</Link>
) : (
<div key={item.name} className="space-y-1">
<button
onClick={() => toggleMenu(item.name)}
className={classNames(
item.children.some(child => isCurrentPath(child.href))
? 'bg-primary-800 text-white'
: 'text-primary-100 hover:text-white hover:bg-primary-600',
'group w-full flex items-center px-2 py-2 text-sm leading-6 font-medium rounded-md transition-colors duration-200'
)}
>
<item.icon className="mr-3 flex-shrink-0 h-6 w-6" aria-hidden="true" />
<span className="flex-1 text-left">{item.name}</span>
<ChevronDownIcon
className={classNames(
expandedMenus.includes(item.name) ? 'rotate-180' : '',
'ml-3 flex-shrink-0 h-5 w-5 transform transition-transform duration-200'
)}
aria-hidden="true"
/>
</button>
{expandedMenus.includes(item.name) && (
<div className="pl-4 space-y-1">
{item.children.map((child) => (
<Link
key={child.name}
href={child.href}
className={classNames(
isCurrentPath(child.href)
? 'bg-primary-800 text-white'
: 'text-primary-100 hover:text-white hover:bg-primary-600',
'group flex items-center pl-8 pr-2 py-2 text-sm font-medium rounded-md transition-colors duration-200'
)}
>
{child.name}
</Link>
))}
</div>
)}
</div>
)
)}
</div>
</nav>
</div>
</div>
{/* Main content */}
<div className="lg:pl-72 flex flex-col flex-1">
<div className="relative z-10 flex-shrink-0 flex h-16 bg-white border-b border-gray-200 lg:border-none">
<button
type="button"
className="border-r border-gray-200 px-4 text-gray-400 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500 lg:hidden"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</button>
{/* Search bar */}
<div className="flex-1 px-4 flex justify-between sm:px-6 lg:max-w-6xl lg:mx-auto lg:px-8">
<div className="flex-1 flex">
<div className="w-full flex md:ml-0">
<label htmlFor="search-field" className="sr-only">
Hľadať
</label>
<div className="relative w-full text-gray-400 focus-within:text-gray-600">
<div className="absolute inset-y-0 left-0 flex items-center pointer-events-none">
<MagnifyingGlassIcon className="h-5 w-5" aria-hidden="true" />
</div>
<input
id="search-field"
className="block w-full h-full pl-8 pr-3 py-2 border-transparent text-gray-900 placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-0 focus:border-transparent sm:text-sm"
placeholder="Hľadať zdroje, domény, hlásenia..."
type="search"
name="search"
/>
</div>
</div>
</div>
<div className="ml-4 flex items-center md:ml-6">
<button
type="button"
className="bg-white p-1 rounded-full text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
<span className="sr-only">View notifications</span>
<BellIcon className="h-6 w-6" aria-hidden="true" />
</button>
{/* Profile dropdown */}
<div className="ml-3 relative">
<div>
<button className="max-w-xs bg-white flex items-center text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
<span className="sr-only">Open user menu</span>
<div className="h-8 w-8 rounded-full bg-primary-600 flex items-center justify-center">
<span className="text-sm font-medium text-white">A</span>
</div>
</button>
</div>
</div>
</div>
</div>
</div>
<main className="flex-1 pb-8">
{/* Page header */}
<div className="bg-white shadow">
<div className="px-4 sm:px-6 lg:max-w-6xl lg:mx-auto lg:px-8">
<div className="py-6 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center">
<div>
<div className="flex items-center">
<h1 className="ml-3 text-2xl font-bold leading-7 text-gray-900 sm:leading-9 sm:truncate">
{title}
</h1>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Page content */}
<div className="mt-8">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{children}
</div>
</div>
</main>
</div>
</div>
)
}