From 7769d2038a6e50fe5e5a90ea4550b0f7fcab3a5e Mon Sep 17 00:00:00 2001 From: root Date: Mon, 10 Aug 2026 12:04:50 +0300 Subject: [PATCH] Add favorite toggle to yoga poses Exercises and workouts both have a favorite toggle; yoga poses had an otherwise identical browse/filter UX but no way to favorite them - an inconsistency rather than a deliberate omission. Mirrors the existing exercise/workout favorite pattern exactly: a pivot table, a belongsToMany on User, and a toggle button on the pose detail page. Co-Authored-By: Claude Sonnet 5 --- app/Http/Controllers/FavoriteController.php | 15 ++++++++++ app/Models/User.php | 10 +++++++ ...90234_create_yoga_pose_favorites_table.php | 30 +++++++++++++++++++ resources/views/yoga/show.blade.php | 16 ++++++++++ routes/web.php | 1 + tests/Feature/FavoriteTest.php | 22 ++++++++++++++ 6 files changed, 94 insertions(+) create mode 100644 database/migrations/2026_08_10_090234_create_yoga_pose_favorites_table.php diff --git a/app/Http/Controllers/FavoriteController.php b/app/Http/Controllers/FavoriteController.php index 0f3076d..879055e 100644 --- a/app/Http/Controllers/FavoriteController.php +++ b/app/Http/Controllers/FavoriteController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Models\Exercise; use App\Models\Workout; +use App\Models\YogaPose; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Auth; @@ -39,4 +40,18 @@ class FavoriteController extends Controller } return back()->with('status', $msg); } + + public function toggleYogaPose(YogaPose $pose): RedirectResponse + { + $user = Auth::user(); + $exists = $user->favoriteYogaPoses()->where('yoga_pose_id', $pose->id)->exists(); + if ($exists) { + $user->favoriteYogaPoses()->detach($pose->id); + $msg = 'Removed from favorites'; + } else { + $user->favoriteYogaPoses()->attach($pose->id); + $msg = 'Added to favorites'; + } + return back()->with('status', $msg); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 254663c..e82ca67 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -79,6 +79,11 @@ class User extends Authenticatable return $this->belongsToMany(Workout::class, 'workout_favorites')->withTimestamps(); } + public function favoriteYogaPoses() + { + return $this->belongsToMany(YogaPose::class, 'yoga_pose_favorites')->withTimestamps(); + } + // Helpers public function hasFavoritedExercise(Exercise $exercise): bool { @@ -89,4 +94,9 @@ class User extends Authenticatable { return $this->favoriteWorkouts()->where('workout_id', $workout->id)->exists(); } + + public function hasFavoritedYogaPose(YogaPose $pose): bool + { + return $this->favoriteYogaPoses()->where('yoga_pose_id', $pose->id)->exists(); + } } diff --git a/database/migrations/2026_08_10_090234_create_yoga_pose_favorites_table.php b/database/migrations/2026_08_10_090234_create_yoga_pose_favorites_table.php new file mode 100644 index 0000000..ecbc8c2 --- /dev/null +++ b/database/migrations/2026_08_10_090234_create_yoga_pose_favorites_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('yoga_pose_id')->constrained('yoga_poses')->cascadeOnDelete(); + $table->timestamps(); + $table->unique(['user_id', 'yoga_pose_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('yoga_pose_favorites'); + } +}; diff --git a/resources/views/yoga/show.blade.php b/resources/views/yoga/show.blade.php index b182292..524f801 100644 --- a/resources/views/yoga/show.blade.php +++ b/resources/views/yoga/show.blade.php @@ -13,6 +13,22 @@ ← {{ __('app.yoga.back') }}

{{ $pose->english_name }}

+ @auth + @php $isFav = auth()->user()->hasFavoritedYogaPose($pose); @endphp +
+ @csrf + +
+ @endauth
@if($pose->category) diff --git a/routes/web.php b/routes/web.php index f85a0fa..a71262b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -26,6 +26,7 @@ Route::middleware('auth')->group(function () { // Favorites Route::post('/exercises/{exercise}/favorite', [\App\Http\Controllers\FavoriteController::class, 'toggleExercise'])->name('exercises.favorite'); Route::post('/workouts/{workout}/favorite', [\App\Http\Controllers\FavoriteController::class, 'toggleWorkout'])->name('workouts.favorite'); + Route::post('/yoga/{pose}/favorite', [\App\Http\Controllers\FavoriteController::class, 'toggleYogaPose'])->name('yoga.favorite'); // Exercise alias (per-user rename) Route::post('/exercises/{exercise}/alias', [\App\Http\Controllers\ExerciseAliasController::class, 'store'])->name('exercises.alias'); diff --git a/tests/Feature/FavoriteTest.php b/tests/Feature/FavoriteTest.php index 368be0c..7ba28a4 100644 --- a/tests/Feature/FavoriteTest.php +++ b/tests/Feature/FavoriteTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature; use App\Models\Exercise; use App\Models\User; use App\Models\Workout; +use App\Models\YogaPose; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -44,4 +45,25 @@ class FavoriteTest extends TestCase $this->actingAs($user)->post(route('workouts.favorite', $workout)); $this->assertFalse($user->favoriteWorkouts()->where('workout_id', $workout->id)->exists()); } + + public function test_guest_cannot_favorite_a_yoga_pose(): void + { + $pose = YogaPose::factory()->create(); + + $response = $this->post(route('yoga.favorite', $pose)); + + $response->assertRedirect(route('login')); + } + + public function test_user_can_toggle_yoga_pose_favorite(): void + { + $user = User::factory()->create(); + $pose = YogaPose::factory()->create(); + + $this->actingAs($user)->post(route('yoga.favorite', $pose)); + $this->assertTrue($user->favoriteYogaPoses()->where('yoga_pose_id', $pose->id)->exists()); + + $this->actingAs($user)->post(route('yoga.favorite', $pose)); + $this->assertFalse($user->favoriteYogaPoses()->where('yoga_pose_id', $pose->id)->exists()); + } }