184 lines
8.1 KiB
PHP
184 lines
8.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Academy;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\AcademyCourse;
|
|
use App\Models\AcademyCourseLesson;
|
|
use App\Services\Academy\AcademyAccessService;
|
|
use App\Services\Academy\AcademyCacheService;
|
|
use App\Services\Academy\AcademyCourseNavigationService;
|
|
use App\Services\Academy\AcademyCourseProgressService;
|
|
use App\Support\Seo\SeoFactory;
|
|
use Illuminate\Http\Request;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
final class AcademyCourseController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly AcademyAccessService $access,
|
|
private readonly AcademyCacheService $cache,
|
|
private readonly AcademyCourseNavigationService $navigation,
|
|
private readonly AcademyCourseProgressService $progress,
|
|
) {
|
|
}
|
|
|
|
public function index(Request $request): Response
|
|
{
|
|
abort_unless((bool) config('academy.enabled', true), 404);
|
|
|
|
$filters = $request->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');
|
|
}
|
|
} |