added yoga stuff, i18n, workout tracking and a dashboard to track progress

This commit is contained in:
2025-10-04 17:55:30 +03:00
parent 1ed9235451
commit d1bc362fab
32 changed files with 1790 additions and 63 deletions
+7 -7
View File
@@ -3,10 +3,10 @@
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">My Workouts</h1>
<p class="mt-2 text-gray-600 dark:text-gray-300">Create routines and manage your exercises.</p>
<h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">{{ __('app.workouts.my_workouts') }}</h1>
<p class="mt-2 text-gray-600 dark:text-gray-300">{{ __('app.workouts.manage_desc') }}</p>
</div>
<a href="{{ route('workouts.create') }}" 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">New Workout</a>
<a href="{{ route('workouts.create') }}" 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">{{ __('app.workouts.new_workout') }}</a>
</div>
</div>
</section>
@@ -16,7 +16,7 @@
<div class="mb-4 rounded-xl bg-emerald-50 text-emerald-700 px-4 py-3">{{ session('status') }}</div>
@endif
@if($workouts->count() === 0)
<div class="rounded-2xl border border-dashed border-gray-300 dark:border-gray-700 p-8 text-center text-gray-600 dark:text-gray-300">No workouts yet. Create your first workout.</div>
<div class="rounded-2xl border border-dashed border-gray-300 dark:border-gray-700 p-8 text-center text-gray-600 dark:text-gray-300">{{ __('app.workouts.none_yet') }}</div>
@else
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($workouts as $workout)
@@ -31,11 +31,11 @@
@endif
</div>
<div class="mt-4 flex items-center justify-between">
<a href="{{ route('workouts.show', $workout) }}" class="text-sm font-semibold text-indigo-600 hover:text-indigo-500">Open</a>
<form method="POST" action="{{ route('workouts.destroy', $workout) }}" onsubmit="return confirm('Delete this workout?');">
<a href="{{ route('workouts.show', $workout) }}" class="text-sm font-semibold text-indigo-600 hover:text-indigo-500">{{ __('app.workouts.open') }}</a>
<form method="POST" action="{{ route('workouts.destroy', $workout) }}" onsubmit="return confirm('{{ __('app.workouts.confirm_delete') }}');">
@csrf
@method('DELETE')
<button type="submit" class="text-sm text-red-600 hover:text-red-500">Delete</button>
<button type="submit" class="text-sm text-red-600 hover:text-red-500">{{ __('app.workouts.delete') }}</button>
</form>
</div>
</div>
+125
View File
@@ -0,0 +1,125 @@
<x-app-layout>
<section class="bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
<div class="flex items-center justify-between">
<div>
<a href="{{ route('workouts.show', $workout) }}" class="text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">← {{ __('app.sessions.back_to_workout') }}</a>
<h1 class="mt-2 text-2xl sm:text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">{{ $workout->title }}</h1>
<p class="mt-1 text-gray-600 dark:text-gray-300">{{ __('app.sessions.progress') }}: {{ $doneByItemId->where('done', true)->count() }} / {{ $workout->items->count() }}</p>
</div>
<form method="POST" action="{{ route('sessions.complete', $session) }}" onsubmit="return confirm('{{ __('app.sessions.mark_complete_confirm') }}');">
@csrf
<button type="submit" class="inline-flex items-center rounded-xl bg-gradient-to-r from-indigo-600 to-fuchsia-600 px-4 py-2 text-sm font-semibold text-white shadow hover:from-indigo-500 hover:to-fuchsia-500" title="{{ __('app.sessions.mark_complete') }}" aria-label="{{ __('app.sessions.mark_complete') }}">{{ __('app.sessions.mark_complete') }}</button>
</form>
</div>
</div>
</section>
<section class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
<div class="rounded-2xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 p-5">
@if($workout->items->count() === 0)
<div class="mt-3 rounded-xl border border-dashed border-gray-300 dark:border-gray-700 p-6 text-sm text-gray-600 dark:text-gray-300">{{ __('app.sessions.no_items') }}</div>
@else
<ul class="mt-1 space-y-3">
@foreach($workout->items as $item)
@php $si = $doneByItemId->get($item->id); $isDone = $si && $si->done; @endphp
<li class="rounded-xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-900 p-4 flex items-start justify-between">
<div class="flex items-start gap-3">
<form method="POST" action="{{ route('sessions.items.toggle', [$session, $item]) }}" class="session-toggle-form" data-item-id="{{ $item->id }}" data-initial-done="{{ $isDone ? '1' : '0' }}">
@csrf
@method('PATCH')
<button type="submit" class="h-6 w-6 rounded-md border flex items-center justify-center transition-colors duration-150"
aria-pressed="{{ $isDone ? 'true' : 'false' }}"
title="{{ $isDone ? __('app.sessions.mark_complete') : __('app.sessions.progress') }}">
<span class="sr-only">Toggle</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4 {{ $isDone ? 'text-white' : 'text-transparent' }}">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-7.5 7.5a1 1 0 01-1.414 0l-3-3a1 1 0 111.414-1.414L8.5 12.086l6.793-6.793a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</button>
</form>
<div>
<div class="exercise-title text-base font-semibold {{ $isDone ? 'line-through text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-white' }}">{{ $item->exercise->name }}</div>
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
@if($item->sets) {{ $item->sets }} {{ __('app.sessions.sets') }} @endif
@if($item->reps) · {{ $item->reps }} {{ __('app.sessions.reps') }} @endif
@if($item->weight) · {{ $item->weight }} kg @endif
@if($item->rest_seconds) · {{ $item->rest_seconds }}s {{ __('app.sessions.rest') }} @endif
</div>
@if($item->notes)
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">{{ $item->notes }}</div>
@endif
</div>
</div>
</li>
@endforeach
</ul>
@endif
</div>
</section>
@push('scripts')
<script>
(function(){
function updateBtnState(btn, done) {
btn.setAttribute('aria-pressed', done ? 'true' : 'false');
// Background/border colors
if (done) {
btn.classList.add('bg-emerald-500','border-emerald-600','text-white');
btn.classList.remove('border-gray-300','dark:border-gray-700');
} else {
btn.classList.remove('bg-emerald-500','border-emerald-600','text-white');
btn.classList.add('border-gray-300','dark:border-gray-700');
}
const icon = btn.querySelector('svg');
if (icon) {
icon.classList.toggle('text-white', !!done);
icon.classList.toggle('text-transparent', !done);
}
// Also toggle line-through for exercise name in the same item row
const row = btn.closest('li');
if (row) {
const titleEl = row.querySelector('.exercise-title');
if (titleEl) {
if (done) {
titleEl.classList.add('line-through','text-gray-500','dark:text-gray-400');
titleEl.classList.remove('text-gray-900','dark:text-white');
} else {
titleEl.classList.remove('line-through','text-gray-500','dark:text-gray-400');
titleEl.classList.add('text-gray-900','dark:text-white');
}
}
}
}
document.addEventListener('click', function(e){
const btn = e.target.closest('.session-toggle-form button[type="submit"]');
if (!btn) return;
const form = btn.closest('form.session-toggle-form');
if (!form) return;
e.preventDefault();
// Optimistically toggle UI
const wasDone = btn.getAttribute('aria-pressed') === 'true';
const next = !wasDone;
updateBtnState(btn, next);
fetch(form.action, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': form.querySelector('input[name="_token"]').value,
'Accept': 'text/html',
},
body: new URLSearchParams(new FormData(form))
}).then(function(res){
if (!res.ok) throw new Error('Network response was not ok');
}).catch(function(){
// Revert on error
updateBtnState(btn, wasDone);
alert('Failed to update. Please try again.');
});
});
})();
</script>
@endpush
</x-app-layout>
+13 -5
View File
@@ -25,11 +25,19 @@
</div>
<p class="mt-1 text-gray-600 dark:text-gray-300">@if($workout->estimated_minutes) ~ {{ $workout->estimated_minutes }} min · @endif {{ $workout->items->count() }} exercises</p>
</div>
<form method="POST" action="{{ route('workouts.destroy', $workout) }}" onsubmit="return confirm('Delete this workout?');">
@csrf
@method('DELETE')
<button type="submit" class="text-sm text-red-600 hover:text-red-500">Delete</button>
</form>
<div class="flex items-center gap-3">
@if(Auth::id() === $workout->user_id)
<form method="POST" action="{{ route('sessions.store', $workout) }}">
@csrf
<button type="submit" class="inline-flex items-center rounded-xl bg-gradient-to-r from-emerald-600 to-teal-600 px-3 py-2 text-sm font-semibold text-white shadow hover:from-emerald-500 hover:to-teal-500" title="{{ __('app.sessions.start') }}" aria-label="{{ __('app.sessions.start') }}">{{ __('app.sessions.start') }}</button>
</form>
<form method="POST" action="{{ route('workouts.destroy', $workout) }}" onsubmit="return confirm('{{ __('app.workouts.confirm_delete') }}');">
@csrf
@method('DELETE')
<button type="submit" class="text-sm text-red-600 hover:text-red-500">{{ __('app.workouts.delete') }}</button>
</form>
@endif
</div>
</div>
</div>
</section>