refactor(admin): split evidence, import, portfolio pages
Extract components and hooks from oversized pages into colocated _components/ and _hooks/ subdirectories to enforce the 500-LOC hard cap. page.tsx files reduced to 205, 121, and 136 LOC respectively. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
116
admin-compliance/app/sdk/gci/_components/GCIHelpers.tsx
Normal file
116
admin-compliance/app/sdk/gci/_components/GCIHelpers.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import { MaturityLevel, MATURITY_INFO, getScoreColor, getScoreRingColor } from '@/lib/sdk/gci/types'
|
||||
|
||||
export type TabId = 'overview' | 'breakdown' | 'nis2' | 'iso' | 'matrix' | 'audit'
|
||||
|
||||
export interface Tab {
|
||||
id: TabId
|
||||
label: string
|
||||
}
|
||||
|
||||
export const TABS: Tab[] = [
|
||||
{ id: 'overview', label: 'Uebersicht' },
|
||||
{ id: 'breakdown', label: 'Breakdown' },
|
||||
{ id: 'nis2', label: 'NIS2' },
|
||||
{ id: 'iso', label: 'ISO 27001' },
|
||||
{ id: 'matrix', label: 'Matrix' },
|
||||
{ id: 'audit', label: 'Audit Trail' },
|
||||
]
|
||||
|
||||
export function TabNavigation({ tabs, activeTab, onTabChange }: { tabs: Tab[]; activeTab: TabId; onTabChange: (tab: TabId) => void }) {
|
||||
return (
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="flex gap-1 -mb-px overflow-x-auto" aria-label="Tabs">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
|
||||
activeTab === tab.id
|
||||
? 'border-purple-600 text-purple-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ScoreCircle({ score, size = 144, label }: { score: number; size?: number; label?: string }) {
|
||||
const radius = (size / 2) - 12
|
||||
const circumference = 2 * Math.PI * radius
|
||||
const strokeDashoffset = circumference - (score / 100) * circumference
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col items-center">
|
||||
<svg className="-rotate-90" width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
||||
<circle cx={size/2} cy={size/2} r={radius} stroke="#e5e7eb" strokeWidth="8" fill="none" />
|
||||
<circle
|
||||
cx={size/2} cy={size/2} r={radius}
|
||||
stroke={getScoreRingColor(score)}
|
||||
strokeWidth="8" fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
className="transition-all duration-1000"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className={`text-3xl font-bold ${getScoreColor(score)}`}>{score.toFixed(1)}</span>
|
||||
{label && <span className="text-xs text-gray-500 mt-1">{label}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MaturityBadge({ level }: { level: MaturityLevel }) {
|
||||
const info = MATURITY_INFO[level] || MATURITY_INFO.HIGH_RISK
|
||||
return (
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${info.bgColor} ${info.color} border ${info.borderColor}`}>
|
||||
{info.label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function AreaScoreBar({ name, score, weight }: { name: string; score: number; weight: number }) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-gray-700">{name}</span>
|
||||
<span className={`font-semibold ${getScoreColor(score)}`}>{score.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
||||
<div
|
||||
className="h-3 rounded-full transition-all duration-700"
|
||||
style={{ width: `${Math.min(score, 100)}%`, backgroundColor: getScoreRingColor(score) }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Gewichtung: {(weight * 100).toFixed(0)}%</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LoadingSpinner() {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ErrorMessage({ message, onRetry }: { message: string; onRetry?: () => void }) {
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700">
|
||||
<p>{message}</p>
|
||||
{onRetry && (
|
||||
<button onClick={onRetry} className="mt-2 text-sm underline hover:no-underline">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user