feat: implement SPA loading bar, harden status page logic, and optimize Dockerfile

This commit is contained in:
2026-02-20 22:15:21 -06:00
parent 44948a6e9f
commit 3bd27f59d7
5 changed files with 202 additions and 95 deletions

View File

@@ -14,50 +14,81 @@ function toggleMenu(collapse) {
}
async function goto(location, { push = true } = {}) {
let response;
const loadingBar = document.getElementById('loading-bar');
console.log(`Navigating to ${location}`);
if (loadingBar) {
loadingBar.style.width = ''; // Clear inline style from previous run
}
let loadingTimeout = setTimeout(() => {
if (loadingBar) {
console.log("Navigation taking > 150ms, showing bar");
loadingBar.classList.remove('finish');
loadingBar.classList.add('active');
loadingBar.classList.add('visible');
}
}, 150);
try {
response = await fetch("/api/goto/" + location, {
const response = await fetch("/api/goto/" + location, {
credentials: "include",
method: "GET",
mode: "cors",
});
if (!response.ok) {
console.error(`Navigation failed: HTTP ${response.status}`);
return;
throw new Error(`HTTP ${response.status}`);
}
} catch (err) {
console.error("Navigation fetch failed:", err);
return;
}
document.dispatchEvent(new Event('beforenavigate'));
// Wait for the full body to download - this is usually the slow part
const [metadata, content] = await response.json();
document.dispatchEvent(new Event('beforenavigate'));
const [metadata, content] = await response.json();
const root = document.getElementById("root");
root.innerHTML = content;
const root = document.getElementById("root");
root.innerHTML = content;
// Re-execute scripts injected via innerHTML (browser ignores them otherwise)
root.querySelectorAll("script").forEach(function(oldScript) {
const newScript = document.createElement("script");
Array.from(oldScript.attributes).forEach(function(attr) {
newScript.setAttribute(attr.name, attr.value);
// Re-execute scripts
root.querySelectorAll("script").forEach(function(oldScript) {
const newScript = document.createElement("script");
Array.from(oldScript.attributes).forEach(function(attr) {
newScript.setAttribute(attr.name, attr.value);
});
newScript.textContent = oldScript.textContent;
oldScript.parentNode.replaceChild(newScript, oldScript);
});
newScript.textContent = oldScript.textContent;
oldScript.parentNode.replaceChild(newScript, oldScript);
});
if (window.location.href.includes("#")) {
const id = decodeURIComponent(window.location.hash.substring(1));
const el = document.getElementById(id);
if (el) el.scrollIntoView();
} else {
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
}
if (window.location.href.includes("#")) {
const id = decodeURIComponent(window.location.hash.substring(1));
const el = document.getElementById(id);
if (el) el.scrollIntoView();
} else {
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
}
toggleMenu(true);
document.querySelector("title").textContent = metadata["title"];
if (push) {
history.pushState(null, null, metadata["canonical"]);
toggleMenu(true);
document.querySelector("title").textContent = metadata["title"];
if (push) {
history.pushState(null, null, metadata["canonical"]);
}
} catch (err) {
console.error("Navigation failed:", err);
} finally {
clearTimeout(loadingTimeout);
if (loadingBar && loadingBar.classList.contains('active')) {
console.log("Navigation finished, hiding bar");
loadingBar.classList.add('finish');
loadingBar.classList.remove('active');
setTimeout(() => {
if (!loadingBar.classList.contains('active')) {
loadingBar.style.width = '0%';
loadingBar.classList.remove('finish');
loadingBar.classList.remove('visible');
}
}, 500);
}
}
}