From f96a46d44de34c0f283f0346d23fda2ca4da630d Mon Sep 17 00:00:00 2001 From: Akshay Rohatgi <52616034+Akshay-Rohatgi@users.noreply.github.com> Date: Sun, 13 Sep 2026 14:09:16 -0700 Subject: [PATCH 1/2] Add role-safe inject countdowns --- static/assets/inject-countdown.js | 85 ++++++++++++++++++++++ static/templates/pages/injects.html | 107 +++++++++++++++++++++++----- 2 files changed, 175 insertions(+), 17 deletions(-) create mode 100644 static/assets/inject-countdown.js diff --git a/static/assets/inject-countdown.js b/static/assets/inject-countdown.js new file mode 100644 index 00000000..87c49717 --- /dev/null +++ b/static/assets/inject-countdown.js @@ -0,0 +1,85 @@ +(() => { + "use strict" + + const UPDATE_INTERVAL = 1000 + const WARNING_THRESHOLD = 30 * 60 * 1000 + let timerID + + function formatDuration(milliseconds) { + const totalSeconds = Math.max(0, Math.ceil(milliseconds / 1000)) + const days = Math.floor(totalSeconds / 86400) + const hours = Math.floor((totalSeconds % 86400) / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + const seconds = totalSeconds % 60 + + if (days > 0) { + return hours > 0 ? `${days}d ${hours}h` : `${days}d` + } + if (hours > 0) { + return `${hours}h ${String(minutes).padStart(2, "0")}m` + } + if (minutes > 0) { + return `${minutes}m ${String(seconds).padStart(2, "0")}s` + } + return `${seconds}s` + } + + function stateFor(element, now) { + const openTime = new Date(element.dataset.openTime) + const dueTime = new Date(element.dataset.dueTime) + const closeTime = new Date(element.dataset.closeTime) + + if (now < openTime) { + return { + className: "text-bg-secondary", + label: `Opens in ${formatDuration(openTime - now)}`, + } + } + if (now < dueTime) { + return { + className: dueTime - now <= WARNING_THRESHOLD ? "text-bg-warning" : "text-bg-primary", + label: `Due in ${formatDuration(dueTime - now)}`, + } + } + if (now < closeTime) { + return { + className: "text-bg-danger", + label: `Late ยท closes in ${formatDuration(closeTime - now)}`, + } + } + return { className: "text-bg-secondary", label: "Closed" } + } + + function update(element, now = new Date()) { + const state = stateFor(element, now) + element.classList.remove("text-bg-primary", "text-bg-warning", "text-bg-danger", "text-bg-secondary") + element.classList.add(state.className) + element.textContent = state.label + element.setAttribute("aria-label", state.label) + } + + function updateAll() { + const now = new Date() + document.querySelectorAll(".inject-countdown-badge").forEach(element => update(element, now)) + } + + function bind(element, inject) { + element.className = "badge rounded-pill inject-countdown-badge" + element.dataset.openTime = inject.OpenTime.toISOString() + element.dataset.dueTime = inject.DueTime.toISOString() + element.dataset.closeTime = inject.CloseTime.toISOString() + element.setAttribute("aria-live", "off") + update(element) + } + + function start() { + if (timerID !== undefined) { + return + } + timerID = window.setInterval(updateAll, UPDATE_INTERVAL) + window.addEventListener("pagehide", () => window.clearInterval(timerID), { once: true }) + } + + window.InjectCountdown = { bind, formatDuration, stateFor, updateAll } + start() +})() diff --git a/static/templates/pages/injects.html b/static/templates/pages/injects.html index 6e6e665a..c118422d 100644 --- a/static/templates/pages/injects.html +++ b/static/templates/pages/injects.html @@ -1,15 +1,15 @@ {{ define "page" }} -
+
-
- +
+ +
@@ -591,13 +588,79 @@

Inje --bs-offcanvas-width: min(72vw, 1100px); } + .inject-countdown-badge { + font-size: .7rem; + font-variant-numeric: tabular-nums; + white-space: nowrap; + } + + .inject-countdown-detail { + vertical-align: middle; + } + + .inject-layout #tab-container { + flex: 0 0 clamp(260px, 24vw, 300px); + min-width: 260px; + max-width: 300px; + padding: .75rem; + } + + .inject-layout .inject-tab { + margin-bottom: .5rem; + padding: .85rem; + box-shadow: var(--bs-box-shadow-sm); + transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; + } + + .inject-layout .inject-tab:hover { + border-color: var(--bs-primary-border-subtle) !important; + transform: translateY(-1px); + } + + .inject-layout .inject-tab.active { + border-color: var(--bs-primary-border-subtle) !important; + box-shadow: inset 4px 0 0 var(--bs-primary), var(--bs-box-shadow-sm); + } + + .inject-tab-time { + color: var(--bs-secondary-color); + font-size: .7rem; + white-space: nowrap; + } + @media (max-width: 767.98px) { .inject-pdf-viewer { --bs-offcanvas-width: 100vw; } + + .inject-layout #tab-container { + flex-basis: 210px; + min-width: 210px; + max-width: 210px; + padding: .5rem; + } + + .inject-layout .inject-tab { + padding: .7rem; + } + + .inject-tab-time { + display: none; + } + } + + @media (prefers-reduced-motion: reduce) { + .inject-layout .inject-tab { + transition: none; + } + + .inject-layout .inject-tab:hover { + transform: none; + } } + {{ end }} -