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>
135 lines
5.7 KiB
TypeScript
135 lines
5.7 KiB
TypeScript
'use client'
|
|
|
|
import type { DisplayEvidence, DisplayEvidenceType } from './EvidenceTypes'
|
|
|
|
const typeIcons: Record<DisplayEvidenceType, React.ReactNode> = {
|
|
document: (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
),
|
|
screenshot: (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
),
|
|
log: (
|
|
<svg className="w-6 h-6" 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>
|
|
),
|
|
'audit-report': (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
),
|
|
certificate: (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
|
</svg>
|
|
),
|
|
}
|
|
|
|
const statusColors = {
|
|
valid: 'bg-green-100 text-green-700 border-green-200',
|
|
expired: 'bg-red-100 text-red-700 border-red-200',
|
|
'pending-review': 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
|
}
|
|
|
|
const statusLabels = {
|
|
valid: 'Gueltig',
|
|
expired: 'Abgelaufen',
|
|
'pending-review': 'Pruefung ausstehend',
|
|
}
|
|
|
|
const typeIconBg: Record<DisplayEvidenceType, string> = {
|
|
certificate: 'bg-yellow-100 text-yellow-600',
|
|
'audit-report': 'bg-purple-100 text-purple-600',
|
|
screenshot: 'bg-blue-100 text-blue-600',
|
|
log: 'bg-green-100 text-green-600',
|
|
document: 'bg-gray-100 text-gray-600',
|
|
}
|
|
|
|
export function EvidenceCard({
|
|
evidence,
|
|
onDelete,
|
|
onView,
|
|
onDownload,
|
|
}: {
|
|
evidence: DisplayEvidence
|
|
onDelete: () => void
|
|
onView: () => void
|
|
onDownload: () => void
|
|
}) {
|
|
return (
|
|
<div className={`bg-white rounded-xl border-2 p-6 ${
|
|
evidence.status === 'expired' ? 'border-red-200' :
|
|
evidence.status === 'pending-review' ? 'border-yellow-200' : 'border-gray-200'
|
|
}`}>
|
|
<div className="flex items-start gap-4">
|
|
<div className={`w-12 h-12 rounded-lg flex items-center justify-center ${typeIconBg[evidence.displayType]}`}>
|
|
{typeIcons[evidence.displayType]}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-gray-900">{evidence.name}</h3>
|
|
<span className={`px-3 py-1 text-xs rounded-full ${statusColors[evidence.status]}`}>
|
|
{statusLabels[evidence.status]}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-500 mt-1">{evidence.description}</p>
|
|
|
|
<div className="mt-3 flex items-center gap-4 text-sm text-gray-500">
|
|
<span>Hochgeladen: {evidence.uploadedAt.toLocaleDateString('de-DE')}</span>
|
|
{evidence.validUntil && (
|
|
<span className={evidence.status === 'expired' ? 'text-red-600' : ''}>
|
|
Gueltig bis: {evidence.validUntil.toLocaleDateString('de-DE')}
|
|
</span>
|
|
)}
|
|
<span>{evidence.fileSize}</span>
|
|
</div>
|
|
|
|
<div className="mt-3 flex items-center gap-2 flex-wrap">
|
|
{evidence.linkedRequirements.map(req => (
|
|
<span key={req} className="px-2 py-0.5 text-xs bg-blue-50 text-blue-600 rounded">
|
|
{req}
|
|
</span>
|
|
))}
|
|
{evidence.linkedControls.map(ctrl => (
|
|
<span key={ctrl} className="px-2 py-0.5 text-xs bg-green-50 text-green-600 rounded">
|
|
{ctrl}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between">
|
|
<span className="text-sm text-gray-500">Hochgeladen von: {evidence.uploadedBy}</span>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={onView}
|
|
disabled={!evidence.fileUrl}
|
|
className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
>
|
|
Anzeigen
|
|
</button>
|
|
<button
|
|
onClick={onDownload}
|
|
disabled={!evidence.fileUrl}
|
|
className="px-3 py-1 text-sm text-purple-600 hover:bg-purple-50 rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
>
|
|
Herunterladen
|
|
</button>
|
|
<button
|
|
onClick={onDelete}
|
|
className="px-3 py-1 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
>
|
|
Loeschen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|