Skip to content

Instantly share code, notes, and snippets.

@proweb
Created January 14, 2026 15:00
Show Gist options
  • Select an option

  • Save proweb/279b2ceadff5a0946a7f1c587f81b6ce to your computer and use it in GitHub Desktop.

Select an option

Save proweb/279b2ceadff5a0946a7f1c587f81b6ce to your computer and use it in GitHub Desktop.
Pinarik 80 years
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memento Mori - Life Planner</title>
<style>
:root {
--bg-color: #f4f4f9;
--text-color: #333;
--grid-gap: 2px;
--box-size: 14px;
--box-empty: #e0e0e0;
--box-hover: #d0d0d0;
--box-checked: #2c3e50; /* Dark blue/grey for "lived" weeks */
--accent-color: #e74c3c;
--font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-family);
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
min-height: 100vh;
}
header {
text-align: center;
margin-bottom: 20px;
max-width: 800px;
}
h1 {
font-weight: 300;
margin-bottom: 10px;
letter-spacing: 1px;
}
.controls {
background: white;
padding: 15px 25px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
justify-content: center;
margin-bottom: 20px;
width: 100%;
max-width: 900px;
}
.input-group {
display: flex;
align-items: center;
gap: 10px;
}
label {
font-weight: 600;
font-size: 0.9rem;
}
input[type="date"] {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
}
button {
padding: 8px 16px;
cursor: pointer;
border: none;
border-radius: 4px;
font-weight: 600;
transition: background 0.2s;
font-family: inherit;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-primary:hover { background-color: #2980b9; }
.btn-danger {
background-color: #fff;
color: var(--accent-color);
border: 1px solid var(--accent-color);
}
.btn-danger:hover { background-color: #fff5f5; }
.stats-bar {
width: 100%;
max-width: 900px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: #666;
}
/* Progress Bar Styling */
.progress-container {
width: 100%;
max-width: 900px;
height: 6px;
background-color: #ddd;
border-radius: 3px;
margin-bottom: 20px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: var(--box-checked);
width: 0%;
transition: width 0.3s ease;
}
/* Main Grid Container */
.grid-container {
display: grid;
/* 52 columns for 52 weeks */
grid-template-columns: repeat(52, var(--box-size));
gap: var(--grid-gap);
padding: 10px;
background: white;
border-radius: 4px;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
overflow-x: auto; /* Allow horizontal scroll on small screens */
max-width: 100%;
}
/* Year Labels on the left side */
.grid-wrapper {
display: flex;
align-items: flex-start;
gap: 10px;
}
.year-labels {
display: flex;
flex-direction: column;
/* Match row height of boxes + gap */
height: calc(80 * (var(--box-size) + var(--grid-gap)));
font-size: 10px;
color: #888;
text-align: right;
padding-top: 1px; /* align with first box */
user-select: none;
}
.year-label {
height: calc(var(--box-size) + var(--grid-gap));
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 5px;
}
/* Only show every 10th label to reduce clutter */
.year-label:nth-child(10n) {
opacity: 1;
}
.year-label:not(:nth-child(10n)) {
opacity: 0; /* Or 0.3 if you want faint lines */
}
/* The Checkboxes (Weeks) */
.week-checkbox {
appearance: none;
-webkit-appearance: none;
width: var(--box-size);
height: var(--box-size);
background-color: var(--box-empty);
border: none;
cursor: pointer;
border-radius: 2px;
transition: background-color 0.1s;
}
.week-checkbox:hover {
background-color: var(--box-hover);
}
.week-checkbox:checked {
background-color: var(--box-checked);
}
/* Tooltip for specific week info */
.week-checkbox:hover::after {
content: attr(data-tooltip);
position: absolute;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 10px;
white-space: nowrap;
pointer-events: none;
z-index: 100;
transform: translateY(-25px);
}
footer {
margin-top: 30px;
font-size: 0.8rem;
color: #888;
text-align: center;
}
@media (max-width: 900px) {
:root {
--box-size: 10px; /* Smaller boxes on mobile */
}
.year-labels {
display: none; /* Hide year labels on very small screens to save space */
}
.grid-container {
justify-content: start; /* Ensure grid scrolls left */
}
}
</style>
</head>
<body>
<header>
<h1>Memento Mori</h1>
<p>Efficient Life Planning Motivator</p>
</header>
<div class="controls">
<div class="input-group">
<label for="birthdate">Your Birthdate:</label>
<input type="date" id="birthdate">
</div>
<button class="btn-primary" id="btn-calculate">Auto-Fill Past</button>
<button class="btn-danger" id="btn-reset">Reset All</button>
</div>
<div class="stats-bar">
<span id="weeks-lived-count">0 Weeks Lived</span>
<span id="percentage-display">0% Completed</span>
</div>
<div class="progress-container">
<div class="progress-fill" id="progress-bar"></div>
</div>
<div class="grid-wrapper">
<!-- Labels for Years (0, 10, 20... 80) -->
<div class="year-labels" id="year-labels">
<!-- JS will populate this -->
</div>
<!-- The Main Grid of Checkboxes -->
<div class="grid-container" id="grid">
<!-- JS will populate 80 rows * 52 columns -->
</div>
</div>
<footer>
<p>Each box is one week. Total: 80 years (4,160 weeks).</p>
<p>State is saved automatically to your browser's LocalStorage.</p>
</footer>
<script>
// Configuration
const TOTAL_YEARS = 80;
const WEEKS_PER_YEAR = 52;
const STORAGE_KEY = 'memento_mori_state';
// DOM Elements
const gridEl = document.getElementById('grid');
const labelsEl = document.getElementById('year-labels');
const weeksLivedEl = document.getElementById('weeks-lived-count');
const percentageEl = document.getElementById('percentage-display');
const progressBarEl = document.getElementById('progress-bar');
const birthdateInput = document.getElementById('birthdate');
const btnCalculate = document.getElementById('btn-calculate');
const btnReset = document.getElementById('btn-reset');
// State Management
let checkedBoxes = new Set();
// Initialize
function init() {
loadState();
renderGrid();
renderLabels();
updateStats();
}
// Load state from LocalStorage
function loadState() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
// Parse JSON array back into a Set
const parsed = JSON.parse(stored);
checkedBoxes = new Set(parsed);
} catch (e) {
console.error("Error parsing saved state", e);
checkedBoxes = new Set();
}
}
}
// Save state to LocalStorage
function saveState() {
// Convert Set to Array for JSON storage
const arrayToStore = Array.from(checkedBoxes);
localStorage.setItem(STORAGE_KEY, JSON.stringify(arrayToStore));
updateStats();
}
// Render the 80x52 Grid
function renderGrid() {
gridEl.innerHTML = '';
// We create 4160 inputs (80 * 52)
for (let year = 0; year < TOTAL_YEARS; year++) {
for (let week = 0; week < WEEKS_PER_YEAR; week++) {
const uniqueId = `y${year}-w${week}`;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'week-checkbox';
checkbox.id = uniqueId;
// Determine if checked based on loaded state
if (checkedBoxes.has(uniqueId)) {
checkbox.checked = true;
}
// Tooltip text
checkbox.dataset.tooltip = `Age ${year}, Week ${week + 1}`;
// Event Listener for State Change
checkbox.addEventListener('change', (e) => {
if (e.target.checked) {
checkedBoxes.add(uniqueId);
} else {
checkedBoxes.delete(uniqueId);
}
saveState();
});
gridEl.appendChild(checkbox);
}
}
}
// Render Year Labels (Left side)
function renderLabels() {
labelsEl.innerHTML = '';
for (let year = 0; year < TOTAL_YEARS; year++) {
const labelDiv = document.createElement('div');
labelDiv.className = 'year-label';
labelDiv.textContent = year;
labelsEl.appendChild(labelDiv);
}
}
// Update Stats Text and Progress Bar
function updateStats() {
const totalBoxes = TOTAL_YEARS * WEEKS_PER_YEAR;
const livedCount = checkedBoxes.size;
const percentage = ((livedCount / totalBoxes) * 100).toFixed(2);
weeksLivedEl.textContent = `${livedCount.toLocaleString()} Weeks Lived`;
percentageEl.textContent = `${percentage}% Completed`;
progressBarEl.style.width = `${percentage}%`;
}
// Feature: Auto-fill based on birthdate
function fastForward() {
const birthDateStr = birthdateInput.value;
if (!birthDateStr) {
// Simple feedback without alert() as requested
weeksLivedEl.textContent = "Please enter a birthdate first.";
setTimeout(() => updateStats(), 2000);
return;
}
const birthDate = new Date(birthDateStr);
const today = new Date();
// Validate dates
if (birthDate > today) {
weeksLivedEl.textContent = "Birthdate cannot be in the future.";
setTimeout(() => updateStats(), 2000);
return;
}
// Calculate difference in weeks
const timeDiff = today - birthDate;
const weeksPassed = Math.floor(timeDiff / (1000 * 60 * 60 * 24 * 7));
// Check against total boxes limit
const weeksToMark = Math.min(weeksPassed, TOTAL_YEARS * WEEKS_PER_YEAR);
// Mark boxes
for (let i = 0; i < weeksToMark; i++) {
const year = Math.floor(i / WEEKS_PER_YEAR);
const week = i % WEEKS_PER_YEAR;
const uniqueId = `y${year}-w${week}`;
checkedBoxes.add(uniqueId);
// Update DOM directly for performance (instead of re-rendering whole grid)
const checkbox = document.getElementById(uniqueId);
if (checkbox) checkbox.checked = true;
}
saveState();
}
// Feature: Reset
function resetAll() {
if(confirm("Are you sure you want to clear all your data?")) {
checkedBoxes.clear();
saveState();
// Uncheck all DOM elements
const checkboxes = document.querySelectorAll('.week-checkbox');
checkboxes.forEach(cb => cb.checked = false);
updateStats();
}
}
// Event Listeners
btnCalculate.addEventListener('click', fastForward);
btnReset.addEventListener('click', resetAll);
// Start the app
init();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment