Add admin-core frontend (Port 3008)
Next.js admin frontend for Core with 3 categories (Communication, Infrastructure, Development), 13 modules, 2 roles (developer, ops), and 11 API proxy routes. Includes docker-compose service and nginx SSL config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
65
admin-core/components/common/Breadcrumbs.tsx
Normal file
65
admin-core/components/common/Breadcrumbs.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { navigation, metaModules, getModuleByHref, getCategoryById, CategoryId } from '@/lib/navigation'
|
||||
|
||||
export function Breadcrumbs() {
|
||||
const pathname = usePathname()
|
||||
|
||||
const items: Array<{ label: string; href: string }> = []
|
||||
|
||||
items.push({ label: 'Dashboard', href: '/dashboard' })
|
||||
|
||||
const pathParts = pathname.split('/').filter(Boolean)
|
||||
|
||||
if (pathParts.length > 0) {
|
||||
const categoryId = pathParts[0] as CategoryId
|
||||
const category = getCategoryById(categoryId)
|
||||
|
||||
if (category) {
|
||||
items.push({ label: category.name, href: `/${category.id}` })
|
||||
|
||||
if (pathParts.length > 1) {
|
||||
const moduleHref = `/${pathParts[0]}/${pathParts[1]}`
|
||||
const result = getModuleByHref(moduleHref)
|
||||
if (result) {
|
||||
items.push({ label: result.module.name, href: moduleHref })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const metaModule = metaModules.find(m => m.href === `/${pathParts[0]}`)
|
||||
if (metaModule && metaModule.href !== '/dashboard') {
|
||||
items.push({ label: metaModule.name, href: metaModule.href })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length <= 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="flex items-center gap-2 text-sm text-slate-500 mb-4">
|
||||
{items.map((item, index) => (
|
||||
<span key={`${index}-${item.href}`} className="flex items-center gap-2">
|
||||
{index > 0 && (
|
||||
<svg className="w-4 h-4 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
)}
|
||||
{index === items.length - 1 ? (
|
||||
<span className="text-slate-900 font-medium">{item.label}</span>
|
||||
) : (
|
||||
<Link
|
||||
href={item.href}
|
||||
className="hover:text-primary-600 transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
169
admin-core/components/common/PagePurpose.tsx
Normal file
169
admin-core/components/common/PagePurpose.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export interface PagePurposeProps {
|
||||
title: string
|
||||
purpose: string
|
||||
audience: string[]
|
||||
gdprArticles?: string[]
|
||||
architecture?: {
|
||||
services: string[]
|
||||
databases: string[]
|
||||
diagram?: string
|
||||
}
|
||||
relatedPages?: Array<{
|
||||
name: string
|
||||
href: string
|
||||
description: string
|
||||
}>
|
||||
collapsible?: boolean
|
||||
defaultCollapsed?: boolean
|
||||
}
|
||||
|
||||
export function PagePurpose({
|
||||
title,
|
||||
purpose,
|
||||
audience,
|
||||
gdprArticles,
|
||||
architecture,
|
||||
relatedPages,
|
||||
collapsible = true,
|
||||
defaultCollapsed = false,
|
||||
}: PagePurposeProps) {
|
||||
const [collapsed, setCollapsed] = useState(defaultCollapsed)
|
||||
const [showArchitecture, setShowArchitecture] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-r from-slate-50 to-slate-100 rounded-xl border border-slate-200 mb-6 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div
|
||||
className={`flex items-center justify-between px-4 py-3 ${
|
||||
collapsible ? 'cursor-pointer hover:bg-slate-100' : ''
|
||||
}`}
|
||||
onClick={collapsible ? () => setCollapsed(!collapsed) : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">🎯</span>
|
||||
<span className="font-semibold text-slate-700">Warum gibt es diese Seite?</span>
|
||||
</div>
|
||||
{collapsible && (
|
||||
<svg
|
||||
className={`w-5 h-5 text-slate-400 transition-transform ${collapsed ? '' : '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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{!collapsed && (
|
||||
<div className="px-4 pb-4 space-y-4">
|
||||
{/* Purpose */}
|
||||
<p className="text-slate-600">{purpose}</p>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
{/* Audience */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-400">👥</span>
|
||||
<span className="text-slate-500">Zielgruppe:</span>
|
||||
<span className="text-slate-700">{audience.join(', ')}</span>
|
||||
</div>
|
||||
|
||||
{/* GDPR Articles */}
|
||||
{gdprArticles && gdprArticles.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-400">📋</span>
|
||||
<span className="text-slate-500">DSGVO-Bezug:</span>
|
||||
<span className="text-slate-700">{gdprArticles.join(', ')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Architecture (expandable) */}
|
||||
{architecture && (
|
||||
<div className="border-t border-slate-200 pt-3">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowArchitecture(!showArchitecture)
|
||||
}}
|
||||
className="flex items-center gap-2 text-sm text-slate-500 hover:text-slate-700"
|
||||
>
|
||||
<span>🏗️</span>
|
||||
<span>Architektur</span>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform ${showArchitecture ? '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>
|
||||
|
||||
{showArchitecture && (
|
||||
<div className="mt-2 p-3 bg-white rounded-lg border border-slate-200 text-sm">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<span className="font-medium text-slate-600">Services:</span>
|
||||
<ul className="mt-1 space-y-1 text-slate-500">
|
||||
{architecture.services.map((service) => (
|
||||
<li key={service} className="flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 bg-primary-400 rounded-full" />
|
||||
{service}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-slate-600">Datenbanken:</span>
|
||||
<ul className="mt-1 space-y-1 text-slate-500">
|
||||
{architecture.databases.map((db) => (
|
||||
<li key={db} className="flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 bg-green-400 rounded-full" />
|
||||
{db}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Related Pages */}
|
||||
{relatedPages && relatedPages.length > 0 && (
|
||||
<div className="border-t border-slate-200 pt-3">
|
||||
<span className="text-sm text-slate-500 flex items-center gap-2 mb-2">
|
||||
<span>🔗</span>
|
||||
<span>Verwandte Seiten</span>
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{relatedPages.map((page) => (
|
||||
<Link
|
||||
key={page.href}
|
||||
href={page.href}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 bg-white border border-slate-200 rounded-lg text-sm text-slate-600 hover:border-primary-300 hover:text-primary-600 transition-colors"
|
||||
title={page.description}
|
||||
>
|
||||
<span>{page.name}</span>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user