Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions static/assets/inject-countdown.js
Original file line number Diff line number Diff line change
@@ -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()
})()
115 changes: 92 additions & 23 deletions static/templates/pages/injects.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{ define "page" }}
<div class="d-flex w-100 h-100">
<div class="inject-layout d-flex w-100 h-100">
<div class="nav-pills d-flex flex-column h-100 overflow-y-scroll justify-content-start align-items-center bg-body-secondary"
role="tablist" id="tab-container">
<button class="btn btn-warning rounded-0 w-100 d-none" onclick="refreshInjects()" id="refresh--btn">
Expand Down Expand Up @@ -437,28 +437,23 @@ <h5 class="modal-title" id="delete__form--label">Delete Inject</h5>
};
</script>
{{ end }}
<button class="active nav-link w-100 text-start bg-body rounded-0 border placeholder-glow" id="tab--placeholder"
<button class="inject-tab active nav-link w-100 text-start bg-body rounded-0 border placeholder-glow" id="tab--placeholder"
data-bs-toggle="pill" data-bs-target="#pane--placeholder" type="button" role="tab"
aria-controls="pane--placeholder" aria-selected="true">
<div class="container-fluid">
<div class="row">
<div class="col-10">
<div class="d-flex flex-column">
<span class="text-nowrap overflow-hidden text-truncate">
<span id="tab__title--placeholder" class="placeholder placeholder-xs col-12"></span>
</span>
<span class="text-nowrap overflow-hidden text-truncate">
<span id="tab__subject--placeholder" class="placeholder placeholder-xs col-12"></span>
</span>
</div>
<div class="container-fluid px-0">
<div class="d-flex justify-content-between align-items-start gap-2">
<div class="inject-tab-title flex-grow-1">
<span class="d-block text-nowrap overflow-hidden text-truncate">
<span id="tab__title--placeholder" class="placeholder placeholder-xs col-12"></span>
</span>
</div>
<div class="col-2">
<div class="flex-shrink-0 pe-1">
<span id="tab__status--placeholder" class="d-flex justify-content-center"></span>
</div>
</div>
<div class="row justify-content-center align-items-center text-center">
<span id="tab__time--placeholder" style="font-size: 10px;"
class="placeholder placeholder-xs col-4 mb-1"></span>
<div class="inject-tab-meta d-flex flex-wrap justify-content-between align-items-center gap-2 pt-2 pe-1">
<span id="tab__time--placeholder" class="inject-tab-time placeholder placeholder-xs col-4"></span>
<span id="tab__countdown--placeholder" class="placeholder placeholder-xs col-3"></span>
</div>
</div>
</button>
Expand Down Expand Up @@ -591,13 +586,77 @@ <h2 class="offcanvas-title fs-5 text-truncate" id="inject-pdf-viewer-title">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;
}

.inject-layout .inject-tab {
height: auto;
overflow: visible;
padding: .375rem .5rem;
}

.inject-layout .inject-tab.active {
color: var(--bs-nav-pills-link-active-color);
}

.inject-layout .inject-tab.active .inject-tab-time,
.inject-layout .inject-tab.active [id^="tab__status--"] i {
color: rgba(var(--bs-white-rgb), .85) !important;
}

.inject-layout .inject-tab.active .inject-countdown-badge.text-bg-primary {
color: var(--bs-primary-text-emphasis) !important;
background-color: rgba(var(--bs-white-rgb), .92) !important;
}

.inject-tab-title {
min-width: 0;
}

.inject-tab-meta .inject-countdown-badge {
flex-shrink: 0;
margin-left: auto;
max-width: 100%;
}

.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;
}

.inject-tab-time {
display: none;
}
}

</style>

<script src="/static/assets/inject-countdown.js"></script>
<script>
const TAB_CONTAINER = document.getElementById("tab-container")
const PANE_CONTAINER = document.getElementById("pane-container")
Expand Down Expand Up @@ -680,14 +739,15 @@ <h2 class="offcanvas-title fs-5 text-truncate" id="inject-pdf-viewer-title">Inje


let title = tab.querySelector("#tab__title--placeholder")
let a = tab.querySelector("#tab__subject--placeholder")
let time = tab.querySelector("#tab__time--placeholder")
let status = tab.querySelector("#tab__status--placeholder")

title.className = ""
a.className = ""
time.className = ""
if (inject.Submissions && inject.Submissions.length > 0) {
if (inject.OpenTime > new Date()) {
status.innerHTML = `<i class="bi bi-clock text-body-secondary" data-bs-toggle="tooltip"
data-bs-title="Scheduled"></i>`
} else if (inject.Submissions && inject.Submissions.length > 0) {
status.innerHTML = `<i class="bi bi-check2-circle text-success-emphasis" data-bs-toggle="tooltip"
data-bs-title="Submitted"></i>`

Expand Down Expand Up @@ -724,8 +784,13 @@ <h2 class="offcanvas-title fs-5 text-truncate" id="inject-pdf-viewer-title">Inje
data-bs-title="Open for submissions"></i>`
}
title.textContent = inject.Title
a.textContent = inject.Title
time.textContent = inject.OpenTime.toLocaleString()
time.className = "inject-tab-time"
time.textContent = inject.OpenTime.toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
})

let from = pane.querySelector("#pane__from--placeholder")
let subject = pane.querySelector("#pane__subject--placeholder")
Expand All @@ -746,8 +811,9 @@ <h2 class="offcanvas-title fs-5 text-truncate" id="inject-pdf-viewer-title">Inje
descTemp.appendChild(descPara)

descTemp.innerHTML += `
<h5>Inject Details</h5>
<h5>Inject Details <span id="pane__countdown--${inject.ID}" class="inject-countdown-detail"></span></h5>
<ul>
<li><p><strong>Release Time:</strong> <span id="pane__open--${inject.ID}">${inject.OpenTime.toLocaleString()}</span></p></li>
<li><p><strong>Due Time:</strong> <span id="pane__due--${inject.ID}">${inject.DueTime.toLocaleString()}</span></p></li>
<li><p><strong>Close Time:</strong> <span id="pane__close--${inject.ID}">${inject.CloseTime.toLocaleString()}</span></p></li>
</ul>
Expand Down Expand Up @@ -796,6 +862,9 @@ <h5>Inject Details</h5>
tab.innerHTML = tab.innerHTML.replaceAll("placeholder", inject.ID)
pane.innerHTML = pane.innerHTML.replaceAll("placeholder", inject.ID)

InjectCountdown.bind(tab.querySelector(`#tab__countdown--${inject.ID}`), inject)
InjectCountdown.bind(pane.querySelector(`#pane__countdown--${inject.ID}`), inject)

pane.querySelectorAll("[data-pdf-file-name]").forEach((viewerTrigger) => {
viewerTrigger.addEventListener("click", (event) => {
event.preventDefault()
Expand Down