chore: commit remaining workspace changes

This commit is contained in:
2026-05-08 21:51:29 +02:00
parent 8d108b8a76
commit ff96ef796e
97 changed files with 18020 additions and 2196 deletions

View File

@@ -0,0 +1,184 @@
<?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');
}
}