validate([ 'difficulty' => ['nullable', 'string', 'max:40'], 'access' => ['nullable', 'string', 'max:40'], ]); $query = AcademyCourse::query()->published()->ordered(); if (filled($filters['difficulty'] ?? null)) { $query->where('difficulty', $filters['difficulty']); } if (filled($filters['access'] ?? null)) { $query->where('access_level', $filters['access']); } $courses = $query->paginate(12)->withQueryString(); $courses->getCollection()->transform(function (AcademyCourse $course) use ($request): array { return $this->access->coursePayload($course, $request->user(), [ 'progress' => $this->progress->getProgress($request->user(), $course), ]); }); $featuredCourses = collect($this->cache->featuredCourses())->map(fn (AcademyCourse $course): array => $this->access->coursePayload($course, $request->user(), [ 'progress' => $this->progress->getProgress($request->user(), $course), ]))->values(); $seoCourses = $featuredCourses ->concat(collect($courses->items())) ->unique(fn (array $course): string => (string) ($course['slug'] ?? '')) ->values(); $seo = app(SeoFactory::class) ->academyCourseListingPage( 'Academy Courses — Skinbase', 'Follow guided Skinbase AI Academy courses built from reusable lessons, chapters, and creator workflows.', route('academy.courses.index', $request->query()), $seoCourses, [ ['name' => 'Academy', 'url' => route('academy.index')], ['name' => 'Courses', 'url' => route('academy.courses.index')], ], ) ->toArray(); return Inertia::render('Academy/CoursesIndex', [ 'seo' => $seo, 'title' => 'Academy courses', 'description' => 'Guided learning paths built from reusable Academy lessons and creator workflows.', 'items' => $courses, 'featuredCourses' => $featuredCourses->all(), 'filters' => $filters, 'pricingUrl' => route('academy.pricing'), ])->rootView('collections'); } public function show(Request $request, AcademyCourse $course): Response { abort_unless((bool) config('academy.enabled', true), 404); abort_unless($course->isPublished(), 404); $course->load(['sections', 'courseLessons.section', 'courseLessons.lesson.category']); $progress = $this->progress->getProgress($request->user(), $course); $completedLessonIds = $request->user() ? $this->progress->getCompletedLessonIds($request->user(), $course) : []; $orderedLessons = $this->navigation->orderedCourseLessons($course); $stepMeta = $orderedLessons ->values() ->mapWithKeys(fn (AcademyCourseLesson $courseLesson, int $index): array => [ $courseLesson->id => [ 'course_step_number' => $index + 1, 'course_step_label' => sprintf('Step %02d', $index + 1), ], ]); $sections = $course->sections ->sortBy([['order_num', 'asc'], ['id', 'asc']]) ->values() ->map(function ($section) use ($completedLessonIds, $orderedLessons, $request, $stepMeta): array { $sectionLessons = $orderedLessons ->where('section_id', $section->id) ->values() ->map(fn (AcademyCourseLesson $courseLesson): array => $this->access->courseLessonPayload($courseLesson, $request->user(), false, [ 'completed_lesson_ids' => $completedLessonIds, ...((array) $stepMeta->get($courseLesson->id, [])), ])) ->all(); return [ 'id' => (int) $section->id, 'title' => (string) $section->title, 'slug' => (string) ($section->slug ?? ''), 'description' => (string) ($section->description ?? ''), 'order_num' => (int) ($section->order_num ?? 0), 'is_visible' => (bool) ($section->is_visible ?? true), 'lessons' => $sectionLessons, ]; }) ->all(); $unsectionedLessons = $orderedLessons ->whereNull('section_id') ->values() ->map(fn (AcademyCourseLesson $courseLesson): array => $this->access->courseLessonPayload($courseLesson, $request->user(), false, [ 'completed_lesson_ids' => $completedLessonIds, ...((array) $stepMeta->get($courseLesson->id, [])), ])) ->all(); $coursePayload = $this->access->coursePayload($course, $request->user(), ['progress' => $progress]); $courseKeywords = collect(explode(',', (string) ($course->meta_keywords ?? ''))) ->map(fn (string $keyword): string => trim($keyword)) ->filter() ->values() ->all(); $courseImage = (string) ($coursePayload['cover_image_url'] ?? $coursePayload['teaser_image_url'] ?? $course->og_image ?? $course->cover_image ?? $course->teaser_image ?? ''); $seo = app(SeoFactory::class) ->academyCoursePage( (string) ($course->seo_title ?: ($course->title . ' — Skinbase Academy')), (string) ($course->seo_description ?: $course->excerpt ?: 'Skinbase Academy course'), route('academy.courses.show', ['course' => $course->slug]), $courseImage, [ ['name' => 'Academy', 'url' => route('academy.index')], ['name' => 'Courses', 'url' => route('academy.courses.index')], ['name' => (string) $course->title, 'url' => route('academy.courses.show', ['course' => $course->slug])], ], $courseKeywords, $course->published_at?->toAtomString(), $course->updated_at?->toAtomString(), (string) ($course->access_level ?? ''), (string) ($course->difficulty ?? ''), (int) ($course->estimated_minutes ?? 0), $orderedLessons ->values() ->map(fn (AcademyCourseLesson $courseLesson): array => $this->access->courseLessonPayload($courseLesson, $request->user(), false, [ 'completed_lesson_ids' => $completedLessonIds, ...((array) $stepMeta->get($courseLesson->id, [])), ])) ->all(), ) ->toArray(); return Inertia::render('Academy/CoursesShow', [ 'seo' => $seo, 'course' => $coursePayload, 'sections' => $sections, 'unsectionedLessons' => $unsectionedLessons, 'pricingUrl' => route('academy.pricing'), 'startUrl' => $request->user() ? route('academy.courses.start', ['course' => $course->slug]) : null, ])->rootView('collections'); } }