diff --git a/TaskTracker.Api/Pages/Partials/_UpcomingTasks.cshtml b/TaskTracker.Api/Pages/Partials/_UpcomingTasks.cshtml index 4c58475..4ee6090 100644 --- a/TaskTracker.Api/Pages/Partials/_UpcomingTasks.cshtml +++ b/TaskTracker.Api/Pages/Partials/_UpcomingTasks.cshtml @@ -16,7 +16,7 @@ var dueAt = task.DueAt!.Value; var dueAtUtc = dueAt.ToUniversalTime().ToString("O"); var localDueAt = dueAt.ToLocalTime().ToString("ddd, MMM d, h:mm tt"); -
@@ -29,7 +29,7 @@ @task.Category } - +
} diff --git a/TaskTracker.Api/wwwroot/js/app.js b/TaskTracker.Api/wwwroot/js/app.js index 5618b7d..0b7c419 100644 --- a/TaskTracker.Api/wwwroot/js/app.js +++ b/TaskTracker.Api/wwwroot/js/app.js @@ -55,6 +55,50 @@ }); } + // ======================================== + // Upcoming task countdowns + // ======================================== + + var countdownTimer = null; + + function formatCountdown(totalSeconds, isFuture) { + var remaining = totalSeconds; + var days = Math.floor(remaining / 86400); + remaining %= 86400; + var hours = Math.floor(remaining / 3600); + remaining %= 3600; + var minutes = Math.floor(remaining / 60); + var seconds = remaining % 60; + var parts = []; + + if (days) parts.push(days + 'd'); + if (hours) parts.push(hours + 'h'); + if (minutes) parts.push(minutes + 'm'); + if (seconds && !days && !hours) parts.push(seconds + 's'); + if (parts.length === 0) parts.push('0s'); + + return isFuture ? parts.join(' ') + ' remaining' : 'Overdue by ' + parts.join(' '); + } + + function updateCountdowns() { + var now = Date.now(); + document.querySelectorAll('[data-due-at] .task-countdown, time.task-countdown[data-due-at]').forEach(function(countdown) { + var dueAtElement = countdown.closest('[data-due-at]') || countdown; + var dueAt = Date.parse(dueAtElement.dataset.dueAt); + if (Number.isNaN(dueAt)) return; + + var deltaMilliseconds = dueAt - now; + var totalSeconds = Math.floor(Math.abs(deltaMilliseconds) / 1000); + countdown.textContent = formatCountdown(totalSeconds, deltaMilliseconds > 0); + }); + } + + function initCountdowns() { + if (countdownTimer) clearInterval(countdownTimer); + updateCountdowns(); + countdownTimer = setInterval(updateCountdowns, 1000); + } + // ======================================== // Detail Panel helpers // ======================================== @@ -111,6 +155,7 @@ // Initialize Kanban drag-and-drop on page load document.addEventListener('DOMContentLoaded', function() { initKanban(); + initCountdowns(); // "New Task" header button — scroll to and focus the create-task input in the Pending column var newTaskBtn = document.getElementById('new-task-trigger'); @@ -132,6 +177,11 @@ if (target && (target.id === 'kanban-board' || target.id === 'board-content' || (target.closest && target.closest('#kanban-board')))) { initKanban(); } + if (target && (target.id === 'board-content' || + (target.matches && target.matches('.upcoming-tasks')) || + (target.querySelector && target.querySelector('.upcoming-tasks')))) { + initCountdowns(); + } }); // Also handle htmx:afterSettle for cases where the DOM isn't fully ready on afterSwap