Update dashboard
All checks were successful
Build Org Website / build (push) Successful in 41s

This commit is contained in:
2026-05-06 23:15:20 +01:00
parent 687126b974
commit fa992de25a
3 changed files with 657 additions and 533 deletions

View File

@@ -1,186 +1,166 @@
/* =============================================================
dashboard.js — Home page dashboard behaviour
Runs after DOM is ready. All IDs are db-* scoped.
============================================================= */
/* Home dashboard behaviour. All selectors are db-* scoped. */
(function () {
"use strict";
/* ── Greeting ──────────────────────────────────────────────── */
function updateGreeting() {
const el = document.getElementById("db-greeting");
if (!el) return;
const h = new Date().getHours();
if (h < 5) el.textContent = "Burning the midnight oil";
else if (h < 12) el.textContent = "Good morning";
else if (h < 17) el.textContent = "Good afternoon";
else if (h < 21) el.textContent = "Good evening";
else el.textContent = "Good night";
}
/* ── Live clock ────────────────────────────────────────────── */
const hour = new Date().getHours();
if (hour < 5) el.textContent = "Late session";
else if (hour < 12) el.textContent = "Good morning";
else if (hour < 17) el.textContent = "Good afternoon";
else if (hour < 21) el.textContent = "Good evening";
else el.textContent = "Evening review";
}
function updateClock() {
const el = document.getElementById("db-clock");
if (!el) return;
const now = new Date();
const pad = (n) => String(n).padStart(2, "0");
el.textContent = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
el.textContent = new Intl.DateTimeFormat(undefined, {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}).format(new Date());
}
/* ── Date stats ────────────────────────────────────────────── */
function getIsoWeek(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
}
function updateStats() {
const now = new Date();
const year = now.getFullYear();
// Day of year
const start = new Date(year, 0, 0);
const diff = now - start;
const oneDay = 1000 * 60 * 60 * 24;
const dayOfYear = Math.floor(diff / oneDay);
// ISO week number
const d = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNum = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
// Days left
const isLeap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
const daysInYear = isLeap ? 366 : 365;
const daysLeft = daysInYear - dayOfYear;
// Month name
const months = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
// Write out
const set = (id, val) => {
const el = document.getElementById(id);
if (el) el.textContent = val;
};
set("db-stat-day", dayOfYear);
set("db-stat-week", "W" + weekNum);
set("db-stat-month", months[now.getMonth()]);
set("db-stat-left", daysLeft);
// Year progress bar
const start = new Date(year, 0, 1);
const nextYear = new Date(year + 1, 0, 1);
const dayOfYear = Math.floor((now - start) / 86400000) + 1;
const daysInYear = Math.round((nextYear - start) / 86400000);
const daysLeft = Math.max(0, daysInYear - dayOfYear);
const month = new Intl.DateTimeFormat(undefined, { month: "short" }).format(now);
const pct = ((dayOfYear / daysInYear) * 100).toFixed(1);
setText("db-stat-day", dayOfYear);
setText("db-stat-week", "W" + getIsoWeek(now));
setText("db-stat-month", month);
setText("db-stat-left", daysLeft);
setText("db-year-pct", pct + "%");
const fill = document.getElementById("db-year-fill");
const label = document.getElementById("db-year-pct");
if (fill) fill.style.width = pct + "%";
if (label) label.textContent = pct + "%";
if (fill) fill.style.width = pct + "%";
}
/* ── Feed builder ──────────────────────────────────────────── */
/**
* Parse a simple HTML sitemap/list page and extract links.
* Looks for <li> elements containing <a> tags.
* @param {Document} doc - parsed document
* @param {number} max - max items to return
* @param {string} prefix - path prefix to prepend to relative hrefs (e.g. "/blogs")
* @returns {{ href: string, title: string, date: string|null }[]}
*/
function extractLinks(doc, max, prefix) {
const base = (prefix || "").replace(/\/$/, ""); // strip trailing slash
const items = [];
// org-publish sitemaps use <li> inside #content
const lis = doc.querySelectorAll("#content li, .org-ul li, ul li");
for (const li of lis) {
const a = li.querySelector("a[href]");
if (!a) continue;
let href = a.getAttribute("href");
if (!href || href.startsWith("http")) continue; // skip external
// Prepend the section prefix when the href doesn't already start with it
if (base && !href.startsWith(base)) {
// href may start with "/" or be relative like "2026/03-march/foo.html"
href = href.startsWith("/")
? base + href
: base + "/" + href;
}
const title = a.textContent.trim();
if (!title) continue;
// Grab any date text that follows the link
const dateMatch = li.textContent.replace(title, "").match(/\d{4}-\d{2}-\d{2}/);
items.push({ href, title, date: dateMatch ? dateMatch[0] : null });
if (items.length >= max) break;
}
return items;
}
/**
* Render items into a db-feed <ul>.
*/
function renderFeed(ulId, items, fallbackHref) {
const ul = document.getElementById(ulId);
if (!ul) return;
if (!items || items.length === 0) {
// keep the fallback link already in the HTML
return;
}
ul.innerHTML = items
.map(
({ href, title, date }) => `
<li class="db-feed__item">
<span class="db-feed__dot"></span>
<a href="${escHtml(href)}">${escHtml(title)}</a>
${date ? `<span class="db-feed__meta">${escHtml(date)}</span>` : ""}
</li>`
)
.join("");
function setText(id, value) {
const el = document.getElementById(id);
if (el) el.textContent = value;
}
function escHtml(str) {
return str
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
/**
* Fetch a sitemap page and populate a feed.
* Fails silently — the fallback link is already in the DOM.
* @param {string} listUrl - absolute path to the sitemap page to fetch
* @param {string} ulId - id of the <ul> to populate
* @param {number} max - max entries to show
* @param {string} prefix - path prefix to prepend to extracted hrefs
*/
function normaliseHref(href, prefix) {
if (!href || href.startsWith("http") || href.startsWith("#")) return null;
const base = (prefix || "").replace(/\/$/, "");
if (!base || href.startsWith(base + "/")) return href;
return href.startsWith("/") ? base + href : base + "/" + href;
}
function extractLinks(doc, max, prefix) {
const items = [];
const seen = new Set();
const links = doc.querySelectorAll("#content li a[href], .org-ul li a[href], ul li a[href]");
for (const a of links) {
const href = normaliseHref(a.getAttribute("href"), prefix);
const title = a.textContent.trim();
if (!href || !title || seen.has(href)) continue;
const li = a.closest("li");
const dateMatch = li ? li.textContent.match(/\b\d{4}-\d{2}-\d{2}\b/) : null;
seen.add(href);
items.push({ href, title, date: dateMatch ? dateMatch[0] : null });
if (items.length >= max) break;
}
return items;
}
function renderFeed(ulId, items) {
const ul = document.getElementById(ulId);
if (!ul || !items.length) return;
ul.innerHTML = items
.map(({ href, title, date }) => (
`<li class="db-feed__item">` +
`<span class="db-feed__dot"></span>` +
`<a href="${escHtml(href)}">${escHtml(title)}</a>` +
(date ? `<span class="db-feed__meta">${escHtml(date)}</span>` : "") +
`</li>`
))
.join("");
}
async function loadFeed(listUrl, ulId, max, prefix) {
try {
const resp = await fetch(listUrl, { credentials: "same-origin" });
if (!resp.ok) return;
const html = await resp.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const items = extractLinks(doc, max, prefix);
renderFeed(ulId, items);
const doc = new DOMParser().parseFromString(html, "text/html");
renderFeed(ulId, extractLinks(doc, max, prefix));
} catch (_) {
// Network error or cross-origin — silently ignore
/* Fallback links remain in the HTML. */
}
}
/* ── Init ──────────────────────────────────────────────────── */
function initCommandFilter() {
const input = document.getElementById("db-command-search");
const nav = document.getElementById("db-quicknav");
if (!input || !nav) return;
const items = Array.from(nav.querySelectorAll(".db-qn-item"));
const applyFilter = () => {
const query = input.value.trim().toLowerCase();
items.forEach((item) => {
const haystack = [
item.textContent,
item.getAttribute("href"),
item.dataset.keywords,
].join(" ").toLowerCase();
item.classList.toggle("is-hidden", Boolean(query && !haystack.includes(query)));
});
};
input.addEventListener("input", applyFilter);
document.addEventListener("keydown", (event) => {
if (event.key === "/" && !/^(input|textarea|select)$/i.test(event.target.tagName)) {
event.preventDefault();
input.focus();
}
});
}
function init() {
updateGreeting();
updateStats();
updateClock();
setInterval(updateClock, 1000);
updateStats();
initCommandFilter();
// Populate feeds — pass the section prefix so relative hrefs are correct
loadFeed("/blogs/blogs-list.html", "db-feed-blogs", 6, "/blogs");
loadFeed("/posts/posts-list.html", "db-feed-posts", 6, "/posts");
loadFeed("/recently-updated.html", "db-feed-recent", 6, "");
setInterval(updateClock, 1000);
loadFeed("/blogs/blogs-list.html", "db-feed-blogs", 6, "/blogs");
loadFeed("/posts/posts-list.html", "db-feed-posts", 6, "/posts");
loadFeed("/recently-updated.html", "db-feed-recent", 6, "");
}
if (document.readyState === "loading") {