From 65ccbd555ac78c30cfb13bad767442efd04ac64f Mon Sep 17 00:00:00 2001 From: Tajim Date: Wed, 26 Aug 2026 11:52:13 +0600 Subject: [PATCH 1/4] feat(db): add resource_completions table and model relationships --- app/Models/Resource.php | 5 +++ app/Models/ResourceCompletion.php | 24 ++++++++++++++ app/Models/User.php | 10 ++++++ ...4000_create_resource_completions_table.php | 31 +++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 app/Models/ResourceCompletion.php create mode 100644 database/migrations/2026_08_26_114000_create_resource_completions_table.php diff --git a/app/Models/Resource.php b/app/Models/Resource.php index c5956d8..c3e4c12 100644 --- a/app/Models/Resource.php +++ b/app/Models/Resource.php @@ -47,4 +47,9 @@ public function user() { return $this->belongsTo(User::class); } + + public function completions() + { + return $this->hasMany(ResourceCompletion::class); + } } diff --git a/app/Models/ResourceCompletion.php b/app/Models/ResourceCompletion.php new file mode 100644 index 0000000..47d784c --- /dev/null +++ b/app/Models/ResourceCompletion.php @@ -0,0 +1,24 @@ +belongsTo(Resource::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 88a954c..206f211 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -82,4 +82,14 @@ public function blogs(): HasMany { return $this->hasmany(Blog::class); } + + public function resourceCompletions(): HasMany + { + return $this->hasMany(ResourceCompletion::class); + } + + public function completedResources() + { + return $this->belongsToMany(Resource::class, 'resource_completions'); + } } diff --git a/database/migrations/2026_08_26_114000_create_resource_completions_table.php b/database/migrations/2026_08_26_114000_create_resource_completions_table.php new file mode 100644 index 0000000..90053b1 --- /dev/null +++ b/database/migrations/2026_08_26_114000_create_resource_completions_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('resource_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['resource_id', 'user_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('resource_completions'); + } +}; From 7ed5337733c1e8674dee4628af74db9aa35a29f1 Mon Sep 17 00:00:00 2001 From: Tajim Date: Wed, 26 Aug 2026 11:52:15 +0600 Subject: [PATCH 2/4] feat(resource): add completion toggle endpoint and completers query --- app/Http/Controllers/ResourceController.php | 38 +++++++++++++++++++-- routes/web.php | 1 + 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/ResourceController.php b/app/Http/Controllers/ResourceController.php index 357e07c..80f4b2e 100644 --- a/app/Http/Controllers/ResourceController.php +++ b/app/Http/Controllers/ResourceController.php @@ -3,13 +3,12 @@ namespace App\Http\Controllers; use App\Models\Resource; +use App\Models\ResourceCompletion; use Illuminate\Support\Facades\Cache; use Inertia\Inertia; class ResourceController extends Controller { - // - public function show($id) { $data = Cache::rememberForever("resource_{$id}", function () use ($id) { @@ -32,6 +31,39 @@ public function show($id) ]; }); - return Inertia::render('Resource', $data); + $isCompleted = auth()->check() + ? ResourceCompletion::where('resource_id', $id)->where('user_id', auth()->id())->exists() + : false; + + $completionsCount = ResourceCompletion::where('resource_id', $id)->count(); + + $completers = ResourceCompletion::where('resource_id', $id) + ->with('user:id,name,image_path,institution') + ->latest() + ->take(10) + ->get() + ->pluck('user') + ->filter() + ->values(); + + return Inertia::render('Resource', array_merge($data, [ + 'isCompleted' => $isCompleted, + 'completionsCount' => $completionsCount, + 'completers' => $completers, + ])); + } + + public function toggleComplete(Resource $resource) + { + $user = auth()->user(); + $existing = $resource->completions()->where('user_id', $user->id)->first(); + + if ($existing) { + $existing->delete(); + } else { + $resource->completions()->create(['user_id' => $user->id]); + } + + return back(); } } diff --git a/routes/web.php b/routes/web.php index 783c8b3..a5d4787 100644 --- a/routes/web.php +++ b/routes/web.php @@ -25,6 +25,7 @@ Route::post('/blogs/{blog}/react', [BlogController::class, 'toggleReaction'])->name('blogs.react'); Route::post('/blogs/{blog}/comments', [BlogController::class, 'storeComment'])->name('blogs.comments.store'); Route::delete('/blogs/comments/{comment}', [BlogController::class, 'destroyComment'])->name('blogs.comments.destroy'); + Route::post('/resources/{resource}/complete', [ResourceController::class, 'toggleComplete'])->name('resources.complete'); }); Route::prefix('admin') From a054da7ca05a9e1b5acbe6733228a3adeefefe4b Mon Sep 17 00:00:00 2001 From: Tajim Date: Wed, 26 Aug 2026 11:52:19 +0600 Subject: [PATCH 3/4] test(resource): add feature tests for guest redirection and user completion toggle --- tests/Feature/ResourceCompletionTest.php | 72 ++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/Feature/ResourceCompletionTest.php diff --git a/tests/Feature/ResourceCompletionTest.php b/tests/Feature/ResourceCompletionTest.php new file mode 100644 index 0000000..39e04aa --- /dev/null +++ b/tests/Feature/ResourceCompletionTest.php @@ -0,0 +1,72 @@ + 'Physics', + 'slug' => 'physics', + 'course' => 'hsc', + 'tailwind_format' => 'bg-indigo-500', + 'icon' => 'atom', + ]); + $node = Node::create([ + 'subject_id' => $subject->id, + 'name' => 'Chapter 1', + 'slug' => 'chapter-1', + ]); + $resource = Resource::create([ + 'node_id' => $node->id, + 'resource_type' => 'image', + 'title' => 'Vectors Handnote', + 'file_path' => 'resources/sample.jpg', + ]); + + $this->post("/resources/{$resource->id}/complete") + ->assertRedirect('/login'); +}); + +test('authenticated user can toggle resource completion status', function () { + $user = User::factory()->create(); + $subject = Subject::create([ + 'name' => 'Physics', + 'slug' => 'physics', + 'course' => 'hsc', + 'tailwind_format' => 'bg-indigo-500', + 'icon' => 'atom', + ]); + $node = Node::create([ + 'subject_id' => $subject->id, + 'name' => 'Chapter 1', + 'slug' => 'chapter-1', + ]); + $resource = Resource::create([ + 'node_id' => $node->id, + 'resource_type' => 'image', + 'title' => 'Vectors Handnote', + 'file_path' => 'resources/sample.jpg', + ]); + + // Mark as done + $this->actingAs($user) + ->post("/resources/{$resource->id}/complete") + ->assertRedirect(); + + $this->assertDatabaseHas('resource_completions', [ + 'resource_id' => $resource->id, + 'user_id' => $user->id, + ]); + + // Undo / Unmark + $this->actingAs($user) + ->post("/resources/{$resource->id}/complete") + ->assertRedirect(); + + $this->assertDatabaseMissing('resource_completions', [ + 'resource_id' => $resource->id, + 'user_id' => $user->id, + ]); +}); From ca73ed463c51a088ccf6c35d4cfa6f013369ed4c Mon Sep 17 00:00:00 2001 From: Tajim Date: Wed, 26 Aug 2026 11:52:22 +0600 Subject: [PATCH 4/4] feat(resource): add mark as done action, completers modal, and optimistic UI --- resources/js/pages/Resource.vue | 293 +++++++++++++++++++++++++++++++- 1 file changed, 289 insertions(+), 4 deletions(-) diff --git a/resources/js/pages/Resource.vue b/resources/js/pages/Resource.vue index df4e7bf..7654429 100644 --- a/resources/js/pages/Resource.vue +++ b/resources/js/pages/Resource.vue @@ -1,5 +1,5 @@