feat: implement Notion-like LMS (Course Player, Exam, Certificate) with R2 integration

This commit is contained in:
2026-01-24 11:11:19 +07:00
parent 594f3727f5
commit 27fc78e811
17 changed files with 1139 additions and 30 deletions

View File

@@ -42,6 +42,14 @@ class CoursePlayerController extends Controller
? Lesson::where('slug', $lessonSlug)->with('vocabularies')->firstOrFail()
: $allLessons->first()->load('vocabularies');
// Navigation Logic (Next/Prev)
$currentIndex = $allLessons->search(function ($item) use ($currentLesson) {
return $item->id === $currentLesson->id;
});
$prevLesson = $currentIndex > 0 ? $allLessons[$currentIndex - 1] : null;
$nextLesson = $currentIndex < $allLessons->count() - 1 ? $allLessons[$currentIndex + 1] : null;
// Get user progress for this course
$completedLessonsIds = UserProgress::where('user_id', $user->id)
->whereIn('lesson_id', $allLessons->pluck('id'))
@@ -55,42 +63,52 @@ class CoursePlayerController extends Controller
['started_at' => now(), 'last_heartbeat_at' => now()]
);
return Inertia::render('Courses/Player', [
return Inertia::render('Courses/Learn', [
'course' => [
'id' => $course->id,
'title' => $course->title,
'slug' => $course->slug,
'modules' => $course->modules->map(function ($module) use ($completedLessonsIds) {
return [
'id' => $module->id,
'title' => $module->title,
'lessons' => $module->lessons->map(function ($lesson) use ($completedLessonsIds) {
return [
'id' => $lesson->id,
'title' => $lesson->title,
'slug' => $lesson->slug,
'type' => $lesson->type,
'is_completed' => in_array($lesson->id, $completedLessonsIds),
];
})
];
})
'progress_percentage' => count($allLessons) > 0 ? round((count($completedLessonsIds) / count($allLessons)) * 100) : 0,
],
'currentLesson' => [
'modules' => $course->modules->map(function ($module) use ($completedLessonsIds) {
return [
'id' => $module->id,
'title' => $module->title,
'lessons' => $module->lessons->map(function ($lesson) use ($completedLessonsIds) {
return [
'id' => $lesson->id,
'title' => $lesson->title,
'slug' => $lesson->slug,
'type' => $lesson->type,
'duration_seconds' => $lesson->duration_seconds,
'is_completed' => in_array($lesson->id, $completedLessonsIds),
];
})
];
}),
'lesson' => [
'id' => $currentLesson->id,
'title' => $currentLesson->title,
'slug' => $currentLesson->slug,
'type' => $currentLesson->type,
'content' => $currentLesson->content,
'video_url' => $currentLesson->video_url,
'content_pdf' => $currentLesson->content_pdf,
// Prefer Spatie Media Library URL (R2), fallback to legacy column
'content_pdf' => $currentLesson->getFirstMediaUrl('content_pdf') ?: $currentLesson->content_pdf,
'duration_seconds' => $currentLesson->duration_seconds,
'is_completed' => in_array($currentLesson->id, $completedLessonsIds),
'vocabularies' => $currentLesson->vocabularies,
'next_lesson_slug' => $nextLesson?->slug,
'prev_lesson_slug' => $prevLesson?->slug,
'attachments' => $currentLesson->getMedia('attachments')->map(function ($media) {
return [
'id' => $media->uuid,
'name' => $media->file_name,
'url' => $media->getUrl(),
'size' => $media->human_readable_size,
];
}),
],
'progress' => [
'completed_count' => count($completedLessonsIds),
'total_count' => count($allLessons),
'percentage' => count($allLessons) > 0 ? round((count($completedLessonsIds) / count($allLessons)) * 100) : 0,
]
]);
}