All 5 files reduced below 500 LOC (hard cap) by extracting sub-components: - training/page.tsx: 780→278 LOC — imports existing _components/, adds BlocksSection - control-provenance/page.tsx: 739→145 LOC — extracts provenance-data.ts, ProvenanceHelpers, LicenseMatrix, SourceRegistry - iace/[projectId]/verification/page.tsx: 673→164 LOC — extracts VerificationForm, CompleteModal, SuggestEvidenceModal, VerificationTable - training/learner/page.tsx: 560→216 LOC — extracts AssignmentsList, ContentView, QuizView, CertificatesView - ControlDetail.tsx: 878→311 LOC — adds ControlSourceCitation, ControlTraceability, ControlRegulatorySection, ControlSimilarControls, ControlReviewActions siblings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
83 lines
3.6 KiB
TypeScript
83 lines
3.6 KiB
TypeScript
'use client'
|
|
|
|
import type { TrainingAssignment, ModuleContent, TrainingMedia, InteractiveVideoManifest } from '@/lib/sdk/training/types'
|
|
import { getMediaStreamURL } from '@/lib/sdk/training/api'
|
|
import InteractiveVideoPlayer from '@/components/training/InteractiveVideoPlayer'
|
|
|
|
function simpleMarkdownToHtml(md: string): string {
|
|
return md
|
|
.replace(/^### (.+)$/gm, '<h3 class="text-lg font-semibold mt-4 mb-2">$1</h3>')
|
|
.replace(/^## (.+)$/gm, '<h2 class="text-xl font-bold mt-6 mb-3">$1</h2>')
|
|
.replace(/^# (.+)$/gm, '<h1 class="text-2xl font-bold mt-6 mb-3">$1</h1>')
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
.replace(/^- (.+)$/gm, '<li class="ml-4 list-disc">$1</li>')
|
|
.replace(/^(\d+)\. (.+)$/gm, '<li class="ml-4 list-decimal">$2</li>')
|
|
.replace(/\n\n/g, '<br/><br/>')
|
|
}
|
|
|
|
interface ContentViewProps {
|
|
selectedAssignment: TrainingAssignment | null
|
|
content: ModuleContent | null
|
|
media: TrainingMedia[]
|
|
interactiveManifest: InteractiveVideoManifest | null
|
|
onStartQuiz: () => void
|
|
onAllCheckpointsPassed: () => void
|
|
}
|
|
|
|
export function ContentView({
|
|
selectedAssignment, content, media, interactiveManifest, onStartQuiz, onAllCheckpointsPassed,
|
|
}: ContentViewProps) {
|
|
if (!selectedAssignment) {
|
|
return <div className="text-center py-12 text-gray-400">Waehlen Sie eine Schulung aus dem Tab "Meine Schulungen"</div>
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h2 className="text-lg font-semibold">{selectedAssignment.module_title}</h2>
|
|
<button onClick={onStartQuiz} className="px-4 py-2 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-700">Quiz starten</button>
|
|
</div>
|
|
|
|
{interactiveManifest && (
|
|
<div className="mb-6">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<p className="text-sm font-medium text-gray-700">Interaktive Video-Schulung</p>
|
|
<span className="px-2 py-0.5 text-xs bg-purple-100 text-purple-700 rounded-full">Interaktiv</span>
|
|
</div>
|
|
<InteractiveVideoPlayer
|
|
manifest={interactiveManifest}
|
|
assignmentId={selectedAssignment.id}
|
|
onAllCheckpointsPassed={onAllCheckpointsPassed}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{media.length > 0 && (
|
|
<div className="mb-6 grid gap-4 md:grid-cols-2">
|
|
{media.filter(m => m.media_type === 'audio' && m.status === 'completed').map(m => (
|
|
<div key={m.id} className="bg-gray-50 p-4 rounded-lg">
|
|
<p className="text-sm font-medium text-gray-700 mb-2">Audio-Schulung</p>
|
|
<audio controls className="w-full" src={getMediaStreamURL(m.id)}>Ihr Browser unterstuetzt kein Audio.</audio>
|
|
</div>
|
|
))}
|
|
{media.filter(m => m.media_type === 'video' && m.status === 'completed' && m.generated_by !== 'tts_ffmpeg_interactive').map(m => (
|
|
<div key={m.id} className="bg-gray-50 p-4 rounded-lg">
|
|
<p className="text-sm font-medium text-gray-700 mb-2">Video-Schulung</p>
|
|
<video controls className="w-full rounded" src={getMediaStreamURL(m.id)}>Ihr Browser unterstuetzt kein Video.</video>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{content ? (
|
|
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
|
<div className="prose max-w-none text-gray-800" dangerouslySetInnerHTML={{ __html: simpleMarkdownToHtml(content.content_body) }} />
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-gray-400">Kein Schulungsinhalt verfuegbar</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|