- 11Sep2024
- Added a halfway beep during long segments.
August 21, 2024 at 16:25
What we have here is a minimal yet highly functional exercise timer. I’ll be using this as a replacement for tracking sets and their timing. It has the ability to quickly customize a routine or build on its basic functionality with whatever we want. This is currently a work in progress, but I’ll continue to update this page as functionality is added.
I’ll quickly run through what this does - it’s basic HTML, JS, and CSS. It’s easy to read and modify, and areas that should be modified will be called out. If you want to take it for a test drive, have a look at files.mcwain.net/exercise. It’s mobile-friendly, so feel free to use it on your phone or tablet.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workout Timer</title>
<link rel="manifest" href="/manifest.json">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<div id="currentExercise" class="current-exercise">Get Ready!</div>
<div id="nextExercise" class="next-exercise"></div>
<div id="timer" class="timer">00:00</div>
<ul id="exerciseList" class="exercise-list"></ul>
<div class="controls">
<button id="startStopBtn">Start</button>
<button id="resetBtn">Reset</button>
</div>
</div>
<audio id="beepSound" src="notification.wav"></audio>
<audio id="countdownBeep" src="ding.mp3"></audio>
<script src="script.js"></script>
</body>
</html>
script.js (below) is where you want to edit your workout routine. Copy an entry that looks like the line below, then change the title and duration. When the page is reloaded, it’ll populate the list, inform you of what is up next, and countdown to 00:00 from the duration (in seconds). A sound plays when the timer is complete, but there’s also a countdown beep during the seconds prior.
“Arnold Presses”, duration: 30 },`
Just make sure when you edit or add lines in the workout list, the last item doesn’t end with a comma ,
. I’m proudly not a JavaScript developer as we certainly don’t get along very well. If you’re curious how the below works or have complaints because I did something against the JavaScript programmer’s charter, feel free to reach out on Twitter. There won’t be any deep dive here. I enjoy what sanity remains in this brain of mine.
script.js
const exercises = [
//{ name: "Stretch", duration: 300 },
{ name: "Countdown", duration: 15 },
{ name: "Jumping Jacks", duration: 30 },
{ name: "Rest", duration: 10 },
{ name: "Shoulder Raise Lateral", duration: 30 },
{ name: "Rest", duration: 10 },
{ name: "Shoulder Raise Front", duration: 30},
{ name: "Rest", duration: 10 },
{ name: "Bicep Curl Left", duration: 45 },
{ name: "Rest", duration: 10 },
{ name: "Bicep Curl Right", duration: 45 },
{ name: "Rest", duration: 10 },
{ name: "Tricep Dips", duration: 30 },
{ name: "Rest", duration: 10 },
{ name: "Butterflies", duration: 30 },
{ name: "Rest", duration: 10 },
{ name: "Arnold Presses", duration: 30 },
{ name: "Rest", duration: 30 },
{ name: "Situps", duration: 50 },
{ name: "Cycle Complete!", duration: 20 }
];
let currentExerciseIndex = 0;
let timer;
let isRunning = false;
const timerElement = document.getElementById("timer");
const currentExerciseElement = document.getElementById("currentExercise");
const exerciseListElement = document.getElementById("exerciseList");
const startStopBtn = document.getElementById("startStopBtn");
const resetBtn = document.getElementById("resetBtn");
const beepSound = document.getElementById("beepSound");
beepSound.load();
const countdownBeep = document.getElementById("countdownBeep");
countdownBeep.load();
const nextExerciseElement = document.getElementById("nextExercise");
// idk if this works, but let's try!
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered with scope:', registration.scope);
})
.catch(error => {
console.log('Service Worker registration failed:', error);
});
}
function updateTimer(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
timerElement.textContent = `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
if (seconds <= 6 && seconds % 2 === 0 && seconds > 0) {
countdownBeep.play();
}
}
function startTimer(duration, callback) {
let seconds = duration;
updateTimer(seconds);
timer = setInterval(() => {
seconds--;
updateTimer(seconds);
if (seconds <= 0) {
clearInterval(timer);
beepSound.play();
callback();
}
}, 1000);
}
function startExercise(index) {
if (index >= exercises.length) {
currentExerciseElement.textContent = "Workout Complete!";
nextExerciseElement.textContent = ""; // Clear next exercise text
isRunning = false;
startStopBtn.textContent = "Start";
return;
}
currentExerciseIndex = index;
const exercise = exercises[currentExerciseIndex];
currentExerciseElement.textContent = exercise.name;
// Update the next exercise
const nextExercise = exercises[currentExerciseIndex + 1];
nextExerciseElement.textContent = nextExercise ? `Next: ${nextExercise.name}` : "";
highlightCurrentExercise();
startTimer(exercise.duration, () => {
startExercise(currentExerciseIndex + 1);
});
}
function highlightCurrentExercise() {
exerciseListElement.innerHTML = ''; // Clear the list
const start = Math.max(currentExerciseIndex - 2, 0);
const end = Math.min(currentExerciseIndex + 3, exercises.length);
for (let i = start; i < end; i++) {
const li = document.createElement("li");
li.textContent = `${exercises[i].name} (${exercises[i].duration}s)`;
if (i === currentExerciseIndex) {
li.classList.add("current");
}
li.addEventListener("click", () => {
if (!isRunning) {
currentExerciseIndex = i;
highlightCurrentExercise();
}
});
exerciseListElement.appendChild(li);
}
}
function startStop() {
if (isRunning) {
clearInterval(timer);
isRunning = false;
startStopBtn.textContent = "Start";
} else {
isRunning = true;
startStopBtn.textContent = "Stop";
startExercise(currentExerciseIndex);
}
}
function reset() {
clearInterval(timer);
currentExerciseIndex = 0;
updateTimer(0);
currentExerciseElement.textContent = "Get Ready!";
highlightCurrentExercise();
isRunning = false;
startStopBtn.textContent = "Start";
}
function initExerciseList() {
highlightCurrentExercise();
}
startStopBtn.addEventListener("click", startStop);
resetBtn.addEventListener("click", reset);
initExerciseList();
styles.css is the file that manages how everything looks. Back in my day, we added formatting directly to the html file, and we liked it! Now, we have fancy stylesheets that allow entire blocks of html to be formatted however we want. If you look back at index.html
, the CSS file was added in <head>
as <link rel="stylesheet" href="styles.css">
.
This is where we apply styles to classes, divs, and page elements. For example, we can address buttons as simply:
button {color:#000, margin: 1px;}
or an entire html class with:
.container {width: 90%; background: #000;}
The more you get into CSS as a designer, the more you’ll find things like this funny:
styles.css
body {
font-family: Arial, sans-serif;
background-image: url('wallpaper.jpg');
background-size: repeat;
background-position: center;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
overflow: hidden;
}
.container {
text-align: center;
width: 90%;
max-width: 800px;
background: rgba(0, 0, 0, 0.6);
padding: 20px;
border-radius: 10px;
box-sizing: border-box;
}
.current-exercise {
font-size: 10vw;
margin-bottom: 20px;
font-weight: bold;
color: #61dafb;
}
.next-exercise {
font-size: 5vw;
color: #ffffff;
margin-bottom: 10px;
}
.timer {
font-size: 8vw;
margin-bottom: 20px;
font-weight: bold;
}
.exercise-list {
list-style: none;
padding: 0;
margin-bottom: 20px;
max-height: 250px;
overflow-y: auto;
}
.exercise-list li {
padding: 10px;
margin: 5px 0;
border: 1px solid #ffffff;
cursor: pointer;
opacity: 0.7;
}
.exercise-list li.current {
background-color: #61dafb;
color: #000;
font-weight: bold;
opacity: 1;
}
.controls {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
font-size: 4vw;
cursor: pointer;
border: none;
background-color: #61dafb;
color: #000;
border-radius: 5px;
margin: 5px;
flex: 1;
}
button:hover {
background-color: #21a1f1;
}
@media (min-width: 768px) {
.current-exercise {
font-size: 8em;
}
.next-exercise {
font-size: 2em;
}
.timer {
font-size: 5em;
}
button {
font-size: 1em;
}
}
That’s it! Pretty simple stuff. The phone app I was previously using was real difficult to manage long routines, and was restricted in setting times. This way, we control the granularity and how it functions.
Future updates:
sound.load();
seems to help.Discussion here: https://x.com/cmcwain/status/1826420286608306302