Сгенерировано в китайской нейронке языковой моделью GLM-4.7
Основано на концепции Календаря-Пинарика
Сгенерировано в китайской нейронке языковой моделью GLM-4.7
Основано на концепции Календаря-Пинарика
| <!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> |