fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
76
admin-v2/components/layout/Header.tsx
Normal file
76
admin-v2/components/layout/Header.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { navigation, metaModules, getModuleByHref } from '@/lib/navigation'
|
||||
|
||||
interface HeaderProps {
|
||||
title?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function Header({ title, description }: HeaderProps) {
|
||||
const pathname = usePathname()
|
||||
|
||||
// Auto-detect title and description from navigation
|
||||
let pageTitle = title
|
||||
let pageDescription = description
|
||||
|
||||
if (!pageTitle) {
|
||||
// Check meta modules first
|
||||
const metaModule = metaModules.find(m => pathname === m.href || pathname.startsWith(m.href + '/'))
|
||||
if (metaModule) {
|
||||
pageTitle = metaModule.name
|
||||
pageDescription = metaModule.description
|
||||
} else {
|
||||
// Check navigation modules
|
||||
const result = getModuleByHref(pathname)
|
||||
if (result) {
|
||||
pageTitle = result.module.name
|
||||
pageDescription = result.module.description
|
||||
} else {
|
||||
// Check category pages
|
||||
const category = navigation.find(cat => pathname === `/${cat.id}`)
|
||||
if (category) {
|
||||
pageTitle = category.name
|
||||
pageDescription = category.description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="h-16 bg-white border-b border-slate-200 flex items-center px-6 sticky top-0 z-10">
|
||||
<div className="flex-1">
|
||||
{pageTitle && <h1 className="text-xl font-semibold text-slate-900">{pageTitle}</h1>}
|
||||
{pageDescription && <p className="text-sm text-slate-500">{pageDescription}</p>}
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Suchen... (Ctrl+K)"
|
||||
className="w-64 pl-10 pr-4 py-2 bg-slate-100 border border-transparent rounded-lg text-sm focus:bg-white focus:border-primary-300 focus:outline-none transition-colors"
|
||||
/>
|
||||
<svg
|
||||
className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* User Area */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-slate-500">Admin v2</span>
|
||||
<div className="w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center text-white text-sm font-medium">
|
||||
A
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
107
admin-v2/components/layout/RoleIndicator.tsx
Normal file
107
admin-v2/components/layout/RoleIndicator.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { roles, getRoleById, getStoredRole, storeRole, RoleId } from '@/lib/roles'
|
||||
|
||||
interface RoleIndicatorProps {
|
||||
collapsed?: boolean
|
||||
onRoleChange?: () => void
|
||||
}
|
||||
|
||||
export function RoleIndicator({ collapsed, onRoleChange }: RoleIndicatorProps) {
|
||||
const router = useRouter()
|
||||
const [currentRole, setCurrentRole] = useState<RoleId | null>(null)
|
||||
const [showDropdown, setShowDropdown] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const role = getStoredRole()
|
||||
setCurrentRole(role)
|
||||
}, [])
|
||||
|
||||
const handleRoleChange = (roleId: RoleId) => {
|
||||
storeRole(roleId)
|
||||
setCurrentRole(roleId)
|
||||
setShowDropdown(false)
|
||||
onRoleChange?.()
|
||||
// Refresh the page to update navigation
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
const role = currentRole ? getRoleById(currentRole) : null
|
||||
|
||||
if (!role) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Role icons
|
||||
const roleIcons: Record<RoleId, React.ReactNode> = {
|
||||
developer: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
),
|
||||
manager: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
),
|
||||
auditor: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
),
|
||||
dsb: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-slate-300 hover:bg-slate-800 transition-colors ${
|
||||
collapsed ? 'justify-center' : ''
|
||||
}`}
|
||||
title={collapsed ? `Rolle: ${role.name}` : undefined}
|
||||
>
|
||||
{currentRole && roleIcons[currentRole]}
|
||||
{!collapsed && (
|
||||
<>
|
||||
<span className="flex-1 text-left">Rolle: {role.name}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform ${showDropdown ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Dropdown */}
|
||||
{showDropdown && (
|
||||
<div className={`absolute ${collapsed ? 'left-full ml-2' : 'left-0 right-0'} bottom-full mb-2 bg-slate-800 rounded-lg shadow-lg border border-slate-700 overflow-hidden`}>
|
||||
{roles.map((r) => (
|
||||
<button
|
||||
key={r.id}
|
||||
onClick={() => handleRoleChange(r.id)}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors ${
|
||||
r.id === currentRole
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'text-slate-300 hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
{roleIcons[r.id]}
|
||||
<span>{r.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
284
admin-v2/components/layout/Sidebar.tsx
Normal file
284
admin-v2/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { navigation, metaModules, NavCategory, CategoryId } from '@/lib/navigation'
|
||||
import { RoleId, getStoredRole, isCategoryVisibleForRole } from '@/lib/roles'
|
||||
import { RoleIndicator } from './RoleIndicator'
|
||||
|
||||
// Icons mapping
|
||||
const categoryIcons: Record<string, React.ReactNode> = {
|
||||
'shield-check': (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
),
|
||||
'clipboard-check': (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
),
|
||||
shield: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
),
|
||||
brain: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
),
|
||||
server: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
</svg>
|
||||
),
|
||||
graduation: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 14l9-5-9-5-9 5 9 5z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998a12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222" />
|
||||
</svg>
|
||||
),
|
||||
mail: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
),
|
||||
code: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
const metaIcons: Record<string, React.ReactNode> = {
|
||||
dashboard: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
),
|
||||
architecture: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
),
|
||||
onboarding: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
),
|
||||
backlog: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
),
|
||||
rbac: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
interface SidebarProps {
|
||||
onRoleChange?: () => void
|
||||
}
|
||||
|
||||
export function Sidebar({ onRoleChange }: SidebarProps) {
|
||||
const pathname = usePathname()
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<CategoryId>>(new Set())
|
||||
const [currentRole, setCurrentRole] = useState<RoleId | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const role = getStoredRole()
|
||||
setCurrentRole(role)
|
||||
// Auto-expand category based on current path
|
||||
if (role) {
|
||||
const category = navigation.find(cat =>
|
||||
cat.modules.some(m => pathname.startsWith(m.href.split('/')[1] ? `/${m.href.split('/')[1]}` : m.href))
|
||||
)
|
||||
if (category) {
|
||||
setExpandedCategories(new Set([category.id]))
|
||||
}
|
||||
}
|
||||
}, [pathname])
|
||||
|
||||
const toggleCategory = (categoryId: CategoryId) => {
|
||||
setExpandedCategories(prev => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(categoryId)) {
|
||||
newSet.delete(categoryId)
|
||||
} else {
|
||||
newSet.add(categoryId)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
|
||||
const isModuleActive = (href: string) => {
|
||||
if (href === '/dashboard') {
|
||||
return pathname === '/dashboard'
|
||||
}
|
||||
return pathname.startsWith(href)
|
||||
}
|
||||
|
||||
const visibleCategories = currentRole
|
||||
? navigation.filter(cat => isCategoryVisibleForRole(cat.id, currentRole))
|
||||
: navigation
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`${
|
||||
collapsed ? 'w-16' : 'w-64'
|
||||
} bg-slate-900 text-white flex flex-col transition-all duration-300 fixed h-full z-20`}
|
||||
>
|
||||
{/* Logo/Header */}
|
||||
<div className="h-16 flex items-center justify-between px-4 border-b border-slate-700">
|
||||
{!collapsed && (
|
||||
<Link href="/dashboard" className="font-bold text-lg">
|
||||
Admin v2
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="p-2 rounded-lg hover:bg-slate-800 transition-colors"
|
||||
title={collapsed ? 'Sidebar erweitern' : 'Sidebar einklappen'}
|
||||
>
|
||||
<svg
|
||||
className={`w-5 h-5 transition-transform ${collapsed ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 py-4 overflow-y-auto">
|
||||
{/* Meta Modules */}
|
||||
<div className="px-2 mb-4">
|
||||
{metaModules.map((module) => (
|
||||
<Link
|
||||
key={module.id}
|
||||
href={module.href}
|
||||
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${
|
||||
isModuleActive(module.href)
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
|
||||
}`}
|
||||
title={collapsed ? module.name : undefined}
|
||||
>
|
||||
<span className="flex-shrink-0">{metaIcons[module.id]}</span>
|
||||
{!collapsed && <span className="truncate">{module.name}</span>}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="mx-4 border-t border-slate-700 my-2" />
|
||||
|
||||
{/* Categories */}
|
||||
<div className="px-2 space-y-1">
|
||||
{visibleCategories.map((category) => {
|
||||
const categoryHref = `/${category.id === 'compliance' ? 'compliance' : category.id}`
|
||||
const isCategoryActive = pathname.startsWith(categoryHref)
|
||||
|
||||
return (
|
||||
<div key={category.id}>
|
||||
<div
|
||||
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${
|
||||
isCategoryActive
|
||||
? 'bg-slate-800 text-white'
|
||||
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
href={categoryHref}
|
||||
className="flex items-center gap-3 flex-1 min-w-0"
|
||||
title={collapsed ? category.name : undefined}
|
||||
>
|
||||
<span
|
||||
className="flex-shrink-0"
|
||||
style={{ color: category.color }}
|
||||
>
|
||||
{categoryIcons[category.icon]}
|
||||
</span>
|
||||
{!collapsed && (
|
||||
<span className="flex-1 text-left truncate">{category.name}</span>
|
||||
)}
|
||||
</Link>
|
||||
{!collapsed && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
toggleCategory(category.id)
|
||||
}}
|
||||
className="p-1 rounded hover:bg-slate-700 transition-colors"
|
||||
title={expandedCategories.has(category.id) ? 'Einklappen' : 'Aufklappen'}
|
||||
>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform ${
|
||||
expandedCategories.has(category.id) ? 'rotate-180' : ''
|
||||
}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Category Modules */}
|
||||
{!collapsed && expandedCategories.has(category.id) && (
|
||||
<div className="ml-4 mt-1 space-y-1">
|
||||
{category.modules.map((module) => (
|
||||
<Link
|
||||
key={module.id}
|
||||
href={module.href}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
|
||||
isModuleActive(module.href)
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
style={{ backgroundColor: category.color }}
|
||||
/>
|
||||
<span className="truncate">{module.name}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Footer with Role Indicator */}
|
||||
<div className="p-4 border-t border-slate-700">
|
||||
<RoleIndicator collapsed={collapsed} onRoleChange={onRoleChange} />
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
className={`flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-colors mt-2 ${
|
||||
collapsed ? 'justify-center' : ''
|
||||
}`}
|
||||
title="Zur Website"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
{!collapsed && <span>Altes Admin</span>}
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user