updating modes and structures
This commit is contained in:
0
assets/scripts/bigger-picture.min.js
vendored
Normal file → Executable file
0
assets/scripts/bigger-picture.min.js
vendored
Normal file → Executable file
0
assets/scripts/comments.js
Normal file → Executable file
0
assets/scripts/comments.js
Normal file → Executable file
0
assets/scripts/competency-status-board.js
Normal file → Executable file
0
assets/scripts/competency-status-board.js
Normal file → Executable file
0
assets/scripts/gallery-init.js
Normal file → Executable file
0
assets/scripts/gallery-init.js
Normal file → Executable file
0
assets/scripts/lunr.js
Normal file → Executable file
0
assets/scripts/lunr.js
Normal file → Executable file
0
assets/scripts/mermaid.min.js
vendored
Normal file → Executable file
0
assets/scripts/mermaid.min.js
vendored
Normal file → Executable file
0
assets/scripts/notes.js
Normal file → Executable file
0
assets/scripts/notes.js
Normal file → Executable file
0
assets/scripts/script.js
Normal file → Executable file
0
assets/scripts/script.js
Normal file → Executable file
0
assets/scripts/search.js
Normal file → Executable file
0
assets/scripts/search.js
Normal file → Executable file
0
assets/scripts/sitemap-interactive.js
Normal file → Executable file
0
assets/scripts/sitemap-interactive.js
Normal file → Executable file
0
assets/scripts/svg-pan-zoom.min.js
vendored
Normal file → Executable file
0
assets/scripts/svg-pan-zoom.min.js
vendored
Normal file → Executable file
166
assets/scripts/wird-tracker.js
Normal file → Executable file
166
assets/scripts/wird-tracker.js
Normal file → Executable file
@@ -327,17 +327,18 @@
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
setTodayLabel();
|
||||
await Promise.all([loadAll(), loadMotalah()]);
|
||||
renderToday();
|
||||
renderNafl();
|
||||
renderMeetingPanel();
|
||||
renderKhatm();
|
||||
renderHistory();
|
||||
renderChart();
|
||||
renderMotalah();
|
||||
renderHeatmap();
|
||||
bindControls();
|
||||
setTodayLabel();
|
||||
await Promise.all([loadAll(), loadMotalah()]);
|
||||
renderToday();
|
||||
renderNafl();
|
||||
renderMeetingPanel();
|
||||
renderKhatm();
|
||||
renderHistory();
|
||||
renderChart();
|
||||
renderMotalah();
|
||||
renderSummary();
|
||||
renderHeatmap();
|
||||
bindControls();
|
||||
});
|
||||
|
||||
function setTodayLabel() {
|
||||
@@ -827,8 +828,9 @@
|
||||
|
||||
const HIDE_TYPES = new Set(["shaykh_meeting", ...Object.keys(NAFL_WIRD), "khatm"]);
|
||||
const sorted = [...allEntries]
|
||||
.filter(e => !HIDE_TYPES.has(e.wirdType))
|
||||
.sort((a, b) => b.date.localeCompare(a.date));
|
||||
.filter(e => !HIDE_TYPES.has(e.wirdType))
|
||||
.sort((a, b) => b.date.localeCompare(a.date))
|
||||
.slice(0, 20);
|
||||
|
||||
if (sorted.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="5" class="loading-cell">No entries yet.</td></tr>`;
|
||||
@@ -1192,6 +1194,144 @@
|
||||
return `${label}: ${Math.round(raw).toLocaleString()} ${meta.unit}`;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DAILY SUMMARY TABLE
|
||||
// ============================================================
|
||||
|
||||
let summaryDays = 30;
|
||||
let summaryFilter = "__all__";
|
||||
|
||||
function renderSummary() {
|
||||
const wrap = document.getElementById("summary-table-wrap");
|
||||
if (!wrap) return;
|
||||
|
||||
const today = isoDate();
|
||||
const cols = summaryFilter === "__all__"
|
||||
? Object.keys(DAILY_WIRD)
|
||||
: [summaryFilter];
|
||||
|
||||
// Build date rows newest→oldest
|
||||
const rows = [];
|
||||
for (let i = 0; i < summaryDays; i++) {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - i);
|
||||
rows.push(isoDate(d));
|
||||
}
|
||||
|
||||
// Pre-aggregate entries into { date: { type: val } }
|
||||
const agg = {};
|
||||
for (const e of allEntries) {
|
||||
if (!cols.includes(e.wirdType)) continue;
|
||||
if (!agg[e.date]) agg[e.date] = {};
|
||||
const meta = DAILY_WIRD[e.wirdType];
|
||||
if (meta && meta.type === "rating") {
|
||||
agg[e.date][e.wirdType] = Math.max(
|
||||
agg[e.date][e.wirdType] || 0, Number(e.value)
|
||||
);
|
||||
} else {
|
||||
agg[e.date][e.wirdType] = (agg[e.date][e.wirdType] || 0) + Number(e.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Build header
|
||||
const thCols = cols.map(t => {
|
||||
const meta = DAILY_WIRD[t];
|
||||
const unitHint = meta.type === "rating" ? "/5" : meta.unit ? meta.unit : "";
|
||||
return `<th title="${meta.label} · target: ${meta.target}${unitHint}">
|
||||
${meta.label}<br>
|
||||
<span style="font-weight:400;opacity:.6">/ ${meta.target}${unitHint}</span>
|
||||
</th>`;
|
||||
}).join("");
|
||||
|
||||
// Build body rows
|
||||
const bodyRows = rows.map(dateIso => {
|
||||
const isToday = dateIso === today;
|
||||
const dayData = agg[dateIso] || {};
|
||||
|
||||
const displayDate = new Date(dateIso + "T00:00:00").toLocaleDateString("en-GB", {
|
||||
weekday: "short", day: "numeric", month: "short"
|
||||
});
|
||||
|
||||
const cells = cols.map(type => {
|
||||
const meta = DAILY_WIRD[type];
|
||||
const val = dayData[type];
|
||||
|
||||
if (val === undefined || val === null) {
|
||||
return `<td class="summary-cell-empty">—</td>`;
|
||||
}
|
||||
|
||||
const cls = statusClass(type, val);
|
||||
const cellCls = cls === "over" ? "summary-cell-over"
|
||||
: cls === "done" ? "summary-cell-done"
|
||||
: cls === "miss" ? "summary-cell-miss"
|
||||
: "";
|
||||
|
||||
let display;
|
||||
if (meta.type === "rating") {
|
||||
display = `${val}<span style="opacity:.45">/5</span>`;
|
||||
} else {
|
||||
display = Number(val).toLocaleString();
|
||||
}
|
||||
|
||||
return `<td class="${cellCls}">${display}</td>`;
|
||||
}).join("");
|
||||
|
||||
return `<tr class="${isToday ? "summary-today" : ""}">
|
||||
<td>${displayDate}${isToday ? " ·" : ""}</td>
|
||||
${cells}
|
||||
</tr>`;
|
||||
}).join("");
|
||||
|
||||
// Footer: days target met per column
|
||||
const footCells = cols.map(type => {
|
||||
const meta = DAILY_WIRD[type];
|
||||
let metCount = 0;
|
||||
for (const dateIso of rows) {
|
||||
const val = (agg[dateIso] || {})[type];
|
||||
if (val !== undefined && val >= meta.target) metCount++;
|
||||
}
|
||||
const pct = Math.round((metCount / summaryDays) * 100);
|
||||
const cls = pct >= 80 ? "summary-cell-done" : pct >= 50 ? "" : "summary-cell-miss";
|
||||
return `<td class="summary-tfoot ${cls}">${metCount}d <span style="opacity:.55">(${pct}%)</span></td>`;
|
||||
}).join("");
|
||||
|
||||
wrap.innerHTML = `
|
||||
<table id="summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
${thCols}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${bodyRows}</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="summary-tfoot" style="color:var(--muted)">days met</td>
|
||||
${footCells}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
// Controls
|
||||
document.querySelectorAll(".summary-preset").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
summaryDays = parseInt(btn.dataset.days);
|
||||
document.querySelectorAll(".summary-preset").forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
renderSummary();
|
||||
});
|
||||
});
|
||||
|
||||
const summaryFilterEl = document.getElementById("summary-filter");
|
||||
if (summaryFilterEl) {
|
||||
summaryFilterEl.addEventListener("change", () => {
|
||||
summaryFilter = summaryFilterEl.value;
|
||||
renderSummary();
|
||||
});
|
||||
}
|
||||
|
||||
function renderHeatmapStats(scoreMap, type, startIso, endIso) {
|
||||
const el = $("#heatmap-stats");
|
||||
const daysLogged = Object.values(scoreMap).filter(v => v > 0).length;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
191
assets/scripts/zhd.js
Executable file
191
assets/scripts/zhd.js
Executable file
@@ -0,0 +1,191 @@
|
||||
/* =============================================================
|
||||
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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user