Coach — Workout Builder
An energetic dark workout builder where coaches drag exercises from a searchable library into a day plan, dial in sets, reps and rest per movement, reorder with live volume and estimated-duration math, and save the session. Electric-lime CTAs, muscle-group filters, empty-state coaching and a toast-confirmed save make programming a full training day feel fast, tactile and motivating.
MCP
Code
:root {
--bg: #0f1115;
--surface: #171a21;
--surface-2: #1f232c;
--lime: #c2f542;
--lime-d: #a3d62f;
--orange: #ff6b35;
--ink: #f2f4f0;
--muted: #9aa0a6;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--white: #fff;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1200px 600px at 80% -10%, rgba(194, 245, 66, 0.08), transparent 60%),
radial-gradient(900px 500px at -10% 110%, rgba(255, 107, 53, 0.07), transparent 55%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
button { font-family: inherit; cursor: pointer; }
:focus-visible {
outline: 2px solid var(--lime);
outline-offset: 2px;
border-radius: 4px;
}
.app {
max-width: 1180px;
margin: 0 auto;
padding: 22px clamp(14px, 3vw, 28px) 48px;
}
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-mark {
display: grid;
place-items: center;
width: 46px; height: 46px;
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--lime), var(--lime-d));
color: #10130a;
font-size: 22px;
box-shadow: 0 8px 24px rgba(194, 245, 66, 0.25);
}
.brand-kicker {
margin: 0;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--lime-d);
}
.brand-title {
margin: 0;
font-size: clamp(20px, 3.6vw, 28px);
font-weight: 800;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.coach-chip {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 7px 14px 7px 8px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
font-size: 13px;
font-weight: 600;
color: var(--ink);
}
.coach-avatar {
display: grid;
place-items: center;
width: 30px; height: 30px;
border-radius: 50%;
background: linear-gradient(135deg, var(--orange), #ff9166);
color: #23120a;
font-size: 12px;
font-weight: 800;
}
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 340px 1fr;
gap: 18px;
align-items: start;
}
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
}
/* ---------- Library ---------- */
.panel-head { margin-bottom: 14px; }
.panel-title {
margin: 0;
font-size: 15px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.panel-sub {
margin: 2px 0 0;
font-size: 12px;
color: var(--muted);
}
.search-row { position: relative; margin-bottom: 12px; }
.search-ico {
position: absolute;
left: 12px; top: 50%;
transform: translateY(-50%);
font-size: 13px;
opacity: 0.7;
}
.search {
width: 100%;
padding: 11px 12px 11px 34px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
color: var(--ink);
font-size: 14px;
}
.search::placeholder { color: var(--muted); }
.search:focus { border-color: var(--line-2); outline: none; }
.filters {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-bottom: 14px;
}
.chip {
padding: 6px 12px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
font-size: 12px;
font-weight: 600;
transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s;
}
.chip:hover { color: var(--ink); border-color: var(--line-2); }
.chip:active { transform: scale(0.96); }
.chip.is-active {
background: var(--lime);
border-color: var(--lime);
color: #10130a;
}
.ex-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 9px;
max-height: 560px;
overflow-y: auto;
padding-right: 4px;
}
.ex-card {
display: flex;
align-items: center;
gap: 12px;
padding: 11px 12px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
cursor: grab;
transition: border-color 0.15s, transform 0.12s, box-shadow 0.15s;
}
.ex-card:hover {
border-color: var(--lime-d);
transform: translateY(-2px);
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.28);
}
.ex-card:active { cursor: grabbing; }
.ex-card.dragging { opacity: 0.45; }
.ex-thumb {
flex: none;
display: grid;
place-items: center;
width: 42px; height: 42px;
border-radius: var(--r-sm);
font-size: 19px;
color: #10130a;
background: linear-gradient(135deg, var(--lime), var(--lime-d));
}
.ex-thumb[data-g="Back"] { background: linear-gradient(135deg, #7ee8fa, #3aa9c9); }
.ex-thumb[data-g="Legs"] { background: linear-gradient(135deg, var(--orange), #ff9166); }
.ex-thumb[data-g="Shoulders"] { background: linear-gradient(135deg, #b388ff, #7c4dff); }
.ex-thumb[data-g="Arms"] { background: linear-gradient(135deg, #ffd166, #f0a500); color: #241c05; }
.ex-thumb[data-g="Core"] { background: linear-gradient(135deg, #ff6b9d, #d63384); color: #fff; }
.ex-info { min-width: 0; flex: 1; }
.ex-name {
margin: 0;
font-size: 13.5px;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ex-meta {
margin: 1px 0 0;
font-size: 11.5px;
color: var(--muted);
}
.ex-add {
flex: none;
width: 28px; height: 28px;
border-radius: var(--r-sm);
border: 1px solid var(--line-2);
background: transparent;
color: var(--lime);
font-size: 18px;
font-weight: 700;
line-height: 1;
transition: background 0.15s, color 0.15s;
}
.ex-add:hover { background: var(--lime); color: #10130a; }
.ex-empty {
text-align: center;
color: var(--muted);
font-size: 13px;
padding: 20px 0;
}
/* ---------- Plan ---------- */
.plan { display: flex; flex-direction: column; min-height: 520px; }
.plan-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.day-name {
width: 100%;
max-width: 420px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--ink);
font-family: inherit;
font-size: clamp(17px, 2.4vw, 21px);
font-weight: 800;
letter-spacing: 0.01em;
padding: 2px 0;
}
.day-name:focus {
outline: none;
border-bottom-color: var(--lime);
}
.plan-tags { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
.badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
border-radius: 999px;
font-size: 11.5px;
font-weight: 700;
letter-spacing: 0.04em;
}
.badge-count {
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--muted);
}
.badge-count b { color: var(--ink); }
.badge-diff {
background: rgba(194, 245, 66, 0.14);
color: var(--lime);
border: 1px solid rgba(194, 245, 66, 0.3);
}
.badge-diff.is-hard {
background: rgba(255, 107, 53, 0.16);
color: var(--orange);
border-color: rgba(255, 107, 53, 0.35);
}
.badge-diff.is-light {
background: rgba(126, 232, 250, 0.14);
color: #7ee8fa;
border-color: rgba(126, 232, 250, 0.3);
}
.btn-ghost {
flex: none;
padding: 8px 14px;
background: transparent;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
color: var(--muted);
font-size: 13px;
font-weight: 600;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.btn-ghost:hover { color: var(--ink); border-color: var(--orange); background: rgba(255, 107, 53, 0.08); }
/* Drop zone */
.drop-zone {
flex: 1;
border: 2px dashed var(--line);
border-radius: var(--r-md);
padding: 12px;
transition: border-color 0.15s, background 0.15s;
}
.drop-zone.drag-over {
border-color: var(--lime);
background: rgba(194, 245, 66, 0.05);
}
.plan-empty {
height: 100%;
min-height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--muted);
gap: 4px;
}
.plan-empty-ico { font-size: 40px; margin-bottom: 6px; filter: grayscale(0.2); }
.plan-empty-title {
margin: 0;
font-size: 16px;
font-weight: 800;
color: var(--ink);
letter-spacing: 0.04em;
text-transform: uppercase;
}
.plan-empty-sub { margin: 0; font-size: 13px; max-width: 260px; }
.plan-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.plan-row {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 12px;
align-items: center;
padding: 12px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
transition: border-color 0.15s, box-shadow 0.15s, transform 0.12s;
}
.plan-row:hover { border-color: var(--line-2); }
.plan-row.dragging { opacity: 0.5; border-color: var(--lime); }
.plan-row.drop-target { box-shadow: 0 -2px 0 var(--lime); }
.plan-row.pop { animation: pop 0.3s ease; }
@keyframes pop {
0% { transform: scale(0.96); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
.row-handle {
align-self: stretch;
display: grid;
place-items: center;
width: 26px;
color: var(--muted);
cursor: grab;
font-size: 15px;
letter-spacing: -2px;
border-radius: var(--r-sm);
background: rgba(255, 255, 255, 0.03);
}
.row-handle:active { cursor: grabbing; }
.row-main { min-width: 0; }
.row-idx {
display: inline-block;
min-width: 20px;
color: var(--lime);
font-weight: 800;
font-size: 12px;
letter-spacing: 0.05em;
}
.row-name {
margin: 0;
font-size: 14.5px;
font-weight: 700;
display: inline;
}
.row-group {
margin: 4px 0 0;
font-size: 11px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.row-fields {
display: flex;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
}
.field {
display: flex;
flex-direction: column;
gap: 3px;
}
.field-label {
font-size: 9.5px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
padding-left: 2px;
}
.stepper {
display: inline-flex;
align-items: center;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-sm);
overflow: hidden;
}
.stepper button {
width: 26px; height: 30px;
border: none;
background: transparent;
color: var(--lime);
font-size: 16px;
font-weight: 700;
line-height: 1;
transition: background 0.12s;
}
.stepper button:hover { background: rgba(194, 245, 66, 0.14); }
.stepper input {
width: 40px;
height: 30px;
border: none;
background: transparent;
color: var(--ink);
text-align: center;
font-family: inherit;
font-size: 13px;
font-weight: 700;
-moz-appearance: textfield;
}
.stepper input::-webkit-outer-spin-button,
.stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.stepper input:focus { outline: none; }
.stepper .unit { padding-right: 8px; font-size: 11px; color: var(--muted); font-weight: 600; }
.row-remove {
align-self: start;
width: 28px; height: 28px;
border-radius: var(--r-sm);
border: 1px solid var(--line);
background: transparent;
color: var(--muted);
font-size: 15px;
line-height: 1;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.row-remove:hover { color: var(--orange); border-color: var(--orange); background: rgba(255, 107, 53, 0.08); }
/* Summary */
.summary {
display: grid;
grid-template-columns: repeat(3, auto) 1fr;
align-items: center;
gap: 18px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--line);
}
.stat { display: flex; flex-direction: column; }
.stat-num {
font-size: 22px;
font-weight: 800;
color: var(--ink);
line-height: 1;
}
.stat-num small { font-size: 13px; color: var(--muted); font-weight: 700; }
.stat-label {
font-size: 10.5px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-top: 3px;
}
.btn-save {
justify-self: end;
padding: 12px 26px;
border: none;
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--lime), var(--lime-d));
color: #10130a;
font-size: 14px;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
box-shadow: 0 8px 24px rgba(194, 245, 66, 0.28);
transition: transform 0.12s, box-shadow 0.15s, filter 0.15s;
}
.btn-save:hover { transform: translateY(-2px); box-shadow: 0 12px 32px rgba(194, 245, 66, 0.4); filter: brightness(1.05); }
.btn-save:active { transform: translateY(0); }
.btn-save:disabled {
background: var(--surface-2);
color: var(--muted);
box-shadow: none;
cursor: not-allowed;
transform: none;
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%) translateY(20px);
background: var(--lime);
color: #10130a;
padding: 12px 22px;
border-radius: 999px;
font-size: 13.5px;
font-weight: 700;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 50;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.layout { grid-template-columns: 1fr; }
.ex-list { max-height: 320px; }
}
@media (max-width: 520px) {
.app { padding: 16px 12px 40px; }
.topbar { flex-wrap: wrap; }
.coach-chip { font-size: 12px; }
.summary {
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.btn-save {
grid-column: 1 / -1;
justify-self: stretch;
text-align: center;
}
.stat-num { font-size: 19px; }
.plan-row { grid-template-columns: auto 1fr; }
.row-remove { grid-column: 2; justify-self: end; }
.row-fields { gap: 6px; }
}(function () {
"use strict";
// ---------- Data ----------
var LIBRARY = [
{ id: "bench", name: "Barbell Bench Press", group: "Chest", icon: "🏋️", sets: 4, reps: 8, rest: 90 },
{ id: "incdb", name: "Incline DB Press", group: "Chest", icon: "💪", sets: 3, reps: 10, rest: 75 },
{ id: "fly", name: "Cable Fly", group: "Chest", icon: "🔗", sets: 3, reps: 12, rest: 60 },
{ id: "pullup", name: "Weighted Pull-Up", group: "Back", icon: "🧗", sets: 4, reps: 6, rest: 90 },
{ id: "row", name: "Barbell Row", group: "Back", icon: "🚣", sets: 4, reps: 8, rest: 90 },
{ id: "latpull", name: "Lat Pulldown", group: "Back", icon: "⬇️", sets: 3, reps: 12, rest: 60 },
{ id: "squat", name: "Back Squat", group: "Legs", icon: "🦵", sets: 5, reps: 5, rest: 120 },
{ id: "rdl", name: "Romanian Deadlift", group: "Legs", icon: "⚙️", sets: 4, reps: 8, rest: 105 },
{ id: "lunge", name: "Walking Lunge", group: "Legs", icon: "🚶", sets: 3, reps: 12, rest: 75 },
{ id: "ohp", name: "Overhead Press", group: "Shoulders", icon: "🙆", sets: 4, reps: 8, rest: 90 },
{ id: "lateral", name: "Lateral Raise", group: "Shoulders", icon: "🕊️", sets: 3, reps: 15, rest: 45 },
{ id: "facepull", name: "Face Pull", group: "Shoulders", icon: "🎯", sets: 3, reps: 15, rest: 45 },
{ id: "curl", name: "EZ-Bar Curl", group: "Arms", icon: "💪", sets: 3, reps: 12, rest: 60 },
{ id: "pushdown", name: "Tricep Pushdown", group: "Arms", icon: "🔽", sets: 3, reps: 12, rest: 60 },
{ id: "hammer", name: "Hammer Curl", group: "Arms", icon: "🔨", sets: 3, reps: 12, rest: 60 },
{ id: "plank", name: "Weighted Plank", group: "Core", icon: "🧘", sets: 3, reps: 45, rest: 45 },
{ id: "hanging", name: "Hanging Leg Raise", group: "Core", icon: "🪝", sets: 3, reps: 15, rest: 45 },
{ id: "cablecrunch", name: "Cable Crunch", group: "Core", icon: "🌀", sets: 3, reps: 15, rest: 45 }
];
// Working plan: array of {uid, ref}
var plan = [];
var uidSeq = 1;
var activeGroup = "all";
var searchTerm = "";
// ---------- Elements ----------
var $ = function (s) { return document.querySelector(s); };
var exList = $("#exList");
var exEmpty = $("#exEmpty");
var planList = $("#planList");
var planEmpty = $("#planEmpty");
var dropZone = $("#dropZone");
var searchEl = $("#search");
var toastEl = $("#toast");
var saveBtn = $("#saveBtn");
var clearBtn = $("#clearBtn");
var byId = function (id) {
for (var i = 0; i < LIBRARY.length; i++) if (LIBRARY[i].id === id) return LIBRARY[i];
return null;
};
// ---------- Toast ----------
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2400);
}
// ---------- Library render ----------
function renderLibrary() {
exList.innerHTML = "";
var shown = 0;
LIBRARY.forEach(function (ex) {
var matchGroup = activeGroup === "all" || ex.group === activeGroup;
var matchSearch = ex.name.toLowerCase().indexOf(searchTerm) !== -1 ||
ex.group.toLowerCase().indexOf(searchTerm) !== -1;
if (!matchGroup || !matchSearch) return;
shown++;
var li = document.createElement("li");
li.className = "ex-card";
li.setAttribute("draggable", "true");
li.dataset.id = ex.id;
li.setAttribute("tabindex", "0");
li.setAttribute("role", "button");
li.setAttribute("aria-label", "Add " + ex.name + " to plan");
li.innerHTML =
'<span class="ex-thumb" data-g="' + ex.group + '" aria-hidden="true">' + ex.icon + "</span>" +
'<div class="ex-info">' +
'<p class="ex-name">' + ex.name + "</p>" +
'<p class="ex-meta">' + ex.group + " · " + ex.sets + "×" + ex.reps + "</p>" +
"</div>" +
'<button class="ex-add" type="button" aria-hidden="true" tabindex="-1">+</button>';
// drag
li.addEventListener("dragstart", function (e) {
e.dataTransfer.setData("text/plain", ex.id);
e.dataTransfer.effectAllowed = "copy";
li.classList.add("dragging");
});
li.addEventListener("dragend", function () { li.classList.remove("dragging"); });
// tap / keyboard add
var add = function () { addExercise(ex.id); };
li.addEventListener("click", add);
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); add(); }
});
exList.appendChild(li);
});
exEmpty.hidden = shown !== 0;
}
// ---------- Plan operations ----------
function addExercise(id) {
var ref = byId(id);
if (!ref) return;
plan.push({
uid: uidSeq++,
id: ref.id,
name: ref.name,
group: ref.group,
icon: ref.icon,
sets: ref.sets,
reps: ref.reps,
rest: ref.rest
});
renderPlan(true);
updateSummary();
toast(ref.name + " added");
}
function removeExercise(uid) {
plan = plan.filter(function (p) { return p.uid !== uid; });
renderPlan(false);
updateSummary();
}
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function renderPlan(animateLast) {
planList.innerHTML = "";
planEmpty.style.display = plan.length ? "none" : "flex";
plan.forEach(function (item, i) {
var li = document.createElement("li");
li.className = "plan-row";
if (animateLast && i === plan.length - 1) li.classList.add("pop");
li.dataset.uid = item.uid;
li.setAttribute("draggable", "true");
li.innerHTML =
'<div class="row-handle" aria-hidden="true" title="Drag to reorder">⋮⋮</div>' +
'<div class="row-main">' +
'<span class="row-idx">' + (i + 1) + "</span>" +
'<h3 class="row-name">' + item.name + "</h3>" +
'<p class="row-group">' + item.group + "</p>" +
'<div class="row-fields">' +
fieldHTML(item.uid, "sets", "Sets", item.sets, "") +
fieldHTML(item.uid, "reps", "Reps", item.reps, "") +
fieldHTML(item.uid, "rest", "Rest", item.rest, "s") +
"</div>" +
"</div>" +
'<button class="row-remove" type="button" aria-label="Remove ' + item.name + '">✕</button>';
// remove
li.querySelector(".row-remove").addEventListener("click", function () {
removeExercise(item.uid);
});
// stepper handlers
li.querySelectorAll(".stepper").forEach(function (st) {
var field = st.dataset.field;
var input = st.querySelector("input");
var conf = FIELD_CONF[field];
st.querySelector(".dec").addEventListener("click", function () {
setField(item.uid, field, item[field] - conf.step);
});
st.querySelector(".inc").addEventListener("click", function () {
setField(item.uid, field, item[field] + conf.step);
});
input.addEventListener("change", function () {
var v = parseInt(input.value, 10);
if (isNaN(v)) v = conf.min;
setField(item.uid, field, v);
});
});
attachRowDrag(li);
planList.appendChild(li);
});
}
var FIELD_CONF = {
sets: { min: 1, max: 12, step: 1 },
reps: { min: 1, max: 60, step: 1 },
rest: { min: 0, max: 300, step: 15 }
};
function fieldHTML(uid, field, label, val, unit) {
return '<div class="field">' +
'<span class="field-label">' + label + "</span>" +
'<div class="stepper" data-field="' + field + '">' +
'<button class="dec" type="button" aria-label="Decrease ' + label + '">−</button>' +
'<input type="text" inputmode="numeric" value="' + val + '" aria-label="' + label + '" />' +
(unit ? '<span class="unit">' + unit + "</span>" : "") +
'<button class="inc" type="button" aria-label="Increase ' + label + '">+</button>' +
"</div>" +
"</div>";
}
function setField(uid, field, value) {
var conf = FIELD_CONF[field];
var v = clamp(value, conf.min, conf.max);
for (var i = 0; i < plan.length; i++) {
if (plan[i].uid === uid) { plan[i][field] = v; break; }
}
renderPlan(false);
updateSummary();
}
// ---------- Reorder drag ----------
var dragUid = null;
function attachRowDrag(li) {
li.addEventListener("dragstart", function (e) {
dragUid = parseInt(li.dataset.uid, 10);
li.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
try { e.dataTransfer.setData("text/reorder", String(dragUid)); } catch (err) {}
});
li.addEventListener("dragend", function () {
li.classList.remove("dragging");
dragUid = null;
Array.prototype.forEach.call(planList.children, function (c) {
c.classList.remove("drop-target");
});
});
li.addEventListener("dragover", function (e) {
if (dragUid === null) return;
e.preventDefault();
li.classList.add("drop-target");
});
li.addEventListener("dragleave", function () { li.classList.remove("drop-target"); });
li.addEventListener("drop", function (e) {
if (dragUid === null) return;
e.preventDefault();
e.stopPropagation();
var targetUid = parseInt(li.dataset.uid, 10);
reorder(dragUid, targetUid);
});
}
function reorder(fromUid, toUid) {
if (fromUid === toUid) return;
var fromIdx = -1, toIdx = -1;
plan.forEach(function (p, i) {
if (p.uid === fromUid) fromIdx = i;
if (p.uid === toUid) toIdx = i;
});
if (fromIdx === -1 || toIdx === -1) return;
var moved = plan.splice(fromIdx, 1)[0];
plan.splice(toIdx, 0, moved);
renderPlan(false);
updateSummary();
}
// ---------- Summary ----------
function updateSummary() {
var totalSets = 0, totalReps = 0, totalSeconds = 0;
plan.forEach(function (p) {
totalSets += p.sets;
totalReps += p.sets * p.reps;
// estimate: ~4s per rep of work + rest between sets
var workPerSet = p.group === "Core" ? p.reps : p.reps * 4;
totalSeconds += p.sets * workPerSet + (p.sets - 1) * p.rest;
});
// transition between exercises
if (plan.length > 1) totalSeconds += (plan.length - 1) * 45;
$("#statSets").textContent = totalSets;
$("#statVolume").textContent = totalReps;
$("#statTime").innerHTML = Math.max(0, Math.round(totalSeconds / 60)) + "<small>m</small>";
$("#rowCount").textContent = plan.length;
updateDifficulty(totalSets);
saveBtn.disabled = plan.length === 0;
}
function updateDifficulty(totalSets) {
var badge = $("#diffBadge");
badge.classList.remove("is-hard", "is-light");
var label;
if (totalSets === 0) { label = "Empty"; }
else if (totalSets <= 12) { label = "Light"; badge.classList.add("is-light"); }
else if (totalSets <= 22) { label = "Moderate"; }
else { label = "Brutal"; badge.classList.add("is-hard"); }
badge.textContent = label;
}
// ---------- Drop zone (add via drag from library) ----------
dropZone.addEventListener("dragover", function (e) {
// only highlight for library adds (no reorder in progress)
if (dragUid !== null) return;
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
dropZone.classList.add("drag-over");
});
dropZone.addEventListener("dragleave", function (e) {
if (e.target === dropZone) dropZone.classList.remove("drag-over");
});
dropZone.addEventListener("drop", function (e) {
dropZone.classList.remove("drag-over");
if (dragUid !== null) return; // reorder handled per-row
var id = e.dataTransfer.getData("text/plain");
if (id && byId(id)) {
e.preventDefault();
addExercise(id);
}
});
// ---------- Filters & search ----------
document.querySelectorAll(".chip").forEach(function (chip) {
chip.addEventListener("click", function () {
document.querySelectorAll(".chip").forEach(function (c) { c.classList.remove("is-active"); });
chip.classList.add("is-active");
activeGroup = chip.dataset.group;
renderLibrary();
});
});
searchEl.addEventListener("input", function () {
searchTerm = searchEl.value.trim().toLowerCase();
renderLibrary();
});
// ---------- Buttons ----------
clearBtn.addEventListener("click", function () {
if (!plan.length) { toast("Plan is already empty"); return; }
plan = [];
renderPlan(false);
updateSummary();
toast("Plan cleared");
});
saveBtn.addEventListener("click", function () {
if (!plan.length) return;
var name = $("#dayName").value.trim() || "Untitled workout";
toast("Saved “" + name + "” · " + plan.length + " exercises 💪");
saveBtn.animate(
[{ transform: "scale(1)" }, { transform: "scale(0.94)" }, { transform: "scale(1)" }],
{ duration: 240 }
);
});
// ---------- Seed ----------
["bench", "incdb", "ohp", "pushdown"].forEach(addExerciseSilent);
function addExerciseSilent(id) {
var ref = byId(id);
if (!ref) return;
plan.push({
uid: uidSeq++, id: ref.id, name: ref.name, group: ref.group,
icon: ref.icon, sets: ref.sets, reps: ref.reps, rest: ref.rest
});
}
// ---------- Init ----------
renderLibrary();
renderPlan(false);
updateSummary();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coach — Workout Builder</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">⚡</span>
<div>
<p class="brand-kicker">Iron Method</p>
<h1 class="brand-title">Workout Builder</h1>
</div>
</div>
<div class="topbar-actions">
<span class="coach-chip">
<span class="coach-avatar" aria-hidden="true">MK</span>
Coach Maya Kessler
</span>
</div>
</header>
<main class="layout">
<!-- LIBRARY PANEL -->
<section class="panel library" aria-label="Exercise library">
<div class="panel-head">
<h2 class="panel-title">Exercise Library</h2>
<p class="panel-sub">Drag or tap to add</p>
</div>
<div class="search-row">
<span class="search-ico" aria-hidden="true">🔍</span>
<input type="search" id="search" class="search" placeholder="Search movements…" aria-label="Search exercises" />
</div>
<div class="filters" role="group" aria-label="Filter by muscle group">
<button class="chip is-active" data-group="all">All</button>
<button class="chip" data-group="Chest">Chest</button>
<button class="chip" data-group="Back">Back</button>
<button class="chip" data-group="Legs">Legs</button>
<button class="chip" data-group="Shoulders">Shoulders</button>
<button class="chip" data-group="Arms">Arms</button>
<button class="chip" data-group="Core">Core</button>
</div>
<ul class="ex-list" id="exList" aria-label="Available exercises"></ul>
<p class="ex-empty" id="exEmpty" hidden>No movements match that search.</p>
</section>
<!-- PLAN PANEL -->
<section class="panel plan" aria-label="Day plan">
<div class="plan-head">
<div class="plan-name-wrap">
<label class="sr-only" for="dayName">Workout day name</label>
<input id="dayName" class="day-name" value="Push Day — Hypertrophy" aria-label="Workout day name" />
<div class="plan-tags">
<span class="badge badge-diff" id="diffBadge">Moderate</span>
<span class="badge badge-count"><b id="rowCount">0</b> exercises</span>
</div>
</div>
<button class="btn-ghost" id="clearBtn" type="button">Clear</button>
</div>
<div class="drop-zone" id="dropZone" aria-label="Workout plan, drop exercises here">
<div class="plan-empty" id="planEmpty">
<span class="plan-empty-ico" aria-hidden="true">🏋️</span>
<p class="plan-empty-title">Build the session</p>
<p class="plan-empty-sub">Drag exercises here or tap a card in the library to start programming.</p>
</div>
<ul class="plan-list" id="planList" aria-label="Programmed exercises"></ul>
</div>
<footer class="summary" aria-live="polite">
<div class="stat">
<span class="stat-num" id="statSets">0</span>
<span class="stat-label">Total sets</span>
</div>
<div class="stat">
<span class="stat-num" id="statVolume">0</span>
<span class="stat-label">Total reps</span>
</div>
<div class="stat">
<span class="stat-num" id="statTime">0<small>m</small></span>
<span class="stat-label">Est. duration</span>
</div>
<button class="btn-save" id="saveBtn" type="button">Save workout</button>
</footer>
</section>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A two-panel workout programming tool built for personal trainers. On the left, a searchable exercise library groups movements by muscle group with quick filter chips; each card shows the target area and default loading. On the right, the day plan is a live canvas where dragged-in exercises become editable rows — set your sets, reps and rest with inline steppers, then reorder them by dragging the handle to sequence the session exactly how you want it.
Every edit recomputes the session summary in real time: total sets, total volume (sets × reps) and an estimated duration derived from work plus rest. The plan header carries a day-name field and a difficulty badge, while an empty canvas shows a friendly coaching prompt instead of a blank void. Remove rows with a single tap, clear the whole plan, or hit the glowing lime Save workout button to commit — a toast confirms the session was stored.
The interface is keyboard-usable and screen-reader aware: exercise cards can be added via keyboard, steppers expose proper labels, and drag handles have accessible roles. It collapses to a single-column layout under 520px so a coach can build a plan just as comfortably from a phone on the gym floor.