add weekends left calculator

This commit is contained in:
Fabian Montero 2026-04-13 17:00:56 -06:00
parent 427ba433db
commit dc791ae47c
Signed by: fabian
GPG key ID: 3EDA9AE3937CCDE3
5 changed files with 477 additions and 0 deletions

255
static/js/weekendsLeft.js Normal file
View file

@ -0,0 +1,255 @@
(function() {
'use strict';
var form = document.getElementById('weekends-form');
var birthYearInput = document.getElementById('birth-year');
var deathYearInput = document.getElementById('death-year');
var resultsDiv = document.getElementById('weekends-results');
var percentageRemainingEl = document.getElementById('percentage-remaining');
var usedCountEl = document.getElementById('used-count');
var totalCountEl = document.getElementById('total-count');
var wakingHoursEl = document.getElementById('waking-hours');
var gridEl = document.getElementById('weekends-grid');
var downloadBtn = document.getElementById('download-btn');
var lastCalculation = null;
function countWeekends(startDate, endDate) {
var count = 0;
var current = new Date(startDate);
current.setHours(0, 0, 0, 0);
while (current <= endDate) {
if (current.getDay() === 6) {
count++;
}
current.setDate(current.getDate() + 1);
}
return count;
}
function formatNumber(num) {
return num.toLocaleString();
}
function renderGrid(usedCount, remainingCount, birthYear) {
gridEl.innerHTML = '';
var existingMilestones = document.querySelector('.grid-milestones');
if (existingMilestones) {
existingMilestones.remove();
}
var total = usedCount + remainingCount;
var maxCells = 5000;
var scaled = total > maxCells;
var currentCellIndex;
var cellCount;
if (scaled) {
var scaleFactor = maxCells / total;
var usedToRender = Math.round(usedCount * scaleFactor);
var remainingToRender = maxCells - usedToRender;
currentCellIndex = usedToRender > 0 ? usedToRender - 1 : 0;
cellCount = maxCells;
for (var i = 0; i < usedToRender; i++) {
var cell = document.createElement('div');
cell.className = 'grid-cell used';
if (i === currentCellIndex) {
cell.className += ' current';
}
gridEl.appendChild(cell);
}
for (var j = 0; j < remainingToRender; j++) {
var cell = document.createElement('div');
cell.className = 'grid-cell';
gridEl.appendChild(cell);
}
var overflow = document.createElement('div');
overflow.className = 'grid-overflow';
overflow.textContent = '(' + formatNumber(total) + ' total weekends)';
gridEl.appendChild(overflow);
} else {
currentCellIndex = usedCount > 0 ? usedCount - 1 : 0;
cellCount = total;
for (var i = 0; i < usedCount; i++) {
var cell = document.createElement('div');
cell.className = 'grid-cell used';
if (i === currentCellIndex) {
cell.className += ' current';
}
gridEl.appendChild(cell);
}
for (var j = 0; j < remainingCount; j++) {
var cell = document.createElement('div');
cell.className = 'grid-cell';
gridEl.appendChild(cell);
}
}
var milestones = [20, 40, 60, 80];
var milestoneText = milestones.map(function(age) {
return 'Age ' + age + ': ' + (birthYear + age);
}).join(' · ');
var milestonesEl = document.createElement('div');
milestonesEl.className = 'grid-milestones';
milestonesEl.textContent = milestoneText;
gridEl.parentNode.appendChild(milestonesEl);
}
function generateImage() {
if (!lastCalculation) return;
var fontUrl = '/fonts/JetBrainsMonoNLNerdFont-Regular.ttf';
var font = new FontFace('JetBrainsMono', 'url(' + fontUrl + ')');
font.load().then(function(loadedFont) {
document.fonts.add(loadedFont);
renderImage();
}).catch(function() {
renderImage();
});
function renderImage() {
var data = lastCalculation;
var canvasWidth = 800;
var cellSize = 6;
var cellGap = 2;
var padding = 30;
var headerHeight = 100;
var footerHeight = 60;
var total = data.usedCount + data.remainingCount;
var maxCells = 5000;
var scaled = total > maxCells;
var cellCount = scaled ? maxCells : total;
var cellsPerRow = Math.floor((canvasWidth - 2 * padding) / (cellSize + cellGap));
var rows = Math.ceil(cellCount / cellsPerRow);
var gridHeight = rows * (cellSize + cellGap);
var canvasHeight = headerHeight + gridHeight + footerHeight + padding;
var canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
var ctx = canvas.getContext('2d');
var fontFamily = 'JetBrainsMono, monospace';
var bgColor = '#131a21';
var textColor = getComputedStyle(document.documentElement).getPropertyValue('--text-color').trim() || '#333333';
var primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#007bff';
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.fillStyle = primaryColor;
ctx.font = 'bold 36px ' + fontFamily;
ctx.fillText(data.percentageRemaining + '% weekends remaining', padding, padding + 30);
ctx.fillStyle = textColor;
ctx.font = '14px ' + fontFamily;
var summary = formatNumber(data.usedCount) + ' / ' + formatNumber(data.totalCount) +
' weekends · ' + formatNumber(data.wakingHours) + ' waking hours remaining (assuming 8h sleep on saturdays)';
ctx.fillText(summary, padding, padding + 55);
var scaleFactor = scaled ? maxCells / total : 1;
var usedToRender = scaled ? Math.round(data.usedCount * scaleFactor) : data.usedCount;
var currentCellIndex = usedToRender > 0 ? usedToRender - 1 : 0;
for (var i = 0; i < cellCount; i++) {
var col = i % cellsPerRow;
var row = Math.floor(i / cellsPerRow);
var x = padding + col * (cellSize + cellGap);
var y = headerHeight + row * (cellSize + cellGap);
var isUsed = i < usedToRender;
ctx.fillStyle = primaryColor;
ctx.globalAlpha = isUsed ? 0.3 : 1;
ctx.beginPath();
ctx.roundRect(x, y, cellSize, cellSize, 1);
ctx.fill();
ctx.globalAlpha = 1;
if (i === currentCellIndex) {
ctx.strokeStyle = textColor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(x - 2, y - 2, cellSize + 4, cellSize + 4, 2);
ctx.stroke();
}
}
ctx.fillStyle = textColor;
ctx.font = '20px ' + fontFamily;
var subtitle = 'posixlycorrect.com/lib/weekends-left';
ctx.fillText(subtitle, padding, canvasHeight - 20);
var link = document.createElement('a');
link.download = 'weekends-left.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
}
function calculate() {
var birthYear = parseInt(birthYearInput.value, 10);
var deathYear = parseInt(deathYearInput.value, 10);
if (!birthYear || !deathYear) {
resultsDiv.style.display = 'none';
return;
}
if (deathYear <= birthYear) {
resultsDiv.style.display = 'none';
return;
}
var birthDate = new Date(birthYear, 0, 1);
var deathDate = new Date(deathYear, 11, 31);
var today = new Date();
today.setHours(0, 0, 0, 0);
var usedWeekends = countWeekends(birthDate, today);
var remainingWeekends = countWeekends(today, deathDate);
var totalWeekends = usedWeekends + remainingWeekends;
var percentage = Math.round((usedWeekends / totalWeekends) * 100);
var wakingHours = remainingWeekends * 40;
var percentageRemaining = 100 - percentage;
lastCalculation = {
usedCount: usedWeekends,
remainingCount: remainingWeekends,
totalCount: totalWeekends,
percentage: percentage,
percentageRemaining: percentageRemaining,
wakingHours: wakingHours,
birthDate: birthDate
};
percentageRemainingEl.textContent = percentageRemaining + '%';
usedCountEl.textContent = formatNumber(usedWeekends);
totalCountEl.textContent = formatNumber(totalWeekends);
wakingHoursEl.textContent = formatNumber(wakingHours) + ' waking hours remaining, assuming 8h of sleep on saturdays';
renderGrid(usedWeekends, remainingWeekends, birthYear);
resultsDiv.style.display = 'block';
}
birthYearInput.addEventListener('input', calculate);
deathYearInput.addEventListener('input', calculate);
form.addEventListener('submit', function(e) {
e.preventDefault();
calculate();
});
downloadBtn.addEventListener('click', generateImage);
})();