Skip to content

Commit 6d56311

Browse files
Add AMRAP handling for workouts (#42)
- add an AMRAP toggle to workout templates that disables rep range inputs - persist the AMRAP flag through the Cloudflare worker API - render AMRAP exercises in the workout logger with integer-only reps inputs instead of dropdowns ------ [Codex Task](https://chatgpt.com/codex/tasks/task_e_692419755c9083258bd08a49632cd1ae)
1 parent 211ad5e commit 6d56311

3 files changed

Lines changed: 111 additions & 29 deletions

File tree

cloudflare-workers/workouts/worker.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ function normalizeExerciseBlock(block) {
9898

9999
const name = typeof block.name === "string" ? block.name.trim() : "";
100100
const sets = Number.isFinite(Number(block.sets)) ? Number(block.sets) : 0;
101+
const amrap = Boolean(block.amrap);
101102
const repRange = block.repRange || {};
102103
const min = Number.isFinite(Number(repRange.min)) ? Number(repRange.min) : 0;
103104
const max = Number.isFinite(Number(repRange.max)) ? Number(repRange.max) : 0;
@@ -111,10 +112,13 @@ function normalizeExerciseBlock(block) {
111112
id: block.id || crypto.randomUUID(),
112113
name,
113114
sets: sets < 0 ? 0 : Math.round(sets),
114-
repRange: {
115-
min,
116-
max,
117-
},
115+
amrap,
116+
repRange: amrap
117+
? null
118+
: {
119+
min,
120+
max,
121+
},
118122
notes,
119123
};
120124
}

workout-logger.html

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ <h2 id="workout-title" style="margin: 0;"></h2>
386386
}
387387

388388
function repOptions(block) {
389+
if (block.amrap) return [];
389390
const options = [];
390391
const min = Number(block.repRange?.min) || 1;
391392
const max = Number(block.repRange?.max) || min;
@@ -413,6 +414,39 @@ <h2 id="workout-title" style="margin: 0;"></h2>
413414
return chip;
414415
}
415416

417+
function createAmrapInput(blockId, setIndex, initialValue) {
418+
const input = document.createElement('input');
419+
input.type = 'number';
420+
input.inputMode = 'numeric';
421+
input.min = '1';
422+
input.step = '1';
423+
input.placeholder = 'Reps (AMRAP)';
424+
input.dataset.blockId = blockId;
425+
input.dataset.setIndex = setIndex;
426+
input.dataset.field = 'reps';
427+
428+
if (Number.isInteger(initialValue)) {
429+
input.value = initialValue;
430+
}
431+
432+
input.addEventListener('input', () => {
433+
if (input.value === '') {
434+
input.setCustomValidity('');
435+
return;
436+
}
437+
438+
const parsed = Number.parseInt(input.value, 10);
439+
if (Number.isNaN(parsed)) {
440+
input.setCustomValidity('Enter a whole number');
441+
} else {
442+
input.value = parsed;
443+
input.setCustomValidity('');
444+
}
445+
});
446+
447+
return input;
448+
}
449+
416450
function createSetRow(blockId, setIndex, block, draft) {
417451
const row = document.createElement('div');
418452
row.className = 'set-row';
@@ -426,21 +460,6 @@ <h2 id="workout-title" style="margin: 0;"></h2>
426460
weightInput.dataset.setIndex = setIndex;
427461
weightInput.dataset.field = 'weight';
428462

429-
const repsSelect = document.createElement('select');
430-
repsSelect.dataset.blockId = blockId;
431-
repsSelect.dataset.setIndex = setIndex;
432-
repsSelect.dataset.field = 'reps';
433-
const placeholder = document.createElement('option');
434-
placeholder.value = '';
435-
placeholder.textContent = 'Reps';
436-
repsSelect.appendChild(placeholder);
437-
repOptions(block).forEach((rep) => {
438-
const option = document.createElement('option');
439-
option.value = rep;
440-
option.textContent = rep;
441-
repsSelect.appendChild(option);
442-
});
443-
444463
const rirSelect = document.createElement('select');
445464
rirSelect.dataset.blockId = blockId;
446465
rirSelect.dataset.setIndex = setIndex;
@@ -461,18 +480,48 @@ <h2 id="workout-title" style="margin: 0;"></h2>
461480
const draftExercise = draft?.exercises?.find((ex) => ex.blockId === blockId);
462481
const draftSet = draftExercise?.sets?.[setIndex];
463482

483+
let repsField = null;
484+
if (block.amrap) {
485+
const initial = draftSet?.reps ?? prevSet?.reps;
486+
repsField = createAmrapInput(blockId, setIndex, initial);
487+
} else {
488+
const repsSelect = document.createElement('select');
489+
repsSelect.dataset.blockId = blockId;
490+
repsSelect.dataset.setIndex = setIndex;
491+
repsSelect.dataset.field = 'reps';
492+
const placeholder = document.createElement('option');
493+
placeholder.value = '';
494+
placeholder.textContent = 'Reps';
495+
repsSelect.appendChild(placeholder);
496+
repOptions(block).forEach((rep) => {
497+
const option = document.createElement('option');
498+
option.value = rep;
499+
option.textContent = rep;
500+
repsSelect.appendChild(option);
501+
});
502+
repsField = repsSelect;
503+
}
504+
464505
if (draftSet) {
465506
weightInput.value = draftSet.weight ?? '';
466-
repsSelect.value = draftSet.reps ?? '';
507+
if (!block.amrap) {
508+
repsField.value = draftSet.reps ?? '';
509+
} else if (Number.isInteger(draftSet.reps)) {
510+
repsField.value = draftSet.reps;
511+
}
467512
rirSelect.value = draftSet.rir ?? '';
468513
} else if (prevSet) {
469514
weightInput.value = prevSet.weight ?? '';
470-
repsSelect.value = prevSet.reps ?? '';
515+
if (!block.amrap) {
516+
repsField.value = prevSet.reps ?? '';
517+
} else if (Number.isInteger(prevSet.reps)) {
518+
repsField.value = prevSet.reps;
519+
}
471520
rirSelect.value = prevSet.rir ?? '';
472521
}
473522

474523
row.appendChild(weightInput);
475-
row.appendChild(repsSelect);
524+
row.appendChild(repsField);
476525
row.appendChild(rirSelect);
477526
return row;
478527
}
@@ -483,7 +532,8 @@ <h2 id="workout-title" style="margin: 0;"></h2>
483532
details.open = true;
484533

485534
const summary = document.createElement('summary');
486-
summary.innerHTML = `<span>${block.name}</span><span class="exercise-meta">${block.sets} sets · ${block.repRange.min}-${block.repRange.max} reps</span>`;
535+
const repLabel = block.amrap ? 'AMRAP' : `${block.repRange?.min ?? '—'}-${block.repRange?.max ?? '—'} reps`;
536+
summary.innerHTML = `<span>${block.name}</span><span class="exercise-meta">${block.sets} sets · ${repLabel}</span>`;
487537

488538
const body = document.createElement('div');
489539
body.className = 'exercise-body';
@@ -560,11 +610,11 @@ <h2 id="workout-title" style="margin: 0;"></h2>
560610
const sets = [];
561611
for (let i = 0; i < block.sets; i += 1) {
562612
const weight = document.querySelector(`input[data-block-id="${block.id}"][data-set-index="${i}"][data-field="weight"]`);
563-
const reps = document.querySelector(`select[data-block-id="${block.id}"][data-set-index="${i}"][data-field="reps"]`);
613+
const reps = document.querySelector(`[data-block-id="${block.id}"][data-set-index="${i}"][data-field="reps"]`);
564614
const rir = document.querySelector(`select[data-block-id="${block.id}"][data-set-index="${i}"][data-field="rir"]`);
565615
sets.push({
566616
weight: weight?.value ? Number(weight.value) : null,
567-
reps: reps?.value ? Number(reps.value) : null,
617+
reps: reps?.value ? Number.parseInt(reps.value, 10) : null,
568618
rir: rir?.value ? Number(rir.value) : null,
569619
});
570620
}

workout-manager.html

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ <h2 style="margin: 0;">API settings</h2>
396396
const exerciseList = exercises.map((block, index) => {
397397
const perExercise = estimateExerciseSeconds(Number(block.sets));
398398
const restAfter = index < exercises.length - 1 ? REST_BETWEEN_EXERCISES : 0;
399+
const repLabel = block.amrap ? 'AMRAP' : `${block.repRange?.min ?? 0}${block.repRange?.max ?? 0} reps`;
399400
return `
400401
<div class="exercise-block">
401402
<header>
@@ -405,7 +406,7 @@ <h2 style="margin: 0;">API settings</h2>
405406
</div>
406407
<div class="pill-group">
407408
<span class="badge">${block.sets || 0} sets</span>
408-
<span class="badge">${block.repRange?.min ?? 0}${block.repRange?.max ?? 0} reps</span>
409+
<span class="badge">${repLabel}</span>
409410
<span class="badge">${formatSeconds(perExercise)} per exercise</span>
410411
${restAfter ? `<span class="badge">${formatSeconds(restAfter)} rest before next</span>` : ''}
411412
</div>
@@ -483,6 +484,13 @@ <h2 style="margin: 0;">API settings</h2>
483484
<label>Sets</label>
484485
<input type="number" min="1" class="exercise-sets" value="${prefill.sets || ''}" placeholder="4">
485486
</div>
487+
<div class="form-group">
488+
<label class="checkbox-label">
489+
<input type="checkbox" class="exercise-amrap" ${prefill.amrap ? 'checked' : ''}>
490+
AMRAP (as many reps as possible)
491+
</label>
492+
<p class="status">Enable when reps are performed to failure.</p>
493+
</div>
486494
<div class="form-group">
487495
<label>Rep range min</label>
488496
<input type="number" min="1" class="exercise-rep-min" value="${prefill.repRange?.min ?? ''}" placeholder="8">
@@ -501,9 +509,27 @@ <h2 style="margin: 0;">API settings</h2>
501509
const removeBtn = wrapper.querySelector('[data-action="remove"]');
502510
const moveUpBtn = wrapper.querySelector('[data-action="move-up"]');
503511
const moveDownBtn = wrapper.querySelector('[data-action="move-down"]');
512+
const amrapCheckbox = wrapper.querySelector('.exercise-amrap');
513+
const repMinInput = wrapper.querySelector('.exercise-rep-min');
514+
const repMaxInput = wrapper.querySelector('.exercise-rep-max');
504515

505516
wrapper.draggable = true;
506517

518+
function toggleRepRangeDisabled(checked) {
519+
repMinInput.disabled = checked;
520+
repMaxInput.disabled = checked;
521+
if (checked) {
522+
repMinInput.value = '';
523+
repMaxInput.value = '';
524+
}
525+
}
526+
527+
amrapCheckbox.addEventListener('change', () => {
528+
toggleRepRangeDisabled(amrapCheckbox.checked);
529+
});
530+
531+
toggleRepRangeDisabled(amrapCheckbox.checked);
532+
507533
wrapper.addEventListener('dragstart', (event) => {
508534
if (event.target.closest('input, textarea, button, select, option')) {
509535
event.preventDefault();
@@ -591,15 +617,17 @@ <h2 style="margin: 0;">API settings</h2>
591617
return Array.from(exerciseBlocksContainer.children).map((block) => {
592618
const name = block.querySelector('.exercise-name').value.trim();
593619
const sets = Number(block.querySelector('.exercise-sets').value) || 0;
594-
const repMin = Number(block.querySelector('.exercise-rep-min').value) || 0;
595-
const repMax = Number(block.querySelector('.exercise-rep-max').value) || 0;
620+
const amrap = block.querySelector('.exercise-amrap').checked;
621+
const repMin = amrap ? null : Number(block.querySelector('.exercise-rep-min').value) || 0;
622+
const repMax = amrap ? null : Number(block.querySelector('.exercise-rep-max').value) || 0;
596623
const notes = block.querySelector('.notes-input').value.trim();
597624

598625
return {
599626
id: block.dataset.blockId,
600627
name,
601628
sets,
602-
repRange: { min: repMin, max: repMax },
629+
amrap,
630+
repRange: amrap ? null : { min: repMin, max: repMax },
603631
notes,
604632
};
605633
}).filter(block => block.name);

0 commit comments

Comments
 (0)