validate([ 'q' => ['nullable', 'string', 'max:120'], 'category' => ['nullable', 'string', 'max:140'], 'difficulty' => ['nullable', 'string', 'max:40'], ]); $query = AcademyLesson::query() ->with('category') ->active() ->published() ->orderedForCourse(); if (filled($filters['q'] ?? null)) { $query->where(function ($builder) use ($filters): void { $builder->where('title', 'like', '%'.$filters['q'].'%') ->orWhere('excerpt', 'like', '%'.$filters['q'].'%'); }); } if (filled($filters['category'] ?? null)) { $query->whereHas('category', fn ($builder) => $builder->where('slug', $filters['category'])); } if (filled($filters['difficulty'] ?? null)) { $query->where('difficulty', $filters['difficulty']); } $lessons = $query->paginate(12)->withQueryString(); $lessons->getCollection()->transform(fn (AcademyLesson $lesson): array => $this->access->lessonPayload($lesson, $request->user())); $seo = app(SeoFactory::class) ->collectionListing( 'Academy Lessons — Skinbase', 'Browse Skinbase AI Academy lessons covering prompting, workflow cleanup, ethics, and upload-ready creative workflows.', route('academy.lessons.index', $request->query()), ) ->toArray(); return Inertia::render('Academy/List', [ 'pageType' => 'lessons', 'title' => 'Academy lessons', 'description' => 'Step-by-step tutorials and workflow guides for AI-assisted creative work on Skinbase.', 'seo' => $seo, 'items' => $lessons, 'filters' => $filters, 'categories' => $this->cache->categoriesByType('lesson'), 'pricingUrl' => route('academy.pricing'), ])->rootView('collections'); } public function show(Request $request, string $slug): Response { abort_unless((bool) config('academy.enabled', true), 404); $lesson = AcademyLesson::query() ->with(['category', 'activeBlocks.activeComparisonResults']) ->active() ->published() ->where('slug', $slug) ->firstOrFail(); $payload = $this->access->lessonPayload($lesson, $request->user(), true); $courseQuery = AcademyLesson::query() ->with('category') ->active() ->published(); if (filled($lesson->series_name)) { $courseQuery->where('series_name', $lesson->series_name); } elseif ($lesson->category_id !== null) { $courseQuery->where('category_id', $lesson->category_id); } else { $courseQuery->whereKey($lesson->id); } $courseLessons = $courseQuery ->orderedForCourse() ->get() ->filter(fn (AcademyLesson $courseLesson): bool => $this->access->canAccessLesson($request->user(), $courseLesson)) ->values(); $currentIndex = $courseLessons->search(fn (AcademyLesson $courseLesson): bool => $courseLesson->is($lesson)); $previousLesson = is_int($currentIndex) && $currentIndex > 0 ? $courseLessons->get($currentIndex - 1) : null; $nextLesson = is_int($currentIndex) && $currentIndex < ($courseLessons->count() - 1) ? $courseLessons->get($currentIndex + 1) : null; $relatedLessons = $courseLessons ->reject(fn (AcademyLesson $courseLesson): bool => $courseLesson->is($lesson)) ->take(6) ->map(fn (AcademyLesson $relatedLesson): array => $this->access->lessonPayload($relatedLesson, $request->user())) ->values() ->all(); $relatedCourses = AcademyCourse::query() ->published() ->ordered() ->whereHas('courseLessons', fn ($builder) => $builder->where('lesson_id', $lesson->id)) ->limit(3) ->get() ->map(fn (AcademyCourse $course): array => $this->access->coursePayload($course, $request->user())) ->values() ->all(); $canonical = route('academy.lessons.show', ['slug' => $lesson->slug]); $description = Str::limit(trim((string) ($lesson->seo_description ?? $lesson->excerpt ?? 'Skinbase Academy lesson.')), 160, '...'); $seo = app(SeoFactory::class)->academyLessonPage( (string) ($lesson->seo_title ?? ($lesson->title.' — Skinbase Academy')), $description, $canonical, (string) ($payload['article_cover_image_url'] ?? $payload['cover_image_url'] ?? $lesson->cover_image ?? ''), [ ['name' => 'Academy', 'url' => route('academy.index')], ['name' => 'Lessons', 'url' => route('academy.lessons.index')], ['name' => (string) $lesson->title, 'url' => $canonical], ], array_values((array) ($payload['tags'] ?? [])), $lesson->published_at?->toAtomString(), $lesson->updated_at?->toAtomString(), (string) ($lesson->series_name ?: $lesson->category?->name ?: 'Academy'), )->toArray(); return Inertia::render('Academy/Show', [ 'pageType' => 'lesson', 'item' => $payload, 'relatedLessons' => $relatedLessons, 'relatedCourses' => $relatedCourses, 'previousLesson' => $previousLesson ? $this->access->lessonPayload($previousLesson, $request->user()) : null, 'nextLesson' => $nextLesson ? $this->access->lessonPayload($nextLesson, $request->user()) : null, 'seo' => $seo, 'pricingUrl' => route('academy.pricing'), 'completeUrl' => $request->user() ? route('academy.lessons.complete', ['lesson' => $lesson->id]) : null, 'completed' => $request->user()?->academyLessonProgress()->where('lesson_id', $lesson->id)->whereNotNull('completed_at')->exists() ?? false, ])->rootView('collections'); } }