Files
fitness/resources/views/exercises/show.blade.php
T
rootandClaude Sonnet 5 fa8bf46f8a Add per-exercise personal record tracking
Session tracking only ever stored a done boolean - no record of what
weight or reps were actually performed, so there was no way to derive
real progress data even though the dashboard implied "progress
tracking" was a thing. Workout templates only hold a static planned
value, not a history of performance.

Adds actual_weight/actual_reps to workout_session_items, captured
inline when checking off a set in a session (defaults to the workout's
planned weight/reps, editable before confirming). The exercise detail
page now shows the user's personal best (heaviest completed set, tie
broken by reps) computed from their completed session history across
all workouts containing that exercise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 12:15:14 +03:00

159 lines
12 KiB
PHP

<x-app-layout>
<section class="relative overflow-hidden">
<div class="absolute inset-0">
<img src="{{ $exercise->poster_url ?: ($exercise->gif_url ?: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?q=80&w=1920&auto=format&fit=crop') }}" alt="{{ $exercise->display_name }}" class="h-full w-full object-cover">
<div class="absolute inset-0 bg-gradient-to-b from-indigo-900/70 via-indigo-800/60 to-transparent"></div>
</div>
<div class="relative mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
<a href="{{ url()->previous() ?? route('exercises.index') }}" class="inline-flex items-center text-sm font-medium text-white/90 hover:text-white">
&#8592; 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">{{ $exercise->display_name }}</h1>
@auth
<div class="flex items-center gap-3">
@php $isFav = auth()->user()->hasFavoritedExercise($exercise); @endphp
<form method="POST" action="{{ route('exercises.favorite', $exercise) }}">
@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>
@php $alias = optional($exercise->aliases->first())->name ?? ''; @endphp
<form method="POST" action="{{ route('exercises.alias', $exercise) }}" class="hidden sm:flex items-center gap-2 bg-white/90 rounded-full px-2 py-1.5 shadow">
@csrf
<input type="text" name="name" value="{{ old('name', $alias) }}" placeholder="{{ __('app.exercises.rename_placeholder') }}" class="w-40 sm:w-56 bg-transparent text-sm text-gray-900 placeholder-gray-500 focus:outline-none">
<button type="submit" class="text-xs font-semibold text-indigo-600 hover:text-indigo-500">{{ __('app.exercises.rename_save') }}</button>
@if($alias !== '')
<button type="submit" name="name" value="" class="text-xs text-gray-500 hover:text-gray-700">{{ __('app.exercises.rename_reset') }}</button>
@endif
</form>
</div>
@endauth
</div>
<div class="mt-4 flex flex-wrap items-center gap-2">
@foreach($exercise->bodyParts as $bp)
<span class="inline-flex items-center rounded-full bg-white/90 px-3 py-1 text-xs font-semibold text-gray-900 shadow">{{ $bp->name }}</span>
@endforeach
@foreach($exercise->targetMuscles as $m)
<span class="inline-flex items-center rounded-full bg-emerald-100 text-emerald-700 px-2.5 py-1 text-xs font-semibold">Target: {{ $m->name }}</span>
@endforeach
@foreach($exercise->secondaryMuscles as $m)
<span class="inline-flex items-center rounded-full bg-sky-100 text-sky-700 px-2.5 py-1 text-xs font-semibold">Secondary: {{ $m->name }}</span>
@endforeach
@foreach($exercise->equipments as $e)
<span class="inline-flex items-center rounded-full bg-amber-100 text-amber-700 px-2.5 py-1 text-xs font-semibold">{{ ucfirst($e->name) }}</span>
@endforeach
</div>
</div>
</section>
<section class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-10">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<div class="lg:col-span-8">
<div class="rounded-3xl overflow-hidden ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800">
<div class="relative aspect-[16/9] bg-black">
<img src="{{ $exercise->gif_url ?: ($exercise->poster_url ?: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?q=80&w=1600&auto=format&fit=crop') }}" alt="{{ $exercise->name }} demo" class="h-full w-full object-cover">
</div>
<div class="p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">How to perform</h2>
<div class="prose prose-indigo dark:prose-invert max-w-none mt-3">
@php $instructions = trim((string) $exercise->instructions); @endphp
@if($instructions !== '')
@foreach(preg_split('/\r?\n+/', $instructions) as $line)
@if(trim($line) !== '')<p>{{ $line }}</p>@endif
@endforeach
@else
<p>No instructions available.</p>
@endif
</div>
</div>
</div>
</div>
<div class="lg:col-span-4">
@auth
<div class="rounded-2xl p-5 ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 mb-6">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{{ __('app.exercises.personal_best') }}</h3>
@if($personalBest)
<div class="mt-2 flex items-baseline gap-2">
<span class="text-2xl font-extrabold text-gray-900 dark:text-white">{{ rtrim(rtrim(number_format($personalBest->actual_weight, 1), '0'), '.') }} kg</span>
@if($personalBest->actual_reps)
<span class="text-sm text-gray-600 dark:text-gray-300">&times; {{ $personalBest->actual_reps }} {{ __('app.sessions.reps') }}</span>
@endif
</div>
<p class="mt-1 text-xs text-gray-500">{{ optional($personalBest->session)->completed_at?->format('M j, Y') ?? $personalBest->updated_at->format('M j, Y') }}</p>
@else
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300">{{ __('app.exercises.no_personal_best') }}</p>
@endif
</div>
@endauth
<div class="rounded-2xl p-5 ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">Exercise Info</h3>
<dl class="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
@if($exercise->bodyParts->count())
<div class="flex justify-between"><dt>Body Part</dt><dd>{{ $exercise->bodyParts->pluck('name')->join(', ') }}</dd></div>
@endif
@if($exercise->targetMuscles->count())
<div class="flex justify-between"><dt>Target Muscles</dt><dd>{{ $exercise->targetMuscles->pluck('name')->join(', ') }}</dd></div>
@endif
@if($exercise->secondaryMuscles->count())
<div class="flex justify-between"><dt>Secondary Muscles</dt><dd>{{ $exercise->secondaryMuscles->pluck('name')->join(', ') }}</dd></div>
@endif
@if($exercise->equipments->count())
<div class="flex justify-between"><dt>Equipment</dt><dd>{{ $exercise->equipments->pluck('name')->join(', ') }}</dd></div>
@endif
</dl>
<div class="mt-5">
<a href="{{ route('workouts.builder') }}" class="inline-flex items-center rounded-xl bg-gradient-to-r from-fuchsia-600 to-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow hover:from-fuchsia-500 hover:to-indigo-500">Add to Workout</a>
</div>
</div>
</div>
</div>
</section>
@if(isset($similarExercises))
<section class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-12">
<div class="mt-8">
<div class="flex items-end justify-between gap-4 mb-4">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">{{ __('app.exercises.similar') }}</h2>
<a href="{{ route('exercises.index') }}" class="text-sm font-semibold text-indigo-600 hover:text-indigo-500">{{ __('app.home.browse_all') }}</a>
</div>
@if($similarExercises->isEmpty())
<div class="rounded-2xl border border-dashed border-gray-300 dark:border-gray-700 p-6 text-center text-gray-600 dark:text-gray-300">{{ __('app.exercises.similar_none') }}</div>
@else
@php $badgePalette = ['bg-rose-100 text-rose-700','bg-amber-100 text-amber-700','bg-emerald-100 text-emerald-700','bg-sky-100 text-sky-700','bg-violet-100 text-violet-700']; @endphp
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($similarExercises as $ex)
<a href="{{ route('exercises.show', $ex->slug) }}" class="group rounded-3xl overflow-hidden bg-white dark:bg-gray-800 ring-1 ring-gray-200 dark:ring-gray-700 shadow-sm hover:shadow-xl transition">
<div class="relative aspect-[16/9] overflow-hidden">
<img src="{{ $ex->poster_url ?: ($ex->gif_url ?: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?q=80&w=1200&auto=format&fit=crop') }}" alt="{{ $ex->display_name }}" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105">
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
<div class="absolute bottom-3 left-3 inline-flex items-center rounded-full bg-white/90 px-3 py-1 text-xs font-semibold text-gray-900 shadow">{{ $ex->display_name }}</div>
</div>
<div class="p-4">
<div class="mt-1 flex flex-wrap gap-2">
@foreach($ex->targetMuscles->take(2) as $i => $m)
<span class="inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium {{ $badgePalette[$i % count($badgePalette)] }}">{{ $m->name }}</span>
@endforeach
@if($ex->bodyParts->count())
<span class="inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200">{{ $ex->bodyParts->first()->name }}</span>
@endif
</div>
</div>
</a>
@endforeach
</div>
@endif
</div>
</section>
@endif
</x-app-layout>