import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ChevronLeft, ChevronRight, PlayCircle, Save } from 'lucide-react' import { useParams } from 'react-router' import { ApiError } from '../api/client' import { commitLearnerScormRuntime, fetchLearnerScormLaunch, fetchLearnerScormRuntime, type LearnerScormLaunchDetail, type LearnerScormRuntime } from '../learner-scorm' import { generateSCOHtml } from './scorm-shared' type LoadState = 'loading' | 'ready' | 'error' export function LearnerScormLaunchPage() { const { id = '' } = useParams<{ id: string }>() const courseID = id.trim() const [detail, setDetail] = useState(null) const [runtime, setRuntime] = useState(null) const [scoIndex, setScoIndex] = useState(0) const [state, setState] = useState('loading') const [error, setError] = useState('') const [saving, setSaving] = useState(false) const valuesRef = useRef>({}) useEffect(() => { if (!courseID) { setError('A SCORM course id is required.'); setState('error'); return } let cancelled = false void fetchLearnerScormLaunch(courseID).then(result => { if (cancelled) return setDetail(result); setState('ready') }).catch(err => { if (cancelled) return setError(err instanceof ApiError ? err.message : 'Failed to load SCORM course'); setState('error') }) return () => { cancelled = true } }, [courseID]) const selectedSCO = detail?.course.scos[scoIndex] ?? null useEffect(() => { if (!selectedSCO || !courseID) return void fetchLearnerScormRuntime(courseID, selectedSCO.id).then(next => { setRuntime(next) valuesRef.current = { ...next.values } }).catch(err => setError(err instanceof Error ? err.message : 'Unable to load runtime state')) }, [courseID, selectedSCO?.id]) const commit = useCallback(async () => { if (!selectedSCO || !courseID) return setSaving(true) try { const values = { ...valuesRef.current } const lessonStatus = String(values['cmi.core.lesson_status'] ?? runtime?.lessonStatus ?? '') const completionStatus = String(values['cmi.completion_status'] ?? runtime?.completionStatus ?? '') const successStatus = String(values['cmi.success_status'] ?? runtime?.successStatus ?? '') const score = Number(values['cmi.core.score.raw'] ?? values['cmi.score.raw'] ?? runtime?.score ?? 0) const progress = Number(values['cmi.progress_measure'] ?? runtime?.progress ?? 0) const result = await commitLearnerScormRuntime(courseID, { scoId: selectedSCO.id, status: 'committed', location: String(values['cmi.core.lesson_location'] ?? values['cmi.location'] ?? ''), suspendData: String(values['cmi.suspend_data'] ?? ''), lessonStatus, completionStatus, successStatus, score: Number.isFinite(score) ? score : 0, progress: Number.isFinite(progress) ? (progress <= 1 ? progress * 100 : progress) : 0, values, }) setRuntime(result.runtime) } finally { setSaving(false) } }, [courseID, runtime, selectedSCO]) useEffect(() => { if (!selectedSCO) return const getValue = (key: string) => String(valuesRef.current[key] ?? '') const setValue = (key: string, value: string) => { valuesRef.current[key] = value; return 'true' } const api12 = { LMSInitialize: () => 'true', LMSFinish: () => { void commit(); return 'true' }, LMSGetValue: getValue, LMSSetValue: setValue, LMSCommit: () => { void commit(); return 'true' }, LMSGetLastError: () => '0', LMSGetErrorString: () => 'No error', LMSGetDiagnostic: () => '', } const api2004 = { Initialize: () => 'true', Terminate: () => { void commit(); return 'true' }, GetValue: getValue, SetValue: setValue, Commit: () => { void commit(); return 'true' }, GetLastError: () => '0', GetErrorString: () => 'No error', GetDiagnostic: () => '', } const globalWindow = window as unknown as Record globalWindow.API = api12 globalWindow.API_1484_11 = api2004 return () => { delete globalWindow.API delete globalWindow.API_1484_11 } }, [commit, selectedSCO]) const html = useMemo(() => detail && selectedSCO ? generateSCOHtml(detail.course, selectedSCO) : '', [detail, selectedSCO]) const percent = detail?.enrollment.progress ?? 0 return (

{detail?.course.title ?? 'SCORM Launch'}

SCORM progress, bookmark data, scores, and suspend data are persisted to your learner record.

{state === 'loading' &&

Loading launch details...

} {state === 'error' &&

{error}

} {state === 'ready' && detail && !detail.launchState.allowed &&
{detail.launchState.message || 'This package is not available yet.'}
} {state === 'ready' && detail && detail.launchState.allowed && selectedSCO && ( <>

Lesson {scoIndex + 1} of {detail.course.scos.length}

{selectedSCO.title}