Extracted components and constants into _components/ subdirectories to bring all three pages under the 300 LOC soft target (was 651/628/612, now 255/232/278 LOC respectively). Zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
256 lines
8.8 KiB
TypeScript
256 lines
8.8 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
import { useParams, useRouter } from 'next/navigation'
|
|
import Link from 'next/link'
|
|
import {
|
|
Course,
|
|
Lesson,
|
|
Enrollment,
|
|
isEnrollmentOverdue,
|
|
} from '@/lib/sdk/academy/types'
|
|
import {
|
|
fetchCourse,
|
|
fetchEnrollments,
|
|
deleteCourse,
|
|
submitQuiz,
|
|
updateLesson,
|
|
generateVideos,
|
|
getVideoStatus,
|
|
} from '@/lib/sdk/academy/api'
|
|
import { CourseHeader } from './_components/CourseHeader'
|
|
import { CourseStats } from './_components/CourseStats'
|
|
import { CourseTabs } from './_components/CourseTabs'
|
|
import { OverviewTab } from './_components/OverviewTab'
|
|
import { LessonsTab } from './_components/LessonsTab'
|
|
import { EnrollmentsTab } from './_components/EnrollmentsTab'
|
|
import { VideosTab } from './_components/VideosTab'
|
|
|
|
type TabId = 'overview' | 'lessons' | 'enrollments' | 'videos'
|
|
|
|
export default function CourseDetailPage() {
|
|
const params = useParams()
|
|
const router = useRouter()
|
|
const courseId = params.id as string
|
|
|
|
const [course, setCourse] = useState<Course | null>(null)
|
|
const [enrollments, setEnrollments] = useState<Enrollment[]>([])
|
|
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [selectedLesson, setSelectedLesson] = useState<Lesson | null>(null)
|
|
const [quizAnswers, setQuizAnswers] = useState<Record<string, number>>({})
|
|
const [quizResult, setQuizResult] = useState<any>(null)
|
|
const [isSubmittingQuiz, setIsSubmittingQuiz] = useState(false)
|
|
const [videoStatus, setVideoStatus] = useState<any>(null)
|
|
const [isGeneratingVideos, setIsGeneratingVideos] = useState(false)
|
|
const [isEditing, setIsEditing] = useState(false)
|
|
const [editContent, setEditContent] = useState('')
|
|
const [editTitle, setEditTitle] = useState('')
|
|
const [isSaving, setIsSaving] = useState(false)
|
|
const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
|
|
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const [courseData, enrollmentData] = await Promise.all([
|
|
fetchCourse(courseId).catch(() => null),
|
|
fetchEnrollments(courseId).catch(() => [])
|
|
])
|
|
setCourse(courseData)
|
|
setEnrollments(Array.isArray(enrollmentData) ? enrollmentData : [])
|
|
if (courseData && courseData.lessons && courseData.lessons.length > 0) {
|
|
setSelectedLesson(courseData.lessons[0])
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load course:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
loadData()
|
|
}, [courseId])
|
|
|
|
const handleDeleteCourse = async () => {
|
|
if (!confirm('Sind Sie sicher, dass Sie diesen Kurs loeschen moechten? Diese Aktion kann nicht rueckgaengig gemacht werden.')) return
|
|
try {
|
|
await deleteCourse(courseId)
|
|
router.push('/sdk/academy')
|
|
} catch (error) {
|
|
console.error('Failed to delete course:', error)
|
|
}
|
|
}
|
|
|
|
const handleSubmitQuiz = async () => {
|
|
if (!selectedLesson) return
|
|
const questions = selectedLesson.quizQuestions || []
|
|
const answers = questions.map((q: any) => quizAnswers[q.id] ?? -1)
|
|
setIsSubmittingQuiz(true)
|
|
try {
|
|
const result = await submitQuiz(selectedLesson.id, { answers })
|
|
setQuizResult(result)
|
|
} catch (error: any) {
|
|
console.error('Quiz submission failed:', error)
|
|
setQuizResult({ error: error.message || 'Fehler bei der Auswertung' })
|
|
} finally {
|
|
setIsSubmittingQuiz(false)
|
|
}
|
|
}
|
|
|
|
const handleStartEdit = () => {
|
|
if (!selectedLesson) return
|
|
setEditContent(selectedLesson.contentMarkdown || '')
|
|
setEditTitle(selectedLesson.title || '')
|
|
setIsEditing(true)
|
|
setSaveMessage(null)
|
|
}
|
|
|
|
const handleCancelEdit = () => {
|
|
setIsEditing(false)
|
|
setSaveMessage(null)
|
|
}
|
|
|
|
const handleSaveLesson = async () => {
|
|
if (!selectedLesson) return
|
|
setIsSaving(true)
|
|
setSaveMessage(null)
|
|
try {
|
|
await updateLesson(selectedLesson.id, { title: editTitle, content_url: editContent })
|
|
const updatedLesson = { ...selectedLesson, title: editTitle, contentMarkdown: editContent }
|
|
setSelectedLesson(updatedLesson)
|
|
if (course) {
|
|
const updatedLessons = course.lessons.map(l => l.id === updatedLesson.id ? updatedLesson : l)
|
|
setCourse({ ...course, lessons: updatedLessons })
|
|
}
|
|
setIsEditing(false)
|
|
setSaveMessage({ type: 'success', text: 'Lektion gespeichert.' })
|
|
} catch (error: any) {
|
|
setSaveMessage({ type: 'error', text: error.message || 'Fehler beim Speichern.' })
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
const handleApproveLesson = async () => {
|
|
if (!selectedLesson) return
|
|
if (!confirm('Lektion fuer Video-Verarbeitung freigeben? Der Text wird als final markiert.')) return
|
|
setIsSaving(true)
|
|
setSaveMessage(null)
|
|
try {
|
|
await updateLesson(selectedLesson.id, { description: 'approved_for_video' })
|
|
const updatedLesson = { ...selectedLesson }
|
|
if (course) {
|
|
const updatedLessons = course.lessons.map(l => l.id === updatedLesson.id ? updatedLesson : l)
|
|
setCourse({ ...course, lessons: updatedLessons })
|
|
}
|
|
setSaveMessage({ type: 'success', text: 'Lektion fuer Video-Verarbeitung freigegeben.' })
|
|
} catch (error: any) {
|
|
setSaveMessage({ type: 'error', text: error.message || 'Fehler bei der Freigabe.' })
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
const handleGenerateVideos = async () => {
|
|
setIsGeneratingVideos(true)
|
|
try {
|
|
const status = await generateVideos(courseId)
|
|
setVideoStatus(status)
|
|
} catch (error) {
|
|
console.error('Video generation failed:', error)
|
|
} finally {
|
|
setIsGeneratingVideos(false)
|
|
}
|
|
}
|
|
|
|
const handleCheckVideoStatus = async () => {
|
|
try {
|
|
const status = await getVideoStatus(courseId)
|
|
setVideoStatus(status)
|
|
} catch (error) {
|
|
console.error('Failed to check video status:', error)
|
|
}
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-20">
|
|
<svg className="animate-spin w-8 h-8 text-purple-600" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!course) {
|
|
return (
|
|
<div className="text-center py-20">
|
|
<h2 className="text-xl font-semibold text-gray-900">Kurs nicht gefunden</h2>
|
|
<Link href="/sdk/academy" className="mt-4 inline-block text-purple-600 hover:underline">
|
|
Zurueck zur Uebersicht
|
|
</Link>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const sortedLessons = [...(course.lessons || [])].sort((a, b) => a.order - b.order)
|
|
const completedEnrollments = enrollments.filter(e => e.status === 'completed').length
|
|
const overdueEnrollments = enrollments.filter(e => isEnrollmentOverdue(e)).length
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<CourseHeader course={course} onDelete={handleDeleteCourse} />
|
|
<CourseStats
|
|
course={course}
|
|
sortedLessons={sortedLessons}
|
|
enrollments={enrollments}
|
|
completedEnrollments={completedEnrollments}
|
|
/>
|
|
<CourseTabs activeTab={activeTab} onTabChange={setActiveTab} />
|
|
|
|
{activeTab === 'overview' && (
|
|
<OverviewTab course={course} sortedLessons={sortedLessons} />
|
|
)}
|
|
|
|
{activeTab === 'lessons' && (
|
|
<LessonsTab
|
|
sortedLessons={sortedLessons}
|
|
selectedLesson={selectedLesson}
|
|
onSelectLesson={(lesson) => { setSelectedLesson(lesson); setQuizResult(null); setQuizAnswers({}) }}
|
|
quizAnswers={quizAnswers}
|
|
onQuizAnswer={setQuizAnswers}
|
|
quizResult={quizResult}
|
|
isSubmittingQuiz={isSubmittingQuiz}
|
|
onSubmitQuiz={handleSubmitQuiz}
|
|
onResetQuiz={() => { setQuizResult(null); setQuizAnswers({}) }}
|
|
isEditing={isEditing}
|
|
editTitle={editTitle}
|
|
editContent={editContent}
|
|
onEditTitle={setEditTitle}
|
|
onEditContent={setEditContent}
|
|
isSaving={isSaving}
|
|
saveMessage={saveMessage}
|
|
onStartEdit={handleStartEdit}
|
|
onCancelEdit={handleCancelEdit}
|
|
onSaveLesson={handleSaveLesson}
|
|
onApproveLesson={handleApproveLesson}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'enrollments' && (
|
|
<EnrollmentsTab enrollments={enrollments} overdueEnrollments={overdueEnrollments} />
|
|
)}
|
|
|
|
{activeTab === 'videos' && (
|
|
<VideosTab
|
|
videoStatus={videoStatus}
|
|
isGeneratingVideos={isGeneratingVideos}
|
|
onGenerateVideos={handleGenerateVideos}
|
|
onCheckVideoStatus={handleCheckVideoStatus}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|