2025-10-04 17:55:30 +03:00
< 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
2025-10-06 22:35:04 +03:00
< 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' )
2026-08-10 12:15:14 +03:00
< input type = "hidden" name = "actual_weight" class = "actual-weight-input" value = "{{ old('actual_weight', $si->actual_weight ?? $item->weight ) }}" >
< input type = "hidden" name = "actual_reps" class = "actual-reps-input" value = "{{ old('actual_reps', $si->actual_reps ?? $item->reps ) }}" >
2025-10-06 22:35:04 +03:00
< 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 >
2026-08-10 12:15:14 +03:00
< div class = "mt-2 flex items-center gap-3 text-sm" >
< label class = "flex items-center gap-1.5 text-gray-600 dark:text-gray-300" >
{{ __ ( 'app.sessions.weight_lifted' ) }}
< input type = "number" step = "0.5" min = "0" class = "actual-weight-field w-20 rounded-lg border-gray-300 dark:border-gray-700 bg-white/80 dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-2 py-1" value = "{{ $si->actual_weight ?? $item->weight }}" >
kg
</ label >
< label class = "flex items-center gap-1.5 text-gray-600 dark:text-gray-300" >
{{ __ ( 'app.sessions.reps_done' ) }}
< input type = "number" min = "0" class = "actual-reps-field w-16 rounded-lg border-gray-300 dark:border-gray-700 bg-white/80 dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-2 py-1" value = "{{ $si->actual_reps ?? $item->reps }}" >
</ label >
</ div >
2025-10-06 22:35:04 +03:00
< 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 >
2025-10-04 17:55:30 +03:00
</ div >
</ 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 );
2026-08-10 12:15:14 +03:00
// Sync the visible weight/reps fields (outside the form) into the hidden form inputs
const row = btn . closest ( 'li' );
if ( row ) {
const weightField = row . querySelector ( '.actual-weight-field' );
const repsField = row . querySelector ( '.actual-reps-field' );
const weightInput = form . querySelector ( '.actual-weight-input' );
const repsInput = form . querySelector ( '.actual-reps-input' );
if ( weightField && weightInput ) weightInput . value = weightField . value ;
if ( repsField && repsInput ) repsInput . value = repsField . value ;
}
2025-10-04 17:55:30 +03:00
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.' );
});
});
2025-10-06 22:35:04 +03:00
// --- 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 );
});
2025-10-04 17:55:30 +03:00
})();
</ script >
@ endpush
</ x - app - layout >