- 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
305 lines
11 KiB
TypeScript
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>
|
|
)
|
|
} |