Skip to content

Instantly share code, notes, and snippets.

@RayMPerry
Created March 8, 2023 18:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RayMPerry/fa57ee365647b4b85262897da679ca8f to your computer and use it in GitHub Desktop.
Save RayMPerry/fa57ee365647b4b85262897da679ca8f to your computer and use it in GitHub Desktop.
Subathon Timer
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Timer</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=Oswald:wght@500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/tachyons@4.12.0/css/tachyons.min.css"/>
<style>
* {
font-family: 'Oswald', sans-serif;
}
#timer-display, #subs-display {
text-shadow: 4px 4px #000;
}
.event:last-child {
border: none !important;
}
.active-break-button {
background-color: #ff4136 !important;
color: #fff !important;
}
.timer-finished {
visibility: hidden;
}
</style>
</head>
<body class="bg-green flex flex-column w-100 h-100">
<div class="row flex w-100">
<div class="timer flex flex-column tc items-center w-50 ma2 ph2 pv3 ">
<span id="timer-display" class="f-headline white text-top">00:00:00</span>
<span id="subs-display" class="f-headline white text-top">
<span id="sub-count-display">0</span>/<span id="sub-goal-display">0</span>
</span>
<div class="event-feed flex flex-column w-50 ba bw2 b--black br3 bg-near-white mt4">
<p class="f3 b bb b--black bw1 pa2 mv0 white bg-near-black">EVENT FEED</p>
</div>
</div>
<div class="buttons ma2 ph2 pv3 tc items-center justify-center w-50 br3 bg-near-white ba bw2 b--black">
<div class="flex justify-center">
<button id="undo-button" class="ph3 pv2 ma1 ba bw2 bg-navy white br4 b--black w-50 outline-0 usn pointer">
<span class="f3">UNDO</span>
</button>
</div>
<div class="big-controls flex">
<button id="pause-button" class="ph3 pv2 ma1 ba bw2 bg-washed-blue br4 b--black w-25 outline-0 usn pointer">
<span class="f3">PAUSE</span>
</button>
<button id="break-10-button" class="ph3 pv2 ma1 ba bw2 bg-lightest-blue br4 b--black w-25 outline-0 usn pointer">
<span class="button-text f3">10 MIN BREAK</span>
<span class="break-display f3 dn"></span>
</button>
<button id="break-15-button" class="ph3 pv2 ma1 ba bw2 bg-light-blue br4 b--black w-25 outline-0 usn pointer">
<span class="button-text f3">15 MIN BREAK</span>
<span class="break-display f3 dn"></span>
</button>
<button id="break-20-button" class="ph3 pv2 ma1 ba bw2 bg-blue br4 b--black w-25 outline-0 usn pointer">
<span class="button-text f3">20 MIN BREAK</span>
<span class="break-display f3 dn"></span>
</button>
</div>
<div class="time-control">
<p class="f3 b bb b--black bw1 pa2">TIMER CONTROL</p>
<div class="time-form flex justify-around mt2">
<input id="hours-time" type="text" placeholder="H" class="w-20 outline-0 mr1" />
<input id="minutes-time" type="text" placeholder="M" class="w-20 outline-0 mr1" />
<input id="seconds-time" type="text" placeholder="S" class="w-20 outline-0 mr1" />
<button id="set-timer" class="w-20">Set Timer</button>
</div>
<div class="buttons mv3">
<button id="add-hour">Add 1 Hour</button>
<button id="add-five-minutes">Add 5 Minutes</button>
<button id="add-one-minute">Add 1 Minute</button>
</div>
<div class="seconds-form flex justify-around mt2">
<input id="seconds-input" type="text" placeholder="Enter # of seconds" class="w-60 outline-0" />
<button id="add-seconds" class="w-30">Add Seconds</button>
</div>
</div>
<div class="flex w-100">
<div class="bits-donations w-50">
<p class="f3 b bb b--black bw1 pa2">BITS/DONATIONS</p>
<div class="bits-form">
<input id="bits-input" type="text" placeholder="Enter # of bits" value="100"/>
<button id="add-bits">Remove Time (Bits)</button>
</div>
<div class="dollars-form mt2">
<input id="dollars-input" type="text" placeholder="Enter # of dollars" value="1"/>
<button id="add-dollars">Remove Time (Dollars)</button>
</div>
</div>
<div class="sub-count w-50">
<p class="f3 b bb b--black bw1 pa2">SUB COUNT CONTROL</p>
<div class="sub-count-form">
<input id="sub-count-input" type="text" placeholder="Enter sub count" />
<button id="set-sub-count">Set Sub Count</button>
</div>
<div class="sub-goal-form mt2">
<input id="sub-goal-input" type="text" placeholder="Enter goal" />
<button id="set-goal">Set Sub Goal</button>
</div>
</div>
</div>
<div class="subs">
<p class="f3 b bb b--black bw1 pa2">SUBS</p>
<div class="add-multiple-subs-form mt2">
<input id="multiple-subs-input" type="text" placeholder="Enter # of subs" class="w-40 outline-0 mr1" />
<button id="add-multiple-tier-1-subs">Add as Tier 1 Subs</button>
<button id="add-multiple-tier-2-subs">Add as Tier 2 Subs</button>
<button id="add-multiple-tier-3-subs">Add as Tier 3 Subs</button>
</div>
</div>
<div class="resubs">
<p class="f3 b bb b--black bw1 pa2">RESUBS</p>
<div class="add-multiple-resubs-form mt2">
<input id="multiple-resubs-input" type="text" placeholder="Enter # of resubs" class="w-40 outline-0 mr1" />
<button id="add-multiple-tier-1-resubs">Add as Tier 1 Resubs</button>
<button id="add-multiple-tier-2-resubs">Add as Tier 2 Resubs</button>
<button id="add-multiple-tier-3-resubs">Add as Tier 3 Resubs</button>
</div>
</div>
<div class="flex flex-column tc items-center justify-center w-100 mt4">
<p class="f3">QUICK CALCULATIONS (in seconds)</p>
<div class="tiers flex b bt b--black bw1 pa2">
<div class="fives flex flex-column tl pr2">
<span class="f4 pa2">5 T1 Subs: <span id="five-tier-1-subs"></span></span>
<span class="f4 pa2">5 T2 Subs: <span id="five-tier-2-subs"></span></span>
<span class="f4 pa2">5 T3 Subs: <span id="five-tier-3-subs"></span></span>
</div>
<div class="fives flex flex-column tl pr2">
<span class="f4 pa2">5 T1 Resubs: <span id="five-tier-1-resubs"></span></span>
<span class="f4 pa2">5 T2 Resubs: <span id="five-tier-2-resubs"></span></span>
<span class="f4 pa2">5 T3 Resubs: <span id="five-tier-3-resubs"></span></span>
</div>
<div class="twenty-fives flex flex-column tl pl2">
<span class="f4 pa2">25 T1 Subs: <span id="twenty-five-tier-1-subs"></span></span>
<span class="f4 pa2">25 T2 Subs: <span id="twenty-five-tier-2-subs"></span></span>
<span class="f4 pa2">25 T3 Subs: <span id="twenty-five-tier-3-subs"></span></span>
</div>
<div class="twenty-fives flex flex-column tl pl2">
<span class="f4 pa2">25 T1 Resubs: <span id="twenty-five-tier-1-resubs"></span></span>
<span class="f4 pa2">25 T2 Resubs: <span id="twenty-five-tier-2-resubs"></span></span>
<span class="f4 pa2">25 T3 Resubs: <span id="twenty-five-tier-3-resubs"></span></span>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.5/plugin/duration.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.5/plugin/relativeTime.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.5/dayjs.min.js"></script>
<script>
(() => {
const SECONDS = {
BITS: 60,
DOLLARS: 60,
TIER_1_SUB: 120,
TIER_1_RESUB: 120,
TIER_2_SUB: 180,
TIER_2_RESUB: 180,
TIER_3_SUB: 240,
TIER_3_RESUB: 240
};
const MAX_NUMBER_OF_CHANGES = 25;
let timerInterval = null;
let blinkInterval = null;
let isPaused = false;
let breakTime = null;
let breakTimeDisplay = null;
let breakTimeInterval = null;
dayjs.extend(dayjs_plugin_duration);
dayjs.extend(dayjs_plugin_relativeTime);
const changes = [];
const pushChange = (change, numberOfSubs) => {
changes.push({ time: change, subs: numberOfSubs });
const newChange = document.createElement("span");
const verb = Math.sign(change.asMinutes()) === 1 ? 'Added' : 'Removed';
const minutesToDisplay = change.asMinutes() * (1 + (-2 * (Math.sign(change.asMinutes()) === -1)))
newChange.textContent = `${verb} ${minutesToDisplay} minute${minutesToDisplay !== 1 ? 's' : ''}`;
newChange.classList.add("event", "pa2", "bb", "bw1", "b--gray");
const eventFeed = document.querySelector(".event-feed");
if (eventFeed) eventFeed.appendChild(newChange);
if (changes.length >= MAX_NUMBER_OF_CHANGES) changes.unshift();
}
const undoChange = () => {
if (changes.length < 1) return;
let { time: timeChange, subs } = changes.pop();
remainingTime = remainingTime.subtract(timeChange);
setSubCount(currentSubCount - subs);
document.querySelector(".event:last-child").remove();
setTimeInSeconds(0);
}
const previousRemainingTime = localStorage.getItem('remainingTime') || 'PT7H50M00.000S';
let currentSubCount = parseInt(localStorage.getItem('currentSubCount')) || 1032;
let subGoal = parseInt(localStorage.getItem('subGoal')) || 1200;
const setSubCount = newSubCount => {
currentSubCount = newSubCount;
localStorage.setItem('currentSubCount', currentSubCount);
document.getElementById('sub-count-display').textContent = currentSubCount;
};
const setSubGoal = newSubGoal => {
subGoal = newSubGoal;
localStorage.setItem('subGoal', subGoal);
document.getElementById('sub-goal-display').textContent = subGoal;
};
setSubCount(currentSubCount);
setSubGoal(subGoal);
const defaultTime = dayjs().add(99, 'hour').add(59, 'minute').add(59, 'second');
let remainingTime = dayjs.duration(previousRemainingTime || defaultTime.diff(dayjs()));
let remainingTimeDisplay = `${Math.floor(remainingTime.asHours())}` + remainingTime.format(':mm:ss');
document.getElementById('timer-display').textContent = remainingTimeDisplay;
const startBreak = numberOfMinutes => {
if (breakTimeInterval) return;
breakTime = dayjs.duration(dayjs().add(numberOfMinutes, 'minutes').diff(dayjs()));
breakTimeDisplay = breakTime.format('mm:ss');
const element = document.getElementById(`break-${numberOfMinutes}-button`);
element.classList.add("active-break-button");
element.querySelector('.button-text').classList.add("dn");
element.querySelector('.break-display').classList.remove("dn");
element.querySelector('.break-display').textContent = breakTimeDisplay;
breakTimeInterval = setInterval(() => {
breakTime = breakTime.subtract(dayjs.duration(1, 's'));
breakTimeDisplay = breakTime.format('mm:ss');
element.querySelector('.break-display').textContent = breakTimeDisplay;
if (breakTime.asSeconds() <= 0) toggleTimer(false);
}, 1000);
};
const startBreak10Mins = () => {
toggleTimer(true);
startBreak(10);
};
const startBreak15Mins = () => {
toggleTimer(true);
startBreak(15);
};
const startBreak20Mins = () => {
toggleTimer(true);
startBreak(20);
};
const endBreak = () => {
if (!breakTimeInterval) return;
const element = document.querySelector(".active-break-button");
breakTime = null;
breakTimeDisplay = "";
element.querySelector('.button-text').classList.remove("dn");
element.querySelector('.break-display').classList.add("dn");
element.querySelector('.break-display').textContent = breakTimeDisplay;
breakTimeInterval = clearInterval(breakTimeInterval) || null;
element.classList.remove("active-break-button");
};
const setTime = unit => (numberOfMinutes, numberOfSubs) => {
if (numberOfMinutes) {
let additionalTime = dayjs.duration(numberOfMinutes, unit);
if (remainingTime.add(additionalTime).asSeconds() >= 0) {
remainingTime = remainingTime.add(additionalTime);
pushChange(additionalTime, numberOfSubs || 0);
}
}
remainingTimeDisplay = `${Math.floor(remainingTime.asHours())}` + remainingTime.format(':mm:ss');
document.getElementById('timer-display').textContent = remainingTimeDisplay;
if (!timerInterval && remainingTime.asSeconds() > 1) {
timerInterval = startTimer();
clearInterval(blinkInterval);
blinkInterval = null;
}
};
const setTimeInMinutes = setTime('m');
const setTimeInSeconds = setTime('s');
const startTimer = () => setInterval(() => {
if (remainingTime.asSeconds() <= 0) {
clearInterval(timerInterval);
timerInterval = null;
blinkInterval = setInterval(() => {
document.getElementById("timer-display").classList.toggle('timer-finished');
}, 500)
return;
}
remainingTime = remainingTime.subtract(dayjs.duration(1, 's'));
localStorage.setItem('remainingTime', remainingTime.toISOString());
setTimeInSeconds(0);
}, 1000);
const toggleTimer = isPausedOverride => {
const timerElem = document.getElementById("timer-display");
const pauseButton = document.querySelector("#pause-button > .f3");
const timerColors = ["white", "red"];
timerElem.classList.remove(timerColors[+isPaused]);
if (isPausedOverride != null) {
isPaused = isPausedOverride;
} else {
isPaused = !isPaused;
}
pauseButton.textContent = `${isPaused ? 'UN' : ''}PAUSE`;
timerElem.classList.add(timerColors[+isPaused]);
if (isPaused) {
clearInterval(timerInterval);
} else {
timerInterval = startTimer();
endBreak();
}
};
timerInterval = startTimer();
const addBits = numberOfBits => setTimeInSeconds(SECONDS.BITS * Math.floor(numberOfBits / 100));
const addDollars = numberOfDollars => setTimeInSeconds(SECONDS.DOLLARS * Math.floor(numberOfDollars));
const removeBits = numberOfBits => setTimeInSeconds(-1 * SECONDS.BITS * Math.floor(numberOfBits / 100));
const removeDollars = numberOfDollars => setTimeInSeconds(-1 * SECONDS.DOLLARS * Math.floor(numberOfDollars));
document.getElementById('undo-button').addEventListener('click', () => undoChange());
document.getElementById('pause-button').addEventListener('click', () => toggleTimer());
document.getElementById('add-hour').addEventListener('click', () => setTimeInMinutes(60));
document.getElementById('add-five-minutes').addEventListener('click', () => setTimeInMinutes(5));
document.getElementById('add-one-minute').addEventListener('click', () => setTimeInMinutes(1));
document.getElementById('add-seconds').addEventListener('click', () => {
const numberOfSeconds = document.getElementById('seconds-input').value || '0';
setTimeInSeconds(parseInt(numberOfSeconds));
});
document.getElementById('add-bits').addEventListener('click', () => {
const numberOfBits = document.getElementById('bits-input').value || '0';
removeBits(parseInt(numberOfBits));
});
document.getElementById('add-dollars').addEventListener('click', () => {
const numberOfDollars = document.getElementById('dollars-input').value || '0';
removeDollars(parseInt(numberOfDollars));
});
document.getElementById('set-sub-count').addEventListener('click', () => {
const numberOfSubs = document.getElementById('sub-count-input').value || '0';
setSubCount(parseInt(numberOfSubs));
});
document.getElementById('set-goal').addEventListener('click', () => {
const numberOfSubs = document.getElementById('sub-goal-input').value || '0';
setSubGoal(parseInt(numberOfSubs));
});
const setTimer = () => {
const hours = document.getElementById('hours-time').value || '0';
const minutes = document.getElementById('minutes-time').value || '0';
const seconds = document.getElementById('seconds-time').value || '0';
remainingTime = dayjs.duration(`PT${hours}H${minutes}M${seconds}.000S`);
setTimeInSeconds(0);
};
document.getElementById('set-timer').addEventListener('click', setTimer);
const addMultipleSubs = (interval, numberOfSubs) => {
setSubCount(currentSubCount + numberOfSubs);
setTimeInSeconds(interval * numberOfSubs, numberOfSubs);
};
document.getElementById('add-multiple-tier-1-subs').addEventListener('click', () => {
const numberOfSubs = document.getElementById('multiple-subs-input').value || '0';
addMultipleSubs(SECONDS.TIER_1_SUB, parseInt(numberOfSubs));
});
document.getElementById('add-multiple-tier-2-subs').addEventListener('click', () => {
const numberOfSubs = document.getElementById('multiple-subs-input').value || '0';
addMultipleSubs(SECONDS.TIER_2_SUB, parseInt(numberOfSubs));
});
document.getElementById('add-multiple-tier-3-subs').addEventListener('click', () => {
const numberOfSubs = document.getElementById('multiple-subs-input').value || '0';
addMultipleSubs(SECONDS.TIER_3_SUB, parseInt(numberOfSubs));
});
document.getElementById('add-multiple-tier-1-resubs').addEventListener('click', () => {
const numberOfSubs = document.getElementById('multiple-resubs-input').value || '0';
addMultipleSubs(SECONDS.TIER_1_RESUB, parseInt(numberOfSubs));
});
document.getElementById('add-multiple-tier-2-resubs').addEventListener('click', () => {
const numberOfSubs = document.getElementById('multiple-resubs-input').value || '0';
addMultipleSubs(SECONDS.TIER_2_RESUB, parseInt(numberOfSubs));
});
document.getElementById('add-multiple-tier-3-resubs').addEventListener('click', () => {
const numberOfSubs = document.getElementById('multiple-resubs-input').value || '0';
addMultipleSubs(SECONDS.TIER_3_RESUB, parseInt(numberOfSubs));
});
document.getElementById('break-10-button').addEventListener('click', () => startBreak10Mins());
document.getElementById('break-15-button').addEventListener('click', () => startBreak15Mins());
document.getElementById('break-20-button').addEventListener('click', () => startBreak20Mins());
for (let tier = 1; tier < 4; tier++) {
document.getElementById(`five-tier-${tier}-subs`).textContent = SECONDS[`TIER_${tier}_SUB`] * 5;
document.getElementById(`five-tier-${tier}-resubs`).textContent = SECONDS[`TIER_${tier}_RESUB`] * 5;
document.getElementById(`twenty-five-tier-${tier}-subs`).textContent = SECONDS[`TIER_${tier}_SUB`] * 25;
document.getElementById(`twenty-five-tier-${tier}-resubs`).textContent = SECONDS[`TIER_${tier}_RESUB`] * 25;
}
})(document);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment