feat: Clock

This commit is contained in:
2026-03-03 17:38:51 +00:00
parent 225ac5e441
commit 8152072bec
7 changed files with 365 additions and 114 deletions

109
backend/static/countdown.js Normal file
View File

@@ -0,0 +1,109 @@
(() => {
function pad2(value) {
return String(value).padStart(2, "0");
}
function splitCountdown(ms) {
const totalSeconds = Math.max(0, Math.floor(ms / 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;
return { days, hours, minutes, seconds };
}
function setAnimatedValue(node, nextValue) {
if (!node) {
return;
}
if (node.textContent === nextValue) {
return;
}
node.textContent = nextValue;
node.classList.remove("is-updated");
void node.offsetWidth;
node.classList.add("is-updated");
}
function initTimer(widget) {
const targetIso = widget.dataset.countdownTarget;
const startedLabel = widget.dataset.countdownStarted || "";
const toggle = widget.querySelector("[data-countdown-toggle]");
const popover = widget.querySelector("[data-countdown-popover]");
const subline = widget.querySelector("[data-countdown-subline]");
const daysNode = widget.querySelector("[data-countdown-days]");
const hoursNode = widget.querySelector("[data-countdown-hours]");
const minutesNode = widget.querySelector("[data-countdown-minutes]");
const secondsNode = widget.querySelector("[data-countdown-seconds]");
if (!targetIso || !toggle || !popover || !subline || !daysNode || !hoursNode || !minutesNode || !secondsNode) {
return;
}
const targetMs = Date.parse(targetIso);
if (Number.isNaN(targetMs)) {
subline.textContent = "--";
return;
}
const update = () => {
const now = Date.now();
const delta = targetMs - now;
if (delta <= 0) {
setAnimatedValue(daysNode, "0");
setAnimatedValue(hoursNode, "00");
setAnimatedValue(minutesNode, "00");
setAnimatedValue(secondsNode, "00");
subline.textContent = startedLabel;
return;
}
const parts = splitCountdown(delta);
setAnimatedValue(daysNode, String(parts.days));
setAnimatedValue(hoursNode, pad2(parts.hours));
setAnimatedValue(minutesNode, pad2(parts.minutes));
setAnimatedValue(secondsNode, pad2(parts.seconds));
};
const close = () => {
popover.hidden = true;
toggle.setAttribute("aria-expanded", "false");
};
const open = () => {
popover.hidden = false;
toggle.setAttribute("aria-expanded", "true");
update();
};
toggle.addEventListener("click", (event) => {
event.preventDefault();
if (popover.hidden) {
open();
} else {
close();
}
});
document.addEventListener("click", (event) => {
if (!widget.contains(event.target)) {
close();
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
close();
}
});
update();
window.setInterval(update, 1000);
}
document.addEventListener("DOMContentLoaded", () => {
const widgets = document.querySelectorAll(".toolbar-timer");
widgets.forEach(initTimer);
});
})();