Compare commits

...

8 Commits

Author SHA1 Message Date
f96143757a Random generation of workout plan 2025-03-30 21:52:16 -07:00
fb4acf962f Cleaner workout done indication 2025-03-30 16:07:32 -07:00
6cfce18819 Hide when workout done 2025-03-30 15:56:32 -07:00
c46b37756a Updated workout done indication 2025-03-29 22:16:10 -07:00
115728fe58 Redesigned buttons
Using CSS icons instead of text
2025-03-29 19:31:53 -07:00
7d7c89a0c9 Make indicators more round 2025-03-23 19:17:56 -07:00
793689e29c Cleanup 2025-03-23 13:12:15 -07:00
20dd0de6c0 Better mobile experience 2025-03-23 13:10:45 -07:00
7 changed files with 373 additions and 88 deletions

11
App.js
View File

@@ -1,9 +1,14 @@
document.addEventListener('DOMContentLoaded', function() {
fetch('routine.json')
fetch('collection.json')
.then(response => response.json())
.then(workout_json => {
// Use the JSON workout_json here
.then(collection_json => {
// Generate Exercise plan
const egen = new ExerciseGenerator(collection_json);
// 2 sets of 4 exercises each, 3 reps per set
let workout_json = egen.generate_build_and_burn(2, 4, 3);
console.log(workout_json);
const wo = new Workout(document.getElementById("content-wrapper"),
workout_json);
wo.render();

132
ExerciseGenerator.js Normal file
View File

@@ -0,0 +1,132 @@
class ExerciseGenerator {
// ----------
constructor(collection) {
this.collection = collection;
// console.log(this.collection);
this.used_exercise_ids = new Array;
this.ex_nxt;
this.ex_last = null;
}
// ----------
exercise_id_used(ex) {
return (this.used_exercise_ids.filter(function(v) { return (v == ex.id) }).length > 0);
}
// ----------
exercise_related_to(ex) {
let related = false;
let gen = this;
if (gen.ex_last == null) {
return related;
}
ex.focus.forEach(function(e) {
related |= gen.ex_last.focus.includes(e);
});
return related;
}
// ----------
get_next_exercise() {
let scope = this.collection.exercises;
let gen = this;
scope = this.collection.exercises.filter(function(e) {
let pick = true;
// Pick if exercise is not already used
pick &= (gen.exercise_id_used(e) == false);
// Pick if nor related to the previous one
pick &= (gen.exercise_related_to(e) == false);
return pick
});
gen.shuffle_exercise_array(scope);
// gen.show_exercise_array(scope);
return scope[0];
}
// ----------
show_exercise_array(arr) {
let gen = this;
arr.map(function(it) {
gen.show_exercise(it);
});
}
// ----------
show_exercise(ex) {
console.log(`${ex.id} : ${ex.name} [` + ex.focus.join(", ") + ']');
}
// ----------
// From answer on https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
shuffle_exercise_array(arr) {
let currentIndex = arr.length;
// While there remain elements to shuffle...
while (currentIndex != 0) {
// Pick a remaining element...
let randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[arr[currentIndex], arr[randomIndex]] = [
arr[randomIndex], arr[currentIndex]];
}
}
// ----------
generate_set(size) {
let set_exercises = Array();
let lim = size;
this.ex_last = null;
do {
this.ex_nxt = this.get_next_exercise();
if (this.ex_nxt == null) {
break;
}
this.used_exercise_ids.push(this.ex_nxt.id);
set_exercises.push(this.ex_nxt);
this.ex_last = this.ex_nxt;
lim--;
} while ((this.ex_nxt != null) && (lim > 0));
this.show_exercise_array(set_exercises);
console.log(this.used_exercise_ids);
return set_exercises;
}
// ----------
generate_build_and_burn(sets, exercises, reps) {
let wo_plan = {
"name": "Daily Build and Burn",
"set": [
]
}
for (let i = 1; i <= sets; i ++) {
let set = {
"name": `Set ${i}`,
"count": reps,
"exercise": this.generate_set(exercises)
}
wo_plan.set.push(set);
}
return wo_plan;
}
}

View File

@@ -7,6 +7,7 @@ constructor(cont, plan) {
cont.appendChild(wo_cont);
// Inputs to the class. Workout plan and container to display it in
this.arena = cont;
this.container = wo_cont;
this.plan = plan;
@@ -16,9 +17,20 @@ constructor(cont, plan) {
this.set_repeat_idx = 0;
this.exercise_idx = 0;
this.workout_done = 0;
this.show_header = 0;
this.show_debug_status = 0;
// // Enable debug
// this.show_debug_status = 1;
// Dimensions
this.vwidth = window.innerWidth;
this.vheight = window.innerHeight;
let qstring = window.location.search;
let url_params = new URLSearchParams(qstring);
// Enable debug
this.show_debug_status = url_params.has("dbg");
// Enable table header
this.show_header = url_params.has("hdr");
}
// ----------
@@ -41,8 +53,6 @@ progress_indicator(id) {
// ----------
render_workout_header() {
const hdr = this.text_element("h1", this.plan.name);
const wo_indicator = this.progress_indicator("wo-indicator");
hdr.appendChild(wo_indicator);
this.container.appendChild(hdr);
}
@@ -64,6 +74,7 @@ update_debug_info() {
const curr_set_repeat_max_idx = this.plan.set[this.set_idx].count-1;
const done_str = (this.workout_done === 1) ? "Done " : "";
dt.textContent = `${done_str}Action: ${this.action} Current: Set-${this.set_idx} [${this.set_repeat_idx}/${curr_set_repeat_max_idx}]/Exercise-${this.exercise_idx}`;
dt.textContent += `\nView ${this.vwidth} x ${this.vheight}`;
}
}
@@ -72,27 +83,24 @@ render_exercise(prnt, el, idx) {
const tblr = document.createElement("tr");
tblr.setAttribute("class", `exercise-row exercise-idx-${idx} exercise-pending`);
// Status
const sts_td = document.createElement("td");
const sts_cb = document.createElement("input");
sts_cb.setAttribute("type", "checkbox");
sts_td.appendChild(sts_cb);
sts_cb.setAttribute("disabled", true);
// Status
let col = sts_td;
col.classList.add("col-status");
tblr.appendChild(col);
el.done = 0;
sts_td.appendChild(sts_cb);
tblr.appendChild(sts_td);
// Name
col = this.text_element("td", el.name);
col.classList.add("col-name");
tblr.appendChild(col);
// Pattern/count
col = this.text_element("td", `${el.pattern} x ${el.count}`);
col.classList.add("col-pattern-count");
tblr.appendChild(col);
const name_td = document.createElement("td");
let ex_name = this.text_element("div", el.name);
ex_name.classList.add("col-name");
let ex_patt = this.text_element("div", `${el.pattern} x ${el.count}`);
ex_patt.classList.add("col-patt");
name_td.appendChild(ex_name);
name_td.appendChild(ex_patt);
tblr.appendChild(name_td);
el.done = 0;
prnt.appendChild(tblr);
}
@@ -112,8 +120,7 @@ render_set(el, idx) {
}
tbl.appendChild(tcap);
let show_header = 0;
if (show_header) {
if (this.show_header) {
// Header of the table
const tblh = document.createElement("thead");
const tblhr = document.createElement("tr");
@@ -124,9 +131,6 @@ render_set(el, idx) {
col = this.text_element("th", "Exercise");
col.classList.add("col-name");
tblhr.appendChild(col);
col = this.text_element("th", "Pattern/Count");
col.classList.add("col-pattern-count");
tblhr.appendChild(col);
tblh.appendChild(tblhr);
tbl.appendChild(tblh);
@@ -246,9 +250,19 @@ update_active_item() {
// Mark workout done
if (this.workout_done == 1) {
const wo_indicator = this.container.querySelector("#wo-indicator");
wo_indicator.classList.remove("pending");
wo_indicator.classList.add("done");
const wo_h1 = this.container.querySelector(".workout-wrapper h1");
wo_h1.classList.add("done");
this.container.classList.add("done");
let buttons = this.container.querySelectorAll("button");
buttons.forEach(function(bttn, bidx) {
bttn.disabled = true;
});
const wo_done = this.text_element("div", "");
wo_done.setAttribute("id", "workout-done");
const wo_done_txt = this.text_element("div", "Workout done!");
wo_done_txt.classList.add("workout-done-txt");
wo_done.appendChild(wo_done_txt);
this.arena.appendChild(wo_done);
}
}
@@ -396,10 +410,10 @@ render_fab() {
const fab_wrapper = document.createElement("div");
fab_wrapper.setAttribute("id", "fab-wrapper");
const fab_regress = this.text_element("button", "<< Regress");
const fab_regress = this.text_element("button", " ");
fab_regress.classList.add("fab-button");
fab_regress.setAttribute("id", "fab-regress");
const fab_advance = this.text_element("button", "Advance >>");
const fab_advance = this.text_element("button", " ");
fab_advance.classList.add("fab-button");
fab_advance.setAttribute("id", "fab-advance");
@@ -427,6 +441,13 @@ render() {
fab_regress.addEventListener('click', function() {
app.handle_action('Button-Regress');
});
window.addEventListener('resize', function() {
// Dimensions
app.vwidth = window.innerWidth;
app.vheight = window.innerHeight;
app.update_debug_info();
});
}
}

65
collection.json Normal file
View File

@@ -0,0 +1,65 @@
{
"name": "Collection of exercises",
"exercises": [
{
"id": 1,
"name": "Leg triads with dumbell",
"count": 10,
"pattern": "each side",
"focus": ["legs"]},
{
"id": 2,
"name": "Forward lunge with dumbell twist",
"count": 10,
"pattern": "each side",
"focus": ["legs"]},
{
"id": 3,
"name": "A and Y core on floor",
"count": 10,
"pattern": "each end",
"focus": ["core"]},
{
"id": 4,
"name": "Bow and arrow pull with bands",
"count": 10,
"pattern": "each side",
"focus": ["arms"]},
{
"id": 5,
"name": "Push ups",
"count": 10,
"pattern": "",
"focus": ["arms", "chest"]},
{
"id": 6,
"name": "Bicep curls",
"count": 10,
"pattern": "each side",
"focus": ["arms"]},
{
"id": 7,
"name": "Russian twists",
"count": 10,
"pattern": "",
"focus": ["core"]},
{
"id": 8,
"name": "Chin up core",
"count": 10,
"pattern": "",
"focus": ["arms", "core"]},
{
"id": 9,
"name": "Tricep curls on the floor",
"count": 15,
"pattern": "",
"focus": ["arms"]},
{
"id": 10,
"name": "Leg curl on floor",
"count": 10,
"pattern": "each leg",
"focus": ["legs"]}
]
}

View File

@@ -5,8 +5,12 @@
<meta name="viewport" content="width=device-width" />
<title>Workout</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=Boldonse&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css" />
<script type="text/javascript" src="ExerciseGenerator.js"></script>
<script type="text/javascript" src="Workout.js"></script>
<script type="text/javascript" src="App.js"></script>

View File

@@ -1,39 +0,0 @@
{
"name": "Daily Build and Burn",
"set": [
{ "name": "Build",
"count": 3,
"exercise": [
{ "name": "Forward lunge with dumbell twist",
"count": 10,
"pattern": "each side"},
{ "name": "A and Y core on floor",
"count": 10,
"pattern": "each end"},
{ "name": "Bow and arrow pull with bands",
"count": 10,
"pattern": "each side"},
{ "name": "Push ups",
"count": 10,
"pattern": ""}
]
},
{ "name": "Burn",
"count": 3,
"exercise": [
{ "name": "Bicep curls",
"count": 10,
"pattern": "each side"},
{ "name": "Russian twists",
"count": 10,
"pattern": ""},
{ "name": "Chin up core",
"count": 10,
"pattern": ""},
{ "name": "Tricep curls on the floor",
"count": 15,
"pattern": ""}
]
}
]
}

131
style.css
View File

@@ -18,30 +18,70 @@
}
html {
font-family: "Iosevka", monospace;
font-family: "Roboto", sans-serif;
font-optical-sizing: auto;
font-size: 16px;
}
/* Dimensions for mobile screen */
.workout-wrapper {
width: 90%;
}
.workout-wrapper.done {
display: none;
}
.workout-wrapper table {
width: 97%;
}
.workout-wrapper input[type=checkbox] {
transform: scale(1.5);
}
/* Styles for larger screens (e.g., tablets and desktops) */
@media (min-width: 1200px) {
body {
font-size: 20px;
}
.workout-wrapper {
width: 1200px;
}
.workout-wrapper table {
width: 90%;
}
}
.workout-wrapper {
width: 60em;
margin: auto;
padding: 1.5em;
}
.workout-wrapper h1 {
font-family: "Boldonse", system-ui;
font-weight: 400;
font-style: normal;
text-align: center;
text-decoration: underline var(--indicator-pending) solid 0.2em;
}
.workout-wrapper h1.done {
text-decoration: underline var(--indicator-done) solid 0.2em;
}
.workout-wrapper table {
margin-left: auto;
margin-right: auto;
margin-top: 2em;
margin-bottom: 1em;
width: 95%;
border-spacing: 0px;
border-collapse: collapse;
box-shadow: 1px 1px 7px var(--table-shadow);
}
.workout-wrapper table caption {
font-size: large;
font-family: "Boldonse", system-ui;
font-weight: 200;
font-style: normal;
font-size: 1.5em;
text-align: left;
padding: 7px 7px 7px 10px;
font-weight: bold;
@@ -74,13 +114,11 @@ html {
}
.workout-wrapper .set-table .col-status {
width: 10%;
width: 20%;
}
.workout-wrapper .set-table .col-name {
width: 60%;
}
.workout-wrapper .set-table .col-pattern-count {
width: 30%;
width: 80%;
font-weight: bold;
}
/* Workout indicator */
@@ -88,9 +126,10 @@ html {
display: inline-block;
white-space: pre-wrap;
width: 1em;
line-height: 1em;
border: thin solid gray;
border-radius: 0.5em;
font-size: 0.3em;
border-radius: 0.7em;
font-size: 0.5em;
vertical-align: middle;
margin-left: 1em;
}
@@ -114,26 +153,84 @@ html {
.workout-wrapper #app_debug_div {
border: thin dotted blue;
color: grey;
font-size: x-small;
}
/* Workout done sheet that covers everything */
#workout-done {
position: fixed;
top: 0%;
left: 0%;
height: 100%;
width: 100%;
background-color: rgba(200,200,200,0.5);
z-index: 2000;
backdrop-filter: blur(0.5em);
}
#workout-done .workout-done-txt {
position: absolute;
top: 45%;
width: 100%;
text-align: center;
font-family: "Boldonse", system-ui;
font-weight: 400;
font-size: xx-large;
color: white;
text-shadow: var(--indicator-done-grade) 0.5em 0.5em 1em;
}
/* Floating Action Buttons */
#fab-wrapper {
position: fixed;
bottom: 20px;
right: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
}
#fab-wrapper .fab-button {
display: inline-block;
white-space: pre-wrap;
border: thin solid gray;
border-radius: 0.5em;
border-radius: 50%;
font-size: 1em;
text-align: center;
vertical-align: middle;
margin: 0.5em;
padding: 0.5em;
margin: 0;
padding: 0;
cursor: pointer;
}
#fab-wrapper #fab-regress {
border: 0.4em solid gray;
width: 3em;
height: 3em;
}
#fab-wrapper #fab-regress:after {
content: '';
display: inline-block;
width: 0.8em;
height: 0.8em;
margin-top: 0.2em;
margin-left: 0.2em;
border-top: 0.4em solid gray;
border-right: 0.4em solid gray;
transform: rotate(-135deg);
}
#fab-wrapper #fab-advance {
border: 0.4em solid gray;
width: 4em;
height: 4em;
margin-left: 1em;
}
#fab-wrapper #fab-advance:after {
content: '';
display: inline-block;
width: 1.2em;
height: 1.2em;
margin-bottom: -0.2em;
margin-right: 0.6em;
border-top: 0.4em solid gray;
border-right: 0.4em solid gray;
transform: rotate(45deg);
}