Update programs functionality
This commit is contained in:
@@ -96,7 +96,7 @@
|
||||
|
||||
{# Nested Template for a single exercise row within a session #}
|
||||
<template id="exercise-row-template">
|
||||
<div class="exercise-row flex items-center space-x-2">
|
||||
<div class="exercise-row flex items-center space-x-4 bg-white p-2 rounded border border-gray-100 shadow-sm">
|
||||
<div class="flex-grow relative">
|
||||
{{ render_partial('partials/custom_select.html',
|
||||
name='exercises_SESSION_INDEX_PLACEHOLDER',
|
||||
@@ -106,6 +106,16 @@
|
||||
placeholder='Select Exercise...')
|
||||
}}
|
||||
</div>
|
||||
<div class="w-16">
|
||||
<input type="number" name="sets_SESSION_INDEX_PLACEHOLDER" placeholder="Sets"
|
||||
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
min="1" value="3">
|
||||
</div>
|
||||
<div class="w-24">
|
||||
<input type="text" name="reps_SESSION_INDEX_PLACEHOLDER" placeholder="Reps (e.g. 8-10)"
|
||||
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
value="8-10">
|
||||
</div>
|
||||
<button type="button" class="remove-exercise-btn text-red-500 hover:text-red-700 flex-shrink-0"
|
||||
title="Remove Exercise">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
@@ -180,10 +190,19 @@
|
||||
function addExerciseSelect(container, sessionIndex) {
|
||||
const newExFragment = exerciseTemplate.content.cloneNode(true);
|
||||
const nativeSelect = newExFragment.querySelector('.native-select');
|
||||
const setsInput = newExFragment.querySelector('input[name^="sets_"]');
|
||||
const repsInput = newExFragment.querySelector('input[name^="reps_"]');
|
||||
const removeBtn = newExFragment.querySelector('.remove-exercise-btn');
|
||||
|
||||
if (nativeSelect) {
|
||||
nativeSelect.name = `exercises_${sessionIndex}`;
|
||||
}
|
||||
if (setsInput) {
|
||||
setsInput.name = `sets_${sessionIndex}`;
|
||||
}
|
||||
if (repsInput) {
|
||||
repsInput.name = `reps_${sessionIndex}`;
|
||||
}
|
||||
|
||||
container.appendChild(newExFragment);
|
||||
|
||||
@@ -251,12 +270,22 @@
|
||||
nameInput.name = `session_name_${newIndex}`;
|
||||
}
|
||||
|
||||
// Update names for the exercise selects within this session
|
||||
const exerciseSelects = row.querySelectorAll('.native-select'); // Target hidden selects
|
||||
// Update names for the exercise selects and metadata within this session
|
||||
const exerciseSelects = row.querySelectorAll('.native-select');
|
||||
exerciseSelects.forEach(select => {
|
||||
select.name = `exercises_${newIndex}`;
|
||||
});
|
||||
|
||||
const setsInputs = row.querySelectorAll('input[name^="sets_"]');
|
||||
setsInputs.forEach(input => {
|
||||
input.name = `sets_${newIndex}`;
|
||||
});
|
||||
|
||||
const repsInputs = row.querySelectorAll('input[name^="reps_"]');
|
||||
repsInputs.forEach(input => {
|
||||
input.name = `reps_${newIndex}`;
|
||||
});
|
||||
|
||||
// Update listener for the "Add Exercise" button
|
||||
const addExerciseBtn = row.querySelector('.add-exercise-btn');
|
||||
if (addExerciseBtn) {
|
||||
|
||||
334
templates/program_edit.html
Normal file
334
templates/program_edit.html
Normal file
@@ -0,0 +1,334 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit {{ program.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8 max-w-3xl">
|
||||
<h1 class="text-3xl font-bold mb-8 text-center text-gray-800">Edit Workout Program</h1>
|
||||
|
||||
<form method="POST" action="{{ url_for('programs.edit_program', program_id=program.program_id) }}"
|
||||
id="edit-program-form" class="bg-white shadow-md rounded-lg px-8 pt-6 pb-8 mb-4">
|
||||
{# Program Details Section #}
|
||||
<div class="mb-6 border-b border-gray-200 pb-4">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-700">Program Details</h2>
|
||||
<div class="mb-4">
|
||||
<label for="program_name" class="block text-gray-700 text-sm font-bold mb-2">Program Name:</label>
|
||||
<input type="text" id="program_name" name="program_name" required value="{{ program.name }}"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-gray-700 text-sm font-bold mb-2">Description
|
||||
(Optional):</label>
|
||||
<textarea id="description" name="description" rows="3"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">{{ program.description or '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Sessions Section #}
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-700">Sessions</h2>
|
||||
<div id="sessions-container" class="space-y-6 mb-4">
|
||||
{% for session in sessions %}
|
||||
{% set session_index = loop.index0 %}
|
||||
<div class="session-row bg-gray-50 border border-gray-300 rounded-lg shadow-sm overflow-hidden"
|
||||
data-index="{{ session_index }}">
|
||||
{# Session Header #}
|
||||
<div class="px-4 py-3 bg-gray-100 border-b border-gray-300 flex justify-between items-center">
|
||||
<h3 class="session-day-number text-lg font-semibold text-gray-700">Day {{ session.session_order
|
||||
}}</h3>
|
||||
<input type="hidden" name="session_order_{{ session_index }}"
|
||||
value="{{ session.session_order }}">
|
||||
<button type="button" class="remove-session-btn text-red-500 hover:text-red-700"
|
||||
title="Remove Session">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Session Body #}
|
||||
<div class="p-4 space-y-4">
|
||||
<div>
|
||||
<label for="session_name_{{ session_index }}"
|
||||
class="block text-sm font-medium text-gray-700 mb-1">Session Name (Optional):</label>
|
||||
<input type="text" id="session_name_{{ session_index }}"
|
||||
name="session_name_{{ session_index }}" value="{{ session.session_name or '' }}"
|
||||
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">
|
||||
</div>
|
||||
{# Container for individual exercise selects #}
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Exercises:</label>
|
||||
<div
|
||||
class="session-exercises-container space-y-2 border border-gray-200 p-3 rounded-md bg-white">
|
||||
{% for exercise in session.exercises %}
|
||||
<div
|
||||
class="exercise-row flex items-center space-x-4 bg-white p-2 rounded border border-gray-100 shadow-sm">
|
||||
<div class="flex-grow relative">
|
||||
{{ render_partial('partials/custom_select.html',
|
||||
name='exercises_' ~ session_index,
|
||||
options=exercises,
|
||||
multiple=false,
|
||||
search=true,
|
||||
selected_values=[exercise.exercise_id],
|
||||
placeholder='Select Exercise...')
|
||||
}}
|
||||
</div>
|
||||
<div class="w-16">
|
||||
<input type="number" name="sets_{{ session_index }}" placeholder="Sets"
|
||||
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
min="1" value="{{ exercise.sets or 3 }}">
|
||||
</div>
|
||||
<div class="w-24">
|
||||
<input type="text" name="reps_{{ session_index }}"
|
||||
placeholder="Reps (e.g. 8-10)"
|
||||
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
value="{{ exercise.rep_range or '8-10' }}">
|
||||
</div>
|
||||
<button type="button"
|
||||
class="remove-exercise-btn text-red-500 hover:text-red-700 flex-shrink-0"
|
||||
title="Remove Exercise">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM7 9a1 1 0 000 2h6a1 1 0 100-2H7z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" data-session-index="{{ session_index }}"
|
||||
class="add-exercise-btn mt-1 inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Add Exercise to Session
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" id="add-session-btn"
|
||||
class="mt-2 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
Add Session
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Form Actions #}
|
||||
<div class="flex items-center justify-between pt-4 border-t border-gray-200">
|
||||
<a href="{{ url_for('programs.view_program', program_id=program.program_id) }}"
|
||||
hx-get="{{ url_for('programs.view_program', program_id=program.program_id) }}" hx-target="#container"
|
||||
hx-push-url="true" class="text-gray-600 hover:text-gray-900 font-medium">Cancel</a>
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# HTML Template for a single session row #}
|
||||
<template id="session-row-template">
|
||||
<div class="session-row bg-gray-50 border border-gray-300 rounded-lg shadow-sm overflow-hidden"
|
||||
data-index="SESSION_INDEX_PLACEHOLDER">
|
||||
{# Session Header #}
|
||||
<div class="px-4 py-3 bg-gray-100 border-b border-gray-300 flex justify-between items-center">
|
||||
<h3 class="session-day-number text-lg font-semibold text-gray-700">Day SESSION_DAY_NUMBER_PLACEHOLDER
|
||||
</h3>
|
||||
<input type="hidden" name="session_order_SESSION_INDEX_PLACEHOLDER"
|
||||
value="SESSION_DAY_NUMBER_PLACEHOLDER">
|
||||
<button type="button" class="remove-session-btn text-red-500 hover:text-red-700" title="Remove Session">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Session Body #}
|
||||
<div class="p-4 space-y-4">
|
||||
<div>
|
||||
<label for="session_name_SESSION_INDEX_PLACEHOLDER"
|
||||
class="block text-sm font-medium text-gray-700 mb-1">Session Name (Optional):</label>
|
||||
<input type="text" id="session_name_SESSION_INDEX_PLACEHOLDER"
|
||||
name="session_name_SESSION_INDEX_PLACEHOLDER" value=""
|
||||
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">
|
||||
</div>
|
||||
{# Container for individual exercise selects #}
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Exercises:</label>
|
||||
<div class="session-exercises-container space-y-2 border border-gray-200 p-3 rounded-md bg-white">
|
||||
{# Exercise rows will be added here by JS #}
|
||||
</div>
|
||||
<button type="button"
|
||||
class="add-exercise-btn mt-1 inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Add Exercise to Session
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{# Nested Template for a single exercise row within a session #}
|
||||
<template id="exercise-row-template">
|
||||
<div class="exercise-row flex items-center space-x-4 bg-white p-2 rounded border border-gray-100 shadow-sm">
|
||||
<div class="flex-grow relative">
|
||||
{{ render_partial('partials/custom_select.html',
|
||||
name='exercises_SESSION_INDEX_PLACEHOLDER',
|
||||
options=exercises,
|
||||
multiple=false,
|
||||
search=true,
|
||||
placeholder='Select Exercise...')
|
||||
}}
|
||||
</div>
|
||||
<div class="w-16">
|
||||
<input type="number" name="sets_SESSION_INDEX_PLACEHOLDER" placeholder="Sets"
|
||||
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
min="1" value="3">
|
||||
</div>
|
||||
<div class="w-24">
|
||||
<input type="text" name="reps_SESSION_INDEX_PLACEHOLDER" placeholder="Reps (e.g. 8-10)"
|
||||
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
value="8-10">
|
||||
</div>
|
||||
<button type="button" class="remove-exercise-btn text-red-500 hover:text-red-700 flex-shrink-0"
|
||||
title="Remove Exercise">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM7 9a1 1 0 000 2h6a1 1 0 100-2H7z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const sessionsContainer = document.getElementById('sessions-container');
|
||||
const addSessionBtn = document.getElementById('add-session-btn');
|
||||
const sessionTemplate = document.getElementById('session-row-template');
|
||||
const exerciseTemplate = document.getElementById('exercise-row-template');
|
||||
let sessionCounter = sessionsContainer.querySelectorAll('.session-row').length;
|
||||
|
||||
// --- Function to add a new session row ---
|
||||
function addSessionRow() {
|
||||
const newRowFragment = sessionTemplate.content.cloneNode(true);
|
||||
const newRow = newRowFragment.querySelector('.session-row');
|
||||
const currentSessionIndex = sessionCounter;
|
||||
|
||||
if (!newRow) return;
|
||||
|
||||
newRow.dataset.index = currentSessionIndex;
|
||||
const dayNumberSpan = newRow.querySelector('.session-day-number');
|
||||
if (dayNumberSpan) dayNumberSpan.textContent = `Day ${currentSessionIndex + 1}`;
|
||||
|
||||
const orderInput = newRow.querySelector('input[type="hidden"]');
|
||||
if (orderInput) {
|
||||
orderInput.name = `session_order_${currentSessionIndex}`;
|
||||
orderInput.value = currentSessionIndex + 1;
|
||||
}
|
||||
|
||||
const nameInput = newRow.querySelector('input[id^="session_name_"]');
|
||||
if (nameInput) {
|
||||
nameInput.id = `session_name_${currentSessionIndex}`;
|
||||
nameInput.name = `session_name_${currentSessionIndex}`;
|
||||
}
|
||||
|
||||
const addExerciseBtn = newRow.querySelector('.add-exercise-btn');
|
||||
const exercisesContainer = newRow.querySelector('.session-exercises-container');
|
||||
if (addExerciseBtn && exercisesContainer) {
|
||||
addExerciseBtn.dataset.sessionIndex = currentSessionIndex;
|
||||
addExerciseBtn.addEventListener('click', handleAddExerciseClick);
|
||||
addExerciseSelect(exercisesContainer, currentSessionIndex);
|
||||
}
|
||||
|
||||
sessionsContainer.appendChild(newRowFragment);
|
||||
attachRemoveListener(newRow.querySelector('.remove-session-btn'));
|
||||
sessionCounter++;
|
||||
}
|
||||
|
||||
function addExerciseSelect(container, sessionIndex) {
|
||||
const newExFragment = exerciseTemplate.content.cloneNode(true);
|
||||
const nativeSelect = newExFragment.querySelector('.native-select');
|
||||
const setsInput = newExFragment.querySelector('input[name^="sets_"]');
|
||||
const repsInput = newExFragment.querySelector('input[name^="reps_"]');
|
||||
const removeBtn = newExFragment.querySelector('.remove-exercise-btn');
|
||||
|
||||
if (nativeSelect) nativeSelect.name = `exercises_${sessionIndex}`;
|
||||
if (setsInput) setsInput.name = `sets_${sessionIndex}`;
|
||||
if (repsInput) repsInput.name = `reps_${sessionIndex}`;
|
||||
|
||||
container.appendChild(newExFragment);
|
||||
attachExerciseRemoveListener(removeBtn);
|
||||
}
|
||||
|
||||
function handleAddExerciseClick(event) {
|
||||
const btn = event.currentTarget;
|
||||
const sessionIndex = parseInt(btn.dataset.sessionIndex, 10);
|
||||
const exercisesContainer = btn.closest('.session-row').querySelector('.session-exercises-container');
|
||||
if (!isNaN(sessionIndex) && exercisesContainer) {
|
||||
addExerciseSelect(exercisesContainer, sessionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function attachRemoveListener(button) {
|
||||
button.addEventListener('click', function () {
|
||||
this.closest('.session-row').remove();
|
||||
updateSessionNumbers();
|
||||
});
|
||||
}
|
||||
|
||||
function attachExerciseRemoveListener(button) {
|
||||
if (button) {
|
||||
button.addEventListener('click', function () {
|
||||
this.closest('.exercise-row').remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateSessionNumbers() {
|
||||
const rows = sessionsContainer.querySelectorAll('.session-row');
|
||||
rows.forEach((row, index) => {
|
||||
const newIndex = index;
|
||||
const daySpan = row.querySelector('.session-day-number');
|
||||
if (daySpan) daySpan.textContent = `Day ${newIndex + 1}`;
|
||||
|
||||
const orderInput = row.querySelector('input[type="hidden"]');
|
||||
if (orderInput) {
|
||||
orderInput.name = `session_order_${newIndex}`;
|
||||
orderInput.value = newIndex + 1;
|
||||
}
|
||||
|
||||
const nameInput = row.querySelector('input[id^="session_name_"]');
|
||||
if (nameInput) {
|
||||
nameInput.id = `session_name_${newIndex}`;
|
||||
nameInput.name = `session_name_${newIndex}`;
|
||||
}
|
||||
|
||||
row.querySelectorAll('.native-select').forEach(s => s.name = `exercises_${newIndex}`);
|
||||
row.querySelectorAll('input[name^="sets_"]').forEach(i => i.name = `sets_${newIndex}`);
|
||||
row.querySelectorAll('input[name^="reps_"]').forEach(i => i.name = `reps_${newIndex}`);
|
||||
|
||||
const addExerciseBtn = row.querySelector('.add-exercise-btn');
|
||||
if (addExerciseBtn) addExerciseBtn.dataset.sessionIndex = newIndex;
|
||||
|
||||
row.dataset.index = newIndex;
|
||||
});
|
||||
sessionCounter = rows.length;
|
||||
}
|
||||
|
||||
addSessionBtn.addEventListener('click', addSessionRow);
|
||||
sessionsContainer.querySelectorAll('.session-row .remove-session-btn').forEach(attachRemoveListener);
|
||||
sessionsContainer.querySelectorAll('.exercise-row .remove-exercise-btn').forEach(attachExerciseRemoveListener);
|
||||
sessionsContainer.querySelectorAll('.session-row .add-exercise-btn').forEach(btn => {
|
||||
btn.addEventListener('click', handleAddExerciseClick);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
89
templates/program_import.html
Normal file
89
templates/program_import.html
Normal file
@@ -0,0 +1,89 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Import Program{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Import Workout Program</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Upload a JSON file containing your program structure, sessions, and sets/reps metadata.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<form action="{{ url_for('programs.import_program') }}" method="POST" enctype="multipart/form-data"
|
||||
class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Program JSON File</label>
|
||||
<div
|
||||
class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md hover:border-indigo-400 transition-colors">
|
||||
<div class="space-y-1 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none"
|
||||
viewBox="0 0 48 48" aria-hidden="true">
|
||||
<path
|
||||
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div class="flex text-sm text-gray-600">
|
||||
<label for="file-upload"
|
||||
class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
|
||||
<span>Upload a file</span>
|
||||
<input id="file-upload" name="file" type="file" accept=".json" class="sr-only"
|
||||
required onchange="updateFileName(this)">
|
||||
</label>
|
||||
<p class="pl-1">or drag and drop</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">JSON file up to 10MB</p>
|
||||
<p id="file-name" class="mt-2 text-sm text-indigo-600 font-semibold"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="{{ url_for('programs.list_programs') }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Upload and Import
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 bg-indigo-50 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-indigo-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-indigo-800">JSON Format Requirement</h3>
|
||||
<div class="mt-2 text-sm text-indigo-700">
|
||||
<p>The JSON file should follow the shared schema, including <code>program_name</code>,
|
||||
<code>description</code>, and a <code>sessions</code> array with <code>exercises</code>.
|
||||
Each exercise should have <code>id</code>, <code>name</code>, <code>sets</code>,
|
||||
<code>rep_range</code>, and <code>order</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function updateFileName(input) {
|
||||
const fileName = input.files[0] ? input.files[0].name : '';
|
||||
document.getElementById('file-name').textContent = fileName;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -6,10 +6,17 @@
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">Workout Programs</h1>
|
||||
<a href="{{ url_for('programs.create_program') }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Create New Program
|
||||
</a>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('programs.import_program') }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
hx-get="{{ url_for('programs.import_program') }}" hx-target="#container" hx-push-url="true">
|
||||
Import from JSON
|
||||
</a>
|
||||
<a href="{{ url_for('programs.create_program') }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Create New Program
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
@@ -40,12 +47,17 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-medium text-indigo-600 truncate">{{ program.name }}</p>
|
||||
<div class="ml-2 flex-shrink-0 flex space-x-2"> {# Added space-x-2 #}
|
||||
{# TODO: Add View/Edit/Assign buttons later #}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 items-center">
|
||||
{# Added items-center #}
|
||||
ID: {{ program.program_id }}
|
||||
</span>
|
||||
{# Edit Button #}
|
||||
<a href="{{ url_for('programs.edit_program', program_id=program.program_id) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900"
|
||||
hx-get="{{ url_for('programs.edit_program', program_id=program.program_id) }}"
|
||||
hx-target="#container" hx-push-url="true" hx-swap="innerHTML">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path
|
||||
d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
</a>
|
||||
{# Delete Button #}
|
||||
<button type="button" class="text-red-600 hover:text-red-800 focus:outline-none"
|
||||
hx-delete="{{ url_for('programs.delete_program', program_id=program.program_id) }}"
|
||||
@@ -60,15 +72,27 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 sm:flex sm:justify-between">
|
||||
<div class="sm:flex">
|
||||
<p class="flex items-center text-sm text-gray-500">
|
||||
{{ program.description | default('No description provided.') }}
|
||||
</p>
|
||||
<div class="mt-2 text-sm text-gray-500">
|
||||
<p class="mb-3">{{ program.description | default('No description provided.') }}</p>
|
||||
|
||||
{% if program.sessions %}
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
{% for session in program.sessions %}
|
||||
<div
|
||||
class="bg-gray-50 border border-gray-200 rounded p-2 text-xs min-w-[120px] max-w-[180px]">
|
||||
<p class="font-bold text-gray-700 mb-1">
|
||||
Day {{ session.session_order }}{% if session.session_name %}: {{
|
||||
session.session_name }}{% endif %}
|
||||
</p>
|
||||
<ul class="list-disc list-inside text-gray-600 space-y-0.5">
|
||||
{% for exercise in session.exercises %}
|
||||
<li class="truncate" title="{{ exercise.name }}">{{ exercise.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{# <div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
|
||||
Created: {{ program.created_at | strftime('%Y-%m-%d') }}
|
||||
</div> #}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -25,7 +25,19 @@
|
||||
{{ program.description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{# Add Edit/Assign buttons here later #}
|
||||
<div class="mt-4 flex space-x-3">
|
||||
<a href="{{ url_for('programs.edit_program', program_id=program.program_id) }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
hx-get="{{ url_for('programs.edit_program', program_id=program.program_id) }}"
|
||||
hx-target="#container" hx-push-url="true" hx-swap="innerHTML">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-1 mr-2 h-5 w-5 text-gray-400" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path
|
||||
d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
Edit Program
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,32 +51,56 @@
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
Day {{ session.session_order }}{% if session.session_name %}: {{ session.session_name }}{% endif %}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Tag: {{ session.tag_name }} (ID: {{ session.tag_id }})</p>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 px-4 py-5 sm:p-0">
|
||||
<dl class="sm:divide-y sm:divide-gray-200">
|
||||
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<div class="py-4 sm:py-5 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500 mb-2">
|
||||
Exercises
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
{% if session.exercises %}
|
||||
<ul role="list" class="border border-gray-200 rounded-md divide-y divide-gray-200">
|
||||
{% for exercise in session.exercises %}
|
||||
<li class="pl-3 pr-4 py-3 flex items-center justify-between text-sm">
|
||||
<div class="w-0 flex-1 flex items-center">
|
||||
<!-- Heroicon name: solid/paper-clip -->
|
||||
{# Could add an icon here #}
|
||||
<span class="ml-2 flex-1 w-0 truncate">
|
||||
{{ exercise.name }} (ID: {{ exercise.exercise_id }})
|
||||
</span>
|
||||
</div>
|
||||
{# Add links/actions per exercise later if needed #}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="overflow-x-auto border border-gray-200 rounded-md">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col"
|
||||
class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Order</th>
|
||||
<th scope="col"
|
||||
class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Exercise</th>
|
||||
<th scope="col"
|
||||
class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Sets</th>
|
||||
<th scope="col"
|
||||
class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Rep Range</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for exercise in session.exercises %}
|
||||
<tr>
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ loop.index if not exercise.exercise_order else
|
||||
exercise.exercise_order }}
|
||||
</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{{ exercise.name }}
|
||||
</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ exercise.sets if exercise.sets else '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ exercise.rep_range if exercise.rep_range else '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500 italic">No exercises found for this session's tag filter.</p>
|
||||
<p class="text-gray-500 italic">No exercises found for this session.</p>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user