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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('yoga_pose_favorites', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -13,6 +13,22 @@
|
||||
<a href="{{ url()->previous() ?? route('yoga.index') }}" class="inline-flex items-center text-sm font-medium text-white/90 hover:text-white">← {{ __('app.yoga.back') }}</a>
|
||||
<div class="mt-3 flex items-start justify-between gap-4">
|
||||
<h1 class="text-3xl sm:text-5xl font-extrabold tracking-tight text-white drop-shadow">{{ $pose->english_name }}</h1>
|
||||
@auth
|
||||
@php $isFav = auth()->user()->hasFavoritedYogaPose($pose); @endphp
|
||||
<form method="POST" action="{{ route('yoga.favorite', $pose) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-full bg-white/90 px-3 py-1.5 text-sm font-semibold text-gray-900 shadow hover:bg-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 {{ $isFav ? 'text-rose-600' : 'text-gray-400' }}">
|
||||
@if($isFav)
|
||||
<path d="M11.645 20.91l-.007-.003-.022-.01a15.247 15.247 0 01-.383-.18 25.18 25.18 0 01-4.244-2.686C4.688 15.734 2.25 13.153 2.25 9.75 2.25 7.005 4.46 4.875 7.125 4.875c1.6 0 3.09.744 4.125 1.917a5.625 5.625 0 014.125-1.917c2.665 0 4.875 2.13 4.875 4.875 0 3.403-2.438 5.984-4.739 7.58a25.175 25.175 0 01-4.244 2.685 15.247 15.247 0 01-.383.18l-.022.01-.007.004-.003.001a.75.75 0 01-.592 0l-.003-.001z"/>
|
||||
@else
|
||||
<path fill-rule="evenodd" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 6.01 4.01 4 6.5 4c1.74 0 3.41.81 4.5 2.09A6.004 6.004 0 0115.5 4C17.99 4 20 6.01 20 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" clip-rule="evenodd"/>
|
||||
@endif
|
||||
</svg>
|
||||
<span>{{ $isFav ? 'Favorited' : 'Favorite' }}</span>
|
||||
</button>
|
||||
</form>
|
||||
@endauth
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||||
@if($pose->category)
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user