some progress

This commit is contained in:
2025-10-06 22:35:04 +03:00
parent 07148700a5
commit beeda8139b
25 changed files with 770 additions and 32 deletions
+116 -24
View File
@@ -23,31 +23,58 @@
<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->display_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
<li class="rounded-xl ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-900 p-4" data-session-id="{{ $session->id }}" data-item-id="{{ $item->id }}" data-rest-seconds="{{ $item->rest_seconds ?? 60 }}">
<div class="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->display_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
<!-- Timers -->
<div class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- Exercise timer (count-up) -->
<div class="rounded-lg bg-gray-50 dark:bg-gray-800/60 ring-1 ring-gray-200 dark:ring-gray-700 p-3 flex items-center justify-between">
<div>
<div class="text-xs text-gray-500">{{ __('app.sessions.exercise_timer') }}</div>
<div class="exercise-timer-display text-sm font-semibold text-gray-900 dark:text-white">00:00</div>
</div>
<button type="button" class="exercise-timer-toggle inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold text-white bg-emerald-600 hover:bg-emerald-500">{{ __('app.sessions.start_timer') }}</button>
</div>
<!-- Rest timer (countdown) -->
<div class="rounded-lg bg-gray-50 dark:bg-gray-800/60 ring-1 ring-gray-200 dark:ring-gray-700 p-3 flex items-center justify-between">
<div>
<div class="text-xs text-gray-500">{{ __('app.sessions.rest_timer') }}</div>
<div class="rest-timer-display text-sm font-semibold text-gray-900 dark:text-white">00:00</div>
</div>
<div class="flex items-center gap-2">
<button type="button" class="rest-start inline-flex items-center rounded-md px-2.5 py-1.5 text-xs font-semibold text-white bg-indigo-600 hover:bg-indigo-500">{{ __('app.sessions.start_rest') }}</button>
<button type="button" class="rest-pause inline-flex items-center rounded-md px-2.5 py-1.5 text-xs font-semibold text-gray-700 dark:text-gray-200 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600">{{ __('app.sessions.pause_rest') }}</button>
<button type="button" class="rest-reset inline-flex items-center rounded-md px-2.5 py-1.5 text-xs font-semibold text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700">{{ __('app.sessions.reset_rest') }}</button>
</div>
</div>
</div>
</div>
@if($item->notes)
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">{{ $item->notes }}</div>
@endif
</div>
</div>
</li>
@@ -119,6 +146,71 @@
alert('Failed to update. Please try again.');
});
});
// --- Timers ---
function pad(n){ return n < 10 ? '0'+n : ''+n; }
function fmt(ms){ ms = Math.max(0, ms|0); const s = Math.floor(ms/1000); const m = Math.floor(s/60); const rs = s%60; return pad(m)+':'+pad(rs); }
function storageGet(key){ try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch(e){ return null; } }
function storageSet(key, val){ try { localStorage.setItem(key, JSON.stringify(val)); } catch(e){} }
const intervals = new Map(); // key -> interval id
function setupExerciseTimer(row){
const sessionId = row.getAttribute('data-session-id');
const itemId = row.getAttribute('data-item-id');
const key = `sess:${sessionId}:item:${itemId}:ex`;
const display = row.querySelector('.exercise-timer-display');
const toggle = row.querySelector('.exercise-timer-toggle');
if (!display || !toggle) return;
let state = storageGet(key) || { running:false, elapsedMs:0, startedAt: null };
function render(){ display.textContent = fmt(state.running ? (Date.now() - (state.startedAt||Date.now()) + (state.elapsedMs||0)) : (state.elapsedMs||0)); toggle.textContent = state.running ? (toggle.getAttribute('data-stop-label') || '{{ __('app.sessions.stop_timer') }}') : (toggle.getAttribute('data-start-label') || '{{ __('app.sessions.start_timer') }}'); }
function start(){ if (state.running) return; state.running = true; state.startedAt = Date.now(); storageSet(key, state); render(); const iid = setInterval(render, 500); intervals.set(key, iid); }
function stop(){ if (!state.running) return; state.elapsedMs = (Date.now() - (state.startedAt||Date.now())) + (state.elapsedMs||0); state.running = false; state.startedAt = null; storageSet(key, state); render(); const iid = intervals.get(key); if (iid) { clearInterval(iid); intervals.delete(key); } }
toggle.addEventListener('click', function(){ state.running ? stop() : start(); });
// resume
if (state.running && state.startedAt){ const iid = setInterval(render, 500); intervals.set(key, iid); }
render();
}
function setupRestTimer(row){
const sessionId = row.getAttribute('data-session-id');
const itemId = row.getAttribute('data-item-id');
const defSeconds = parseInt(row.getAttribute('data-rest-seconds')||'60', 10);
const key = `sess:${sessionId}:item:${itemId}:rest`;
const display = row.querySelector('.rest-timer-display');
const btnStart = row.querySelector('.rest-start');
const btnPause = row.querySelector('.rest-pause');
const btnReset = row.querySelector('.rest-reset');
if (!display || !btnStart || !btnPause || !btnReset) return;
let state = storageGet(key) || { running:false, remainingMs: defSeconds*1000, targetAt: null, defaultMs: defSeconds*1000 };
function render(){ const ms = state.running ? Math.max(0, (state.targetAt||Date.now()) - Date.now()) : (state.remainingMs||0); display.textContent = fmt(ms); if (ms <= 0 && state.running){ // auto-stop when finished
state.running = false; state.remainingMs = 0; state.targetAt = null; storageSet(key, state); const iid = intervals.get(key); if (iid) { clearInterval(iid); intervals.delete(key); } try{ display.classList.add('text-emerald-600'); setTimeout(()=>display.classList.remove('text-emerald-600'), 2000);}catch(e){}
} }
function start(){ if (state.running) return; const ms = (state.remainingMs==null ? state.defaultMs : state.remainingMs); state.running = true; state.targetAt = Date.now() + Math.max(0, ms); storageSet(key, state); render(); const iid = setInterval(render, 500); intervals.set(key, iid); }
function pause(){ if (!state.running) return; state.remainingMs = Math.max(0, (state.targetAt||Date.now()) - Date.now()); state.running = false; state.targetAt = null; storageSet(key, state); render(); const iid = intervals.get(key); if (iid) { clearInterval(iid); intervals.delete(key); } }
function reset(){ state.running = false; state.remainingMs = state.defaultMs; state.targetAt = null; storageSet(key, state); render(); const iid = intervals.get(key); if (iid) { clearInterval(iid); intervals.delete(key); } }
btnStart.addEventListener('click', start);
btnPause.addEventListener('click', pause);
btnReset.addEventListener('click', reset);
// resume
if (state.running && state.targetAt){ const iid = setInterval(render, 500); intervals.set(key, iid); }
render();
}
document.querySelectorAll('li[data-session-id][data-item-id]').forEach(function(row){
setupExerciseTimer(row);
setupRestTimer(row);
});
})();
</script>
@endpush