192 lines
6.9 KiB
JavaScript
Executable File
192 lines
6.9 KiB
JavaScript
Executable File
/* =============================================================
|
|
dashboard.js — Home page dashboard behaviour
|
|
Runs after DOM is ready. All IDs 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 ────────────────────────────────────────────── */
|
|
|
|
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())}`;
|
|
}
|
|
|
|
/* ── Date stats ────────────────────────────────────────────── */
|
|
|
|
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 pct = ((dayOfYear / daysInYear) * 100).toFixed(1);
|
|
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 + "%";
|
|
}
|
|
|
|
/* ── 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 escHtml(str) {
|
|
return str
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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);
|
|
} catch (_) {
|
|
// Network error or cross-origin — silently ignore
|
|
}
|
|
}
|
|
|
|
/* ── Init ──────────────────────────────────────────────────── */
|
|
|
|
function init() {
|
|
updateGreeting();
|
|
updateStats();
|
|
updateClock();
|
|
setInterval(updateClock, 1000);
|
|
|
|
// 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, "");
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|