add weekends left calculator
This commit is contained in:
parent
427ba433db
commit
dc791ae47c
5 changed files with 477 additions and 0 deletions
255
static/js/weekendsLeft.js
Normal file
255
static/js/weekendsLeft.js
Normal 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);
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue