Extract hooks, sub-components, and constants into colocated files to bring all three page.tsx files under the 500-LOC hard cap (225, 134, 111 LOC). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
75 lines
3.8 KiB
TypeScript
75 lines
3.8 KiB
TypeScript
'use client'
|
||
|
||
import { ChevronRight, BookOpen, Clock } from 'lucide-react'
|
||
import { CanonicalControl, SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, EvidenceTypeBadge, TargetAudienceBadge, GenerationStrategyBadge, ObligationTypeBadge } from './helpers'
|
||
|
||
interface ControlListItemProps {
|
||
ctrl: CanonicalControl
|
||
sortBy: string
|
||
prevSource: string | null
|
||
onClick: () => void
|
||
}
|
||
|
||
export function ControlListItem({ ctrl, sortBy, prevSource, onClick }: ControlListItemProps) {
|
||
const curSource = ctrl.source_citation?.source || 'Ohne Quelle'
|
||
const showSourceHeader = sortBy === 'source' && curSource !== prevSource
|
||
|
||
return (
|
||
<div key={ctrl.control_id}>
|
||
{showSourceHeader && (
|
||
<div className="flex items-center gap-2 pt-3 pb-1">
|
||
<div className="h-px flex-1 bg-blue-200" />
|
||
<span className="text-xs font-semibold text-blue-700 bg-blue-50 px-2 py-0.5 rounded whitespace-nowrap">{curSource}</span>
|
||
<div className="h-px flex-1 bg-blue-200" />
|
||
</div>
|
||
)}
|
||
<button
|
||
onClick={onClick}
|
||
className="w-full text-left bg-white border border-gray-200 rounded-lg p-4 hover:border-purple-300 hover:shadow-sm transition-all group"
|
||
>
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||
<span className="text-xs font-mono text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded">{ctrl.control_id}</span>
|
||
<SeverityBadge severity={ctrl.severity} />
|
||
<StateBadge state={ctrl.release_state} />
|
||
<LicenseRuleBadge rule={ctrl.license_rule} />
|
||
<VerificationMethodBadge method={ctrl.verification_method} />
|
||
<CategoryBadge category={ctrl.category} />
|
||
<EvidenceTypeBadge type={ctrl.evidence_type} />
|
||
<TargetAudienceBadge audience={ctrl.target_audience} />
|
||
<GenerationStrategyBadge strategy={ctrl.generation_strategy} pipelineInfo={ctrl} />
|
||
<ObligationTypeBadge type={ctrl.generation_metadata?.obligation_type as string} />
|
||
{ctrl.risk_score !== null && (
|
||
<span className="text-xs text-gray-400">Score: {ctrl.risk_score}</span>
|
||
)}
|
||
</div>
|
||
<h3 className="text-sm font-medium text-gray-900 group-hover:text-purple-700">{ctrl.title}</h3>
|
||
<p className="text-xs text-gray-500 mt-1 line-clamp-2">{ctrl.objective}</p>
|
||
<div className="flex items-center gap-2 mt-2">
|
||
<BookOpen className="w-3 h-3 text-green-600" />
|
||
<span className="text-xs text-green-700">{ctrl.open_anchors.length} Referenzen</span>
|
||
{ctrl.source_citation?.source && (
|
||
<>
|
||
<span className="text-gray-300">|</span>
|
||
<span className="text-xs text-blue-600">
|
||
{ctrl.source_citation.source}
|
||
{ctrl.source_citation.article && ` ${ctrl.source_citation.article}`}
|
||
{ctrl.source_citation.paragraph && ` ${ctrl.source_citation.paragraph}`}
|
||
</span>
|
||
</>
|
||
)}
|
||
<span className="text-gray-300">|</span>
|
||
<Clock className="w-3 h-3 text-gray-400" />
|
||
<span className="text-xs text-gray-400" title={ctrl.created_at}>
|
||
{ctrl.created_at ? new Date(ctrl.created_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' }) : '–'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<ChevronRight className="w-4 h-4 text-gray-300 group-hover:text-purple-500 flex-shrink-0 mt-1 ml-4" />
|
||
</div>
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|