Reorganize workout view for desktop and compact session details

This commit is contained in:
Peter Stockings
2026-09-06 17:10:06 +10:00
parent 1aa5d7418b
commit d5e3e1d930
9 changed files with 345 additions and 183 deletions
+16 -14
View File
@@ -2,7 +2,7 @@
<form class="w-full" id="new-set-workout-{{ workout_id }}"
hx-post="{{ url_for('workout.create_topset', person_id=person_id, workout_id=workout_id) }}" hx-swap="beforeend"
hx-target="#new-workout" _="on htmx:afterOnLoad
if #no-workouts add .hidden to #no-workouts end
if detail.xhr.status == 200
set #validation-error-{{ workout_id }}.innerText to ''
add .hidden to #validation-error-{{ workout_id }}
@@ -19,16 +19,16 @@
<div id="validation-error-{{ workout_id }}"
class="hidden text-red-500 text-xs italic mb-4 p-2 bg-red-50 border border-red-200 rounded"></div>
<div class="flex flex-wrap -mx-3 mb-2">
<div class="w-full md:w-[30%] px-2 md:px-3 mb-6 md:mb-0">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" for="grid-state">
<div class="workout-set-fields">
<div class="workout-set-field">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" for="exercise-search">
Exercise
</label>
{{ render_partial('partials/exercise/exercise_select.html', person_id=person_id,
exercise_id=exercise_id, exercise_name=exercise_name) }}
</div>
<div class="w-full md:w-[30%] px-2 md:px-3 mb-6 md:mb-0">
<div class="workout-set-field">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" for="grid-city">
Reps
</label>
@@ -38,9 +38,9 @@
_="on clearNewSetInputs set my.placeholder to ''" {% endif %}>
</div>
<div class="w-full md:w-[30%] px-2 md:px-3 mb-6 md:mb-0">
<div class="workout-set-field">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" for="grid-zip">
Weight
Weight (kg)
</label>
<input
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
@@ -48,14 +48,15 @@
_="on clearNewSetInputs set my.placeholder to ''" {% endif %}>
</div>
<div class="w-full md:w-[10%] px-2 md:px-3 mb-6 md:mb-0">
<div class="workout-set-submit">
<button
class="flex items-center justify-center py-2 px-2 md:px-3 mb-3 text-sm font-medium text-center text-gray-900 bg-cyan-600 hover:bg-cyan-700 rounded-lg border border-gray-300 hover:scale-[1.02] transition-transform mb-6 md:mb-0 mt-0 md:mt-6 w-full"
class="workout-add-button"
type="submit">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" class="w-7 h-7">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" class="w-5 h-5">
<path
d="M12 4a1 1 0 011 1v6h6a1 1 0 110 2h-6v6a1 1 0 11-2 0v-6H5a1 1 0 110-2h6V5a1 1 0 011-1z" />
</svg>
<span>Add set</span>
</button>
</div>
@@ -66,17 +67,18 @@
<div hx-trigger="exerciseSelected from:body"
hx-get="{{ url_for('workout.get_most_recent_topset_for_exercise', person_id=person_id, workout_id=workout_id) }}"
hx-target="#new-set-form-container-{{ workout_id }}" hx-include="[name='exercise_id']">
hx-target="#new-set-form-container-{{ workout_id }}" hx-swap="outerHTML" hx-include="[name='exercise_id']">
</div>
{% if exercise_id %}
<div class="flex items-center justify-center">
<details class="workout-previous-details">
<summary>Previous performance{% if repetitions %}<span>{{ repetitions }} &times; {{ weight }} kg</span>{% endif %}</summary>
<div class="w-full">
<div class="hidden"
hx-get="{{ url_for('get_exercise_progress_for_user', person_id=person_id, exercise_id=exercise_id) }}"
hx-trigger="load" hx-target="this" hx-swap="outerHTML">
hx-trigger="toggle once from:closest details" hx-target="this" hx-swap="outerHTML">
</div>
</div>
</div>
</details>
{% endif %}
</div>
+8 -8
View File
@@ -1,12 +1,12 @@
<tr id="topset-{{ topset_id }}">
<tr class="workout-set-row" id="topset-{{ topset_id }}">
<td class="p-0 sm:p-4 text-sm font-semibold text-gray-900 break-normal">
{% if is_edit|default(false, true) == false %}
<div class="flex items-center space-x-2">
<div class="workout-exercise-name">
<span class="cursor-pointer" hx-get="{{ url_for('tags.goto_tag') }}"
hx-vals='{"filter": "?exercise_id={{ exercise_id }}", "person_id" : "{{ person_id }}" }'
hx-target="#container" hx-swap="innerHTML" hx-push-url="true"
_='on click trigger closeModalWithoutRefresh'>{{ exercise_name }}</span>
<div class="flex flex-col sm:flex-row space-y-1 sm:space-y-0 sm:space-x-1">
<div class="workout-exercise-tools">
<button
class="inline-flex justify-center p-1 text-gray-500 rounded cursor-pointer hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-600"
title="Show Progress Graph"
@@ -17,7 +17,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
</svg>
<span class="sr-only">Show Progress Graph</span>
<span class="workout-tool-label">Progress</span>
</button>
<button
class="inline-flex justify-center p-1 text-gray-500 rounded cursor-pointer hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-600"
@@ -29,7 +29,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
</svg>
<span class="sr-only">Show History</span>
<span class="workout-tool-label">History</span>
</button>
</div>
</div>
@@ -57,11 +57,11 @@
<div class="flex items-center flex-col sm:flex-row">
<input type="number"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-4 sm:pl-10 p-2.5 "
name="repetitions" value="{{ repetitions }}">
aria-label="Repetitions" name="repetitions" value="{{ repetitions }}">
<p class="px-2">x</p>
<input type="number"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-4 sm:pl-10 p-2.5 "
name="weight" value="{{ weight }}">
aria-label="Weight in kilograms" step="any" name="weight" value="{{ weight }}">
<p class="px-2">kg</p>
</div>
{% endif %}
@@ -118,7 +118,7 @@
</td>
</tr>
{# Target row modified for dismissible extra content (graph or history) #}
<tr id="extra-target-{{ topset_id }}">
<tr id="extra-target-{{ topset_id }}" {% if existing_row|default(false) or is_edit|default(false) %}hx-swap-oob="outerHTML"{% endif %}>
<td colspan="3" class="p-0 relative">
<div id="extra-content-{{ topset_id }}" class="extra-content-container" _="
on htmx:afterSwap
+12 -58
View File
@@ -1,64 +1,18 @@
{% set distribution = distribution or muscle_distribution %}
{% set category_name = category_name or 'Muscle Group' %}
{% set distribution = distribution|default([], true) %}
{% set category_name = category_name|default('Muscle Group', true) %}
{% set breakdown_id = category_name.lower().replace(' ', '-') %}
{% if distribution %}
<div class="px-4 py-3 bg-white relative" id="{{ breakdown_id }}-breakdown"
<div class="workout-breakdown" id="{{ breakdown_id }}-breakdown"
hx-get="{{ url_for('workout.get_workout_distribution', person_id=person_id, workout_id=workout_id, category=category_name) }}"
hx-trigger="topsetAdded from:body" hx-swap="outerHTML">
<!-- Shared Popover Container (managed by Hyperscript) -->
<div id="popover-{{ breakdown_id }}"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-30 hidden bg-white border border-gray-200 shadow-2xl rounded-xl p-4 min-w-[200px] animate-in fade-in zoom-in duration-200"
_="on click from elsewhere add .hidden to me">
<div id="content-{{ breakdown_id }}"></div>
<button class="absolute top-2 right-2 text-gray-400 hover:text-gray-600"
_="on click add .hidden to #popover-{{ breakdown_id }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
hx-trigger="topsetAdded from:body, topsetChanged from:body" hx-swap="outerHTML">
<h3>{{ category_name }}</h3>
{% if distribution %}
<div class="workout-distribution-bar" aria-hidden="true">
{% for item in distribution %}<span style="width: {{ item.percentage }}%; background-color: {{ item.color }};"></span>{% endfor %}
</div>
<div class="flex items-center justify-between mb-2">
<h4 class="text-[9px] font-black text-gray-400 uppercase tracking-[0.25em]">{{ category_name }} Distribution
</h4>
</div>
<!-- Sleek Performance Bar -->
<div class="w-full h-8 flex overflow-hidden rounded-lg bg-gray-100 shadow-inner p-0.5 cursor-pointer">
<ul class="workout-distribution-legend">
{% for item in distribution %}
<div class="h-full transition-all duration-700 ease-in-out flex items-center justify-center relative group first:rounded-l-md last:rounded-r-md"
style="width: {{ item.percentage }}%; background-color: {{ item.color }};" _="on click
halt the event
put '<div class=\'flex flex-col gap-1\'>
<div class=\'flex items-center gap-2\'>
<div class=\'w-3 h-3 rounded-full\' style=\'background-color: {{ item.color }}\'></div>
<span class=\'font-black text-sm uppercase\'>{{ item.attribute_name }}</span>
</div>
<div class=\'text-2xl font-black text-gray-900\'>{{ item.percentage }}%</div>
<div class=\'text-[10px] font-bold text-gray-400 uppercase tracking-wider\'>{{ item.count }} Sets Targeted</div>
</div>' into #content-{{ breakdown_id }}
remove .hidden from #popover-{{ breakdown_id }}"
title="{{ item.attribute_name }}: {{ item.percentage }}%">
<!-- Labels (Name & Percentage grouped and centered) -->
<div
class="flex items-center justify-center gap-1.5 leading-none text-white pointer-events-none px-1 overflow-hidden">
{% if item.percentage > 18 %}
<span class="text-[9px] font-black uppercase tracking-tighter truncate drop-shadow-sm">{{
item.attribute_name }}</span>
<span class="text-[9px] font-bold opacity-90 whitespace-nowrap drop-shadow-sm">{{ item.percentage
}}%</span>
{% elif item.percentage > 8 %}
<span class="text-[9px] font-black drop-shadow-sm">{{ item.percentage }}%</span>
{% endif %}
</div>
<!-- Enhanced Hover State -->
<div class="absolute inset-0 bg-white/15 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</div>
<li><span class="workout-legend-label"><i style="background-color: {{ item.color }};" aria-hidden="true"></i>{{ item.attribute_name }}</span><span title="{{ item.count }} sets targeted">{{ item.percentage }}%</span></li>
{% endfor %}
</div>
</ul>
{% else %}<p class="workout-subtle">No categorized sets yet.</p>{% endif %}
</div>
{% endif %}
+14
View File
@@ -0,0 +1,14 @@
{% set totals = namespace(reps=0) %}
{% for top_set in top_sets %}{% set totals.reps = totals.reps + (top_set.repetitions or 0)|int %}{% endfor %}
<section class="workout-card workout-summary" id="workout-summary"
aria-label="Session summary" aria-live="polite"
hx-get="{{ url_for('workout.get_workout_summary', person_id=person_id, workout_id=workout_id) }}"
hx-trigger="topsetAdded from:body, topsetChanged from:body" hx-swap="outerHTML">
<p class="workout-eyebrow">This session</p>
<dl>
<div><dt>Exercises</dt><dd>{{ top_sets|map(attribute='exercise_id')|unique|list|length }}</dd></div>
<div><dt>Top sets</dt><dd>{{ top_sets|length }}</dd></div>
<div><dt>Reps</dt><dd>{{ totals.reps }}</dd></div>
</dl>
<p class="workout-subtle">Based on your logged top sets.</p>
</section>