Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions app/Livewire/Customer/Course/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@
#[Title('Course')]
class Index extends Component
{
#[Computed]
public function isAdmin(): bool
{
return auth()->user()?->isAdmin() ?? false;
}

#[Computed]
public function course(): ?Course
{
return Course::where('is_published', true)
return Course::query()
->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true))
->with(['modules' => function ($query) {
$query->where('is_published', true)
$query->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true))
->orderBy('sort_order')
->with(['lessons' => function ($query) {
$query->where('is_published', true)->orderBy('sort_order');
$query->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true))->orderBy('sort_order');
}]);
}])
->first();
Expand Down
26 changes: 23 additions & 3 deletions app/Livewire/Customer/Course/LessonShow.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Models\CourseLesson;
use App\Models\LessonProgress;
use App\Models\Product;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Component;
Expand All @@ -19,19 +20,27 @@ public function mount(CourseLesson $lesson): void
{
$this->lesson = $lesson->load('module.course');

if (! $this->lesson->is_free && ! $this->hasPurchased) {
abort_unless($this->lesson->is_published || $this->isAdmin, 404);

if (! $this->lesson->is_free && ! $this->hasPurchased && ! $this->isAdmin) {
abort(403, 'You need Pro access to view this lesson.');
}
}

#[Computed]
public function isAdmin(): bool
{
return auth()->user()?->isAdmin() ?? false;
}

#[Computed]
public function course(): Course
{
return $this->lesson->module->course->load(['modules' => function ($query) {
$query->where('is_published', true)
$query->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true))
->orderBy('sort_order')
->with(['lessons' => function ($query) {
$query->where('is_published', true)->orderBy('sort_order');
$query->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true))->orderBy('sort_order');
}]);
}]);
}
Expand All @@ -44,6 +53,17 @@ public function hasPurchased(): bool
return $product && $product->isOwnedBy(auth()->user());
}

/**
* @return Collection<int, CourseLesson>
*/
#[Computed]
public function moduleLessons(): Collection
{
return $this->lesson->module->lessons()
->when(! $this->isAdmin, fn ($query) => $query->where('is_published', true))
->get();
}

#[Computed]
public function completedLessonIds(): array
{
Expand Down
13 changes: 9 additions & 4 deletions resources/views/livewire/customer/course.blade.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
<div>
@if ($this->hasPurchased)
{{-- Purchased: Show course content --}}
@if ($this->hasPurchased || $this->isAdmin)
{{-- Purchased (or admin preview): Show course content --}}
<div class="mb-6">
<flux:heading size="xl">Course</flux:heading>
<flux:text>The NativePHP Masterclass</flux:text>
<flux:heading size="xl">The NativePHP Masterclass</flux:heading>
</div>

@if ($this->course)
Expand Down Expand Up @@ -55,6 +54,9 @@ class="h-2 rounded-full bg-emerald-500 transition-all"
@else
<flux:badge variant="pill" color="violet" size="sm">Pro</flux:badge>
@endif
@if (! $module->is_published)
<flux:badge variant="pill" color="amber" size="sm">Draft</flux:badge>
@endif
</div>
@if ($module->description)
<flux:text class="text-sm mt-1">{{ $module->description }}</flux:text>
Expand All @@ -79,6 +81,9 @@ class="h-2 rounded-full bg-emerald-500 transition-all"
{{ $lesson->title }}
</a>
</div>
@if (! $lesson->is_published)
<flux:badge variant="pill" color="amber" size="sm">Draft</flux:badge>
@endif
@if ($lesson->duration_in_seconds)
<flux:text class="text-xs shrink-0">{{ gmdate('i:s', $lesson->duration_in_seconds) }}</flux:text>
@endif
Expand Down
13 changes: 9 additions & 4 deletions resources/views/livewire/customer/course/lesson-show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class="absolute inset-0 w-full h-full"
@else
<flux:badge variant="pill" color="violet" size="sm">Pro</flux:badge>
@endif
@if (! $lesson->is_published)
<flux:badge variant="pill" color="amber" size="sm">Draft</flux:badge>
@endif
<flux:text class="text-xs">{{ $lesson->module->title }}</flux:text>
</div>
<flux:heading size="lg">{{ $lesson->title }}</flux:heading>
Expand Down Expand Up @@ -69,17 +72,16 @@ class="shrink-0 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-semibo
</div>

{{-- Other lessons in this module --}}
@php $moduleLessons = $lesson->module->lessons()->where('is_published', true)->orderBy('sort_order')->get(); @endphp
@if ($moduleLessons->count() > 1)
@if ($this->moduleLessons->count() > 1)
<div class="mt-8 rounded-lg border border-zinc-200 bg-white dark:border-white/10 dark:bg-white/5">
<div class="p-4 border-b border-zinc-200 dark:border-white/10">
<flux:heading size="sm">{{ $lesson->module->title }}</flux:heading>
</div>
@foreach ($moduleLessons as $moduleLesson)
@foreach ($this->moduleLessons as $moduleLesson)
@php
$isCurrent = $moduleLesson->id === $lesson->id;
$isLessonComplete = in_array($moduleLesson->id, $this->completedLessonIds);
$canAccess = $moduleLesson->is_free || $this->hasPurchased;
$canAccess = $moduleLesson->is_free || $this->hasPurchased || $this->isAdmin;
@endphp
@if ($canAccess)
<a
Expand All @@ -96,6 +98,9 @@ class="flex items-center gap-3 px-4 py-3 text-sm transition-colors {{ $isCurrent
@endif
</div>
<span class="{{ $isCurrent ? 'font-medium' : '' }}">{{ $moduleLesson->title }}</span>
@if (! $moduleLesson->is_published)
<flux:badge variant="pill" color="amber" size="sm">Draft</flux:badge>
@endif
@if ($moduleLesson->duration_in_seconds)
<span class="ml-auto text-xs text-zinc-400 dark:text-zinc-500">{{ gmdate('i:s', $moduleLesson->duration_in_seconds) }}</span>
@endif
Expand Down
96 changes: 96 additions & 0 deletions tests/Feature/CourseContentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ public function course_dashboard_shows_modules_and_lessons_for_owners(): void

Livewire::actingAs($user)
->test(Index::class)
->assertSee('The NativePHP Masterclass')
->assertSee('Test Module')
->assertSee('Test Lesson');
}
Expand Down Expand Up @@ -280,6 +281,101 @@ public function user_can_uncomplete_a_lesson(): void
]);
}

#[Test]
public function admin_sees_unpublished_course_content_in_dashboard(): void
{
config(['filament.users' => ['admin@test.com']]);
$admin = User::factory()->create(['email' => 'admin@test.com']);

$course = Course::factory()->create();
$module = CourseModule::factory()->create([
'course_id' => $course->id,
'title' => 'Hidden Module',
]);
CourseLesson::factory()->create([
'course_module_id' => $module->id,
'title' => 'Hidden Lesson',
]);

Livewire::actingAs($admin)
->test(Index::class)
->assertSee('Hidden Module')
->assertSee('Hidden Lesson')
->assertSee('Draft');
}

#[Test]
public function non_admin_owner_does_not_see_unpublished_content_in_dashboard(): void
{
$user = User::factory()->create();
$product = Product::where('slug', 'nativephp-masterclass')->first();
ProductLicense::factory()->create([
'user_id' => $user->id,
'product_id' => $product->id,
]);

$course = Course::factory()->published()->create();
$liveModule = CourseModule::factory()->published()->create([
'course_id' => $course->id,
'title' => 'Live Module',
]);
CourseLesson::factory()->create([
'course_module_id' => $liveModule->id,
'title' => 'Hidden Lesson',
]);
CourseModule::factory()->create([
'course_id' => $course->id,
'title' => 'Hidden Module',
]);

Livewire::actingAs($user)
->test(Index::class)
->assertSee('Live Module')
->assertDontSee('Hidden Lesson')
->assertDontSee('Hidden Module');
}

#[Test]
public function admin_can_view_unpublished_pro_lesson_without_purchase(): void
{
config(['filament.users' => ['admin@test.com']]);
$admin = User::factory()->create(['email' => 'admin@test.com']);

$course = Course::factory()->create();
$module = CourseModule::factory()->create(['course_id' => $course->id]);
$lesson = CourseLesson::factory()->create([
'course_module_id' => $module->id,
'is_free' => false,
'title' => 'Hidden Pro Lesson',
]);

Livewire::actingAs($admin)
->test(LessonShow::class, ['lesson' => $lesson])
->assertSee('Hidden Pro Lesson')
->assertSee('Draft');
}

#[Test]
public function unpublished_lesson_is_not_accessible_to_non_admins(): void
{
$user = User::factory()->create();
$product = Product::where('slug', 'nativephp-masterclass')->first();
ProductLicense::factory()->create([
'user_id' => $user->id,
'product_id' => $product->id,
]);

$course = Course::factory()->published()->create();
$module = CourseModule::factory()->published()->create(['course_id' => $course->id]);
$lesson = CourseLesson::factory()->create([
'course_module_id' => $module->id,
]);

Livewire::actingAs($user)
->test(LessonShow::class, ['lesson' => $lesson])
->assertNotFound();
}

#[Test]
public function course_model_has_modules_relationship(): void
{
Expand Down
Loading