diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.packages/archives/elpa/archive-contents b/.packages/archives/elpa/archive-contents old mode 100644 new mode 100755 diff --git a/.packages/archives/elpa/archive-contents.signed b/.packages/archives/elpa/archive-contents.signed old mode 100644 new mode 100755 diff --git a/.packages/archives/melpa/archive-contents b/.packages/archives/melpa/archive-contents old mode 100644 new mode 100755 diff --git a/.packages/htmlize-20250724.1703/htmlize-autoloads.el b/.packages/htmlize-20250724.1703/htmlize-autoloads.el old mode 100644 new mode 100755 diff --git a/.packages/htmlize-20250724.1703/htmlize-pkg.el b/.packages/htmlize-20250724.1703/htmlize-pkg.el old mode 100644 new mode 100755 diff --git a/.packages/htmlize-20250724.1703/htmlize.el b/.packages/htmlize-20250724.1703/htmlize.el old mode 100644 new mode 100755 diff --git a/.packages/htmlize-20250724.1703/htmlize.elc b/.packages/htmlize-20250724.1703/htmlize.elc old mode 100644 new mode 100755 diff --git a/.projectile b/.projectile old mode 100644 new mode 100755 diff --git a/.stignore b/.stignore old mode 100644 new mode 100755 diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 diff --git a/Makefile.sync-conflict-20260325-224704-NE5VEIB b/Makefile.sync-conflict-20260325-224704-NE5VEIB deleted file mode 100644 index b473990..0000000 --- a/Makefile.sync-conflict-20260325-224704-NE5VEIB +++ /dev/null @@ -1,54 +0,0 @@ -.PHONY: all clean norm help - -VENV := .venv -PY := $(VENV)/bin/python -PIP := $(VENV)/bin/pip - -all: clean-output build search clean-venv - -build: - @echo "Building project..." - emacs -Q --script build-site.el - -search: $(VENV) - @echo "Generating search index" - $(PY) search-index-json.py - -$(VENV): - @echo "Generating virtual environment" - python3 -m venv $(VENV) - $(PIP) install -r requirements.txt - -clean-output: - @echo "Cleaning output directory..." - rm -rf output/ - -clean-venv: - @echo "Cleaning virtual environment..." - rm -rf $(VENV) - -norm: - @echo "sorting out the backups..." - find . -path ./backups -prune -o -type f -name '*~' -exec mv {} backups/ \; - -fix-permissions: - echo "shakkal123" | sudo -S chown -R zaine:zaine . - -restore-permissions: - echo "shakkal123" | sudo -S chown -R 33:33 . - -perm-change-logs: - echo "shakkal123" | sudo -S chown -R zaine:zaine org-web-build.log - -# Show help message -help: - @echo "Available targets:" - @echo " make - Full rebuild" - @echo " make build - Rebuild the output directory" - @echo " make clean-output - Remove output directory" - @echo " make clean-venv - Remove venv directory" - @echo " make norm - Move all files that end with ~ to the backup folder" - @echo " make search - creates the search index" - @echo " make fix-permissions - Fix permissions for the current user" - @echo " make restore-permissions - Restore permissions to www-data" - @echo " make help - Show this help message!" diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/assets/New Document.rnote b/assets/New Document.rnote old mode 100644 new mode 100755 diff --git a/assets/emacs-calibre.rnote b/assets/emacs-calibre.rnote old mode 100644 new mode 100755 diff --git a/assets/fonts/NotoSans-Regular.ttf b/assets/fonts/NotoSans-Regular.ttf old mode 100644 new mode 100755 diff --git a/assets/fonts/Roboto-Regular.ttf b/assets/fonts/Roboto-Regular.ttf old mode 100644 new mode 100755 diff --git a/assets/fonts/times.ttf b/assets/fonts/times.ttf old mode 100644 new mode 100755 diff --git a/assets/icons/apple-touch-icon.png b/assets/icons/apple-touch-icon.png old mode 100644 new mode 100755 diff --git a/assets/icons/icons8-favicon-50.png b/assets/icons/icons8-favicon-50.png old mode 100644 new mode 100755 diff --git a/assets/icons/icons8-film-tape-100.png b/assets/icons/icons8-film-tape-100.png old mode 100644 new mode 100755 diff --git a/assets/icons/icons8-z-key-64.png b/assets/icons/icons8-z-key-64.png old mode 100644 new mode 100755 diff --git a/assets/images/1_to_2.svg b/assets/images/1_to_2.svg old mode 100644 new mode 100755 diff --git a/assets/images/240226-pr.png b/assets/images/240226-pr.png old mode 100644 new mode 100755 diff --git a/assets/images/26-02-26-di-errors-2.png b/assets/images/26-02-26-di-errors-2.png old mode 100644 new mode 100755 diff --git a/assets/images/26-02-26-di-errors.png b/assets/images/26-02-26-di-errors.png old mode 100644 new mode 100755 diff --git a/assets/images/27-02-26-xserver.png b/assets/images/27-02-26-xserver.png old mode 100644 new mode 100755 diff --git a/assets/images/blogs/2026-03-12-journeys-branch.png b/assets/images/blogs/2026-03-12-journeys-branch.png old mode 100644 new mode 100755 diff --git a/assets/images/blogs/2026-03-18-slack-dag-manager.png b/assets/images/blogs/2026-03-18-slack-dag-manager.png old mode 100644 new mode 100755 diff --git a/assets/images/blogs/2026-03-23-install-emacs.png b/assets/images/blogs/2026-03-23-install-emacs.png old mode 100644 new mode 100755 diff --git a/assets/images/blogs/2026-03-24-lunch.jpg b/assets/images/blogs/2026-03-24-lunch.jpg old mode 100644 new mode 100755 diff --git a/assets/images/blogs/2026-03-29-dinner-1.jpg b/assets/images/blogs/2026-03-29-dinner-1.jpg new file mode 100755 index 0000000..46c70c2 Binary files /dev/null and b/assets/images/blogs/2026-03-29-dinner-1.jpg differ diff --git a/assets/images/blogs/2026-03-29-dinner-2.jpg b/assets/images/blogs/2026-03-29-dinner-2.jpg new file mode 100755 index 0000000..eb82dfd Binary files /dev/null and b/assets/images/blogs/2026-03-29-dinner-2.jpg differ diff --git a/assets/images/blogs/2026-03-29-dinner-3.jpg b/assets/images/blogs/2026-03-29-dinner-3.jpg new file mode 100755 index 0000000..c48a155 Binary files /dev/null and b/assets/images/blogs/2026-03-29-dinner-3.jpg differ diff --git a/assets/images/career/2025-10-10-stress-in-workplace.png b/assets/images/career/2025-10-10-stress-in-workplace.png old mode 100644 new mode 100755 diff --git a/assets/images/career/2025-10-10-user_stories.jpg b/assets/images/career/2025-10-10-user_stories.jpg old mode 100644 new mode 100755 diff --git a/assets/images/career/2025-11-05-retrospective_boat_model.png b/assets/images/career/2025-11-05-retrospective_boat_model.png old mode 100644 new mode 100755 diff --git a/assets/images/career/2025-11-10-Aware-of-the-Benefits-of-Normalisation.png b/assets/images/career/2025-11-10-Aware-of-the-Benefits-of-Normalisation.png old mode 100644 new mode 100755 diff --git a/assets/images/career/2025-11-15-airflow.png b/assets/images/career/2025-11-15-airflow.png old mode 100644 new mode 100755 diff --git a/assets/images/career/2025-12-02-retros.jpg b/assets/images/career/2025-12-02-retros.jpg old mode 100644 new mode 100755 diff --git a/assets/images/career/2026-01-17-airflow-flow.png b/assets/images/career/2026-01-17-airflow-flow.png old mode 100644 new mode 100755 diff --git a/assets/images/career/2026-01-17-datamart-high-level.png b/assets/images/career/2026-01-17-datamart-high-level.png old mode 100644 new mode 100755 diff --git a/assets/images/career/2026-01-17-high-level-dora.png b/assets/images/career/2026-01-17-high-level-dora.png old mode 100644 new mode 100755 diff --git a/assets/images/career/2026-03-05-api-protocols.png b/assets/images/career/2026-03-05-api-protocols.png old mode 100644 new mode 100755 diff --git a/assets/images/career/owasp/Screenshot 2025-10-16 134403.png b/assets/images/career/owasp/Screenshot 2025-10-16 134403.png old mode 100644 new mode 100755 diff --git a/assets/images/career/owasp/Screenshot 2025-10-16 134438.png b/assets/images/career/owasp/Screenshot 2025-10-16 134438.png old mode 100644 new mode 100755 diff --git a/assets/images/career/owasp/Screenshot 2025-10-16 134457.png b/assets/images/career/owasp/Screenshot 2025-10-16 134457.png old mode 100644 new mode 100755 diff --git a/assets/images/career/owasp/Screenshot 2025-10-16 134512.png b/assets/images/career/owasp/Screenshot 2025-10-16 134512.png old mode 100644 new mode 100755 diff --git a/assets/images/career/owasp/swappy-20251106-214613.png b/assets/images/career/owasp/swappy-20251106-214613.png old mode 100644 new mode 100755 diff --git a/assets/images/career/owasp/swappy-20251106-214648.png b/assets/images/career/owasp/swappy-20251106-214648.png old mode 100644 new mode 100755 diff --git a/assets/images/career/owasp/swappy-20251106-214700.png b/assets/images/career/owasp/swappy-20251106-214700.png old mode 100644 new mode 100755 diff --git a/assets/images/career/owasp/swappy-20251106-214709.png b/assets/images/career/owasp/swappy-20251106-214709.png old mode 100644 new mode 100755 diff --git a/assets/images/career/owasp/swappy-20251106-214717.png b/assets/images/career/owasp/swappy-20251106-214717.png old mode 100644 new mode 100755 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 112139.png b/assets/images/career/wireframes/Screenshot 2025-10-31 112139.png old mode 100644 new mode 100755 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 112206.png b/assets/images/career/wireframes/Screenshot 2025-10-31 112206.png old mode 100644 new mode 100755 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 112427.png b/assets/images/career/wireframes/Screenshot 2025-10-31 112427.png old mode 100644 new mode 100755 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 115621.png b/assets/images/career/wireframes/Screenshot 2025-10-31 115621.png old mode 100644 new mode 100755 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 115949.png b/assets/images/career/wireframes/Screenshot 2025-10-31 115949.png old mode 100644 new mode 100755 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 120038.png b/assets/images/career/wireframes/Screenshot 2025-10-31 120038.png old mode 100644 new mode 100755 diff --git a/assets/images/career/wireframes/Screenshot 2025-10-31 120203.png b/assets/images/career/wireframes/Screenshot 2025-10-31 120203.png old mode 100644 new mode 100755 diff --git a/assets/images/gr.png b/assets/images/gr.png old mode 100644 new mode 100755 diff --git a/assets/images/hzone/patpat.png b/assets/images/hzone/patpat.png old mode 100644 new mode 100755 diff --git a/assets/images/lima-third-meeting.jpg b/assets/images/lima-third-meeting.jpg old mode 100644 new mode 100755 diff --git a/assets/images/org-roam-ui-graph.png b/assets/images/org-roam-ui-graph.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/02-12-25-retros.jpg b/assets/images/reviews/02-12-25-retros.jpg old mode 100644 new mode 100755 diff --git a/assets/images/reviews/07-12-25-daily-retros.jpg b/assets/images/reviews/07-12-25-daily-retros.jpg old mode 100644 new mode 100755 diff --git a/assets/images/reviews/15-11-2025-guac.png b/assets/images/reviews/15-11-2025-guac.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/16-11-25-daily-retros.jpg b/assets/images/reviews/16-11-25-daily-retros.jpg old mode 100644 new mode 100755 diff --git a/assets/images/reviews/23-11-2025-netdata.png b/assets/images/reviews/23-11-2025-netdata.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/23-11-2025-reader.png b/assets/images/reviews/23-11-2025-reader.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/23-11-2025-vnc.png b/assets/images/reviews/23-11-2025-vnc.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/23-11-25-daily-retros.jpg b/assets/images/reviews/23-11-25-daily-retros.jpg old mode 100644 new mode 100755 diff --git a/assets/images/reviews/30-11-2025-acutal.png b/assets/images/reviews/30-11-2025-acutal.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/30-11-25-daily-retros.jpg b/assets/images/reviews/30-11-25-daily-retros.jpg old mode 100644 new mode 100755 diff --git a/assets/images/reviews/PDT-bruno-vehicle-web-api.png b/assets/images/reviews/PDT-bruno-vehicle-web-api.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/c-sharp-vehicle-breakpoints.png b/assets/images/reviews/c-sharp-vehicle-breakpoints.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/competencies/competencies-09-11-25.png b/assets/images/reviews/competencies/competencies-09-11-25.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/competencies/dev-plan-09-11-25.png b/assets/images/reviews/competencies/dev-plan-09-11-25.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/competency-powerbi-report.png b/assets/images/reviews/competency-powerbi-report.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/journeys-api-side-by-side-upgrade.png b/assets/images/reviews/journeys-api-side-by-side-upgrade.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/probation-objectives/probation-objectives-1.png b/assets/images/reviews/probation-objectives/probation-objectives-1.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/probation-objectives/probation-objectives-2.png b/assets/images/reviews/probation-objectives/probation-objectives-2.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/probation-objectives/probation-objectives-3.png b/assets/images/reviews/probation-objectives/probation-objectives-3.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/probation-objectives/probation-objectives-4.png b/assets/images/reviews/probation-objectives/probation-objectives-4.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/probation-objectives/probation-objectives-5.png b/assets/images/reviews/probation-objectives/probation-objectives-5.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/probation-objectives/rotation-calendar.png b/assets/images/reviews/probation-objectives/rotation-calendar.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/timesheets/2026-03-15-timesheet.png b/assets/images/reviews/timesheets/2026-03-15-timesheet.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/timesheets/2026-03-22-timesheet.png b/assets/images/reviews/timesheets/2026-03-22-timesheet.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/timesheets/2026-03-29-timesheet.png b/assets/images/reviews/timesheets/2026-03-29-timesheet.png new file mode 100755 index 0000000..37deb0d Binary files /dev/null and b/assets/images/reviews/timesheets/2026-03-29-timesheet.png differ diff --git a/assets/images/reviews/timesheets/timesheet-01-02.png b/assets/images/reviews/timesheets/timesheet-01-02.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/timesheets/timesheet-01-03-26.png b/assets/images/reviews/timesheets/timesheet-01-03-26.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/timesheets/timesheet-08-02.png b/assets/images/reviews/timesheets/timesheet-08-02.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/timesheets/timesheet-08-03-26.png b/assets/images/reviews/timesheets/timesheet-08-03-26.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/timesheets/timesheet-15-02-26.png b/assets/images/reviews/timesheets/timesheet-15-02-26.png old mode 100644 new mode 100755 diff --git a/assets/images/reviews/timesheets/timesheet-22-02-26.png b/assets/images/reviews/timesheets/timesheet-22-02-26.png old mode 100644 new mode 100755 diff --git a/assets/images/syntax/assets.png b/assets/images/syntax/assets.png old mode 100644 new mode 100755 diff --git a/assets/images/syntax/filebrowser.png b/assets/images/syntax/filebrowser.png old mode 100644 new mode 100755 diff --git a/assets/images/syntax/not_clutter.png b/assets/images/syntax/not_clutter.png old mode 100644 new mode 100755 diff --git a/assets/rnote/07-12-2025-retrospectives.rnote b/assets/rnote/07-12-2025-retrospectives.rnote old mode 100644 new mode 100755 diff --git a/assets/rnote/09-11-2025-nginx-architecture.rnote b/assets/rnote/09-11-2025-nginx-architecture.rnote old mode 100644 new mode 100755 diff --git a/assets/rnote/09-11-2025-retrospectives.rnote b/assets/rnote/09-11-2025-retrospectives.rnote old mode 100644 new mode 100755 diff --git a/assets/rnote/10-08-2025-weekly-review.rnote b/assets/rnote/10-08-2025-weekly-review.rnote old mode 100644 new mode 100755 diff --git a/assets/rnote/2025-08-08-emacs.rnote b/assets/rnote/2025-08-08-emacs.rnote old mode 100644 new mode 100755 diff --git a/assets/rnote/23-11-2025-retrospectives.rnote b/assets/rnote/23-11-2025-retrospectives.rnote old mode 100644 new mode 100755 diff --git a/assets/rnote/30-11-2025-retrospectives.rnote b/assets/rnote/30-11-2025-retrospectives.rnote old mode 100644 new mode 100755 diff --git a/assets/rnote/retros-template.rnote b/assets/rnote/retros-template.rnote old mode 100644 new mode 100755 diff --git a/assets/scripts/bigger-picture.min.js b/assets/scripts/bigger-picture.min.js old mode 100644 new mode 100755 diff --git a/assets/scripts/comments.js b/assets/scripts/comments.js old mode 100644 new mode 100755 diff --git a/assets/scripts/competency-status-board.js b/assets/scripts/competency-status-board.js old mode 100644 new mode 100755 diff --git a/assets/scripts/gallery-init.js b/assets/scripts/gallery-init.js old mode 100644 new mode 100755 diff --git a/assets/scripts/lunr.js b/assets/scripts/lunr.js old mode 100644 new mode 100755 diff --git a/assets/scripts/mermaid.min.js b/assets/scripts/mermaid.min.js old mode 100644 new mode 100755 diff --git a/assets/scripts/notes.js b/assets/scripts/notes.js old mode 100644 new mode 100755 diff --git a/assets/scripts/script.js b/assets/scripts/script.js old mode 100644 new mode 100755 diff --git a/assets/scripts/search.js b/assets/scripts/search.js old mode 100644 new mode 100755 diff --git a/assets/scripts/sitemap-interactive.js b/assets/scripts/sitemap-interactive.js old mode 100644 new mode 100755 diff --git a/assets/scripts/svg-pan-zoom.min.js b/assets/scripts/svg-pan-zoom.min.js old mode 100644 new mode 100755 diff --git a/assets/scripts/wird-tracker.js b/assets/scripts/wird-tracker.js old mode 100644 new mode 100755 index a256a35..fc9e389 --- a/assets/scripts/wird-tracker.js +++ b/assets/scripts/wird-tracker.js @@ -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 = `No entries yet.`; @@ -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 ` + ${meta.label}
+ / ${meta.target}${unitHint} + `; + }).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 `—`; + } + + 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}/5`; + } else { + display = Number(val).toLocaleString(); + } + + return `${display}`; + }).join(""); + + return ` + ${displayDate}${isToday ? " ·" : ""} + ${cells} + `; + }).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 `${metCount}d (${pct}%)`; + }).join(""); + + wrap.innerHTML = ` + + + + + ${thCols} + + + ${bodyRows} + + + + ${footCells} + + +
Date
days met
+ `; + } + + // 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; diff --git a/assets/scripts/wird-tracker.sync-conflict-20260325-231828-NE5VEIB.js b/assets/scripts/wird-tracker.sync-conflict-20260325-231828-NE5VEIB.js deleted file mode 100644 index 3856c4d..0000000 --- a/assets/scripts/wird-tracker.sync-conflict-20260325-231828-NE5VEIB.js +++ /dev/null @@ -1,1236 +0,0 @@ -/* ============================================================ - wird-tracker.js (full replacement) - ============================================================ */ - -(function () { - "use strict"; - - const API = "/api/wird"; - - // type: "count" | "juz" | "min" | "rating" | "nafl" | "khatm" - const DAILY_WIRD = { - durood: { label: "Durood", type: "count", unit: "count", target: 500 }, - istighfar: { label: "Istighfar", type: "count", unit: "count", target: 200 }, - quran: { label: "Qurʾān", type: "juz", unit: "juz", target: 3 }, - muraqabah: { label: "Murāqabah", type: "min", unit: "min", target: 10 }, - wuqoof_qalbi: { label: "Wuqūf Qalbī", type: "rating", unit: "", target: 3 }, - }; - - // Nafl prayers — stored in wird_entries, value=1 when done - const NAFL_WIRD = { - salatul_tawbah: { label: "Ṣalāt al-Tawbah", type: "nafl", unit: "", target: 1, rakats: 2, desc: "2 rakʿāt · repentance", icon: "🤲" }, - salatul_hajaat: { label: "Ṣalāt al-Ḥājah", type: "nafl", unit: "", target: 1, rakats: 2, desc: "2 rakʿāt · need & supplication", icon: "🌙" }, - tahajjud: { label: "Tahajjud", type: "nafl", unit: "", target: 1, rakats: 0, desc: "night vigil", icon: "⭐" }, - }; - - const WIRD_META = { - ...DAILY_WIRD, - ...NAFL_WIRD, - shaykh_meeting: { label: "Meeting w/ Shaykh", type: "meeting", unit: "meeting", target: 1 }, - khatm: { label: "Khatm", type: "khatm", unit: "", target: 1 }, - motalah: { label: "Mutālaʿah", type: "motalah", unit: "min", target: 30 }, - }; - - const RATING_LABELS = { - 1: "distracted", 2: "scattered", 3: "present", 4: "attentive", 5: "absorbed", - }; - - const KHATM_TYPES = { - tilawah: "Tilāwah", - hifz: "Ḥifẓ", - tadabbur: "Tadabbur", - }; - - const MEETING_CYCLE_DAYS = 21; - const HEATMAP_WEEKS = 52; - - const $ = (sel, ctx = document) => ctx.querySelector(sel); - const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)]; - const isoDate = (d = new Date()) => d.toISOString().slice(0, 10); - - function daysBetween(a, b) { - return Math.round((new Date(b) - new Date(a)) / 86400000); - } - function addDays(iso, n) { - const d = new Date(iso); d.setDate(d.getDate() + n); return isoDate(d); - } - function fmtDisplay(iso) { - return new Date(iso + "T00:00:00").toLocaleDateString("en-GB", { - day: "numeric", month: "short", year: "numeric", - }); - } - - function fmtValue(type, val) { - const meta = WIRD_META[type]; - if (!meta) return val; - if (meta.type === "rating") return val ? `${RATING_LABELS[val] || val} (${val}/5)` : "—"; - if (meta.type === "meeting") return val >= 1 ? "✓ attended" : "✗ missed"; - if (meta.type === "nafl") return val >= 1 ? "✓ prayed" : "—"; - if (meta.type === "khatm") return val >= 1 ? "✓ completed" : "—"; - return `${Number(val).toLocaleString()} ${meta.unit}`; - } - - function statusClass(type, val) { - const meta = WIRD_META[type]; - if (!meta || !val || val <= 0) return ""; - if (meta.type === "rating") { - if (val >= 4) return "over"; - if (val >= 3) return "done"; - return "miss"; - } - if (val >= meta.target * 1.1) return "over"; - if (val >= meta.target) return "done"; - return "miss"; - } - - // ── Mutālaʿah (Study) ───────────────────────────────────── - - const MOTALAH_API = "/api/wird/motalah"; - const CALIBRE_API = "/api/calibre/books"; // see controller below - const MOTALAH_TARGET = 30; // daily target in minutes - - let motalahEntries = []; - let calibreBooks = []; - - async function loadMotalah() { - try { - const [mRes, bRes] = await Promise.all([ - fetch(MOTALAH_API), - fetch(CALIBRE_API), - ]); - if (mRes.ok) motalahEntries = await mRes.json(); - if (bRes.ok) calibreBooks = await bRes.json(); - } catch (e) { - console.warn("Mutalaah load error:", e); - } - } - - function renderMotalah() { - const today = isoDate(); - const todayM = motalahEntries.filter(e => e.date === today); - const todayMins = todayM.reduce((s, e) => s + Number(e.durationMinutes), 0); - - // streak - let streak = 0; - const now = new Date(); - for (let i = 0; i < 365; i++) { - const d = new Date(now); d.setDate(d.getDate() - i); - const iso = isoDate(d); - const mins = motalahEntries.filter(e => e.date === iso) - .reduce((s, e) => s + Number(e.durationMinutes), 0); - if (mins >= MOTALAH_TARGET) streak++; - else if (i > 0) break; - } - - // month total - const monthStart = today.slice(0, 7); - const monthMins = motalahEntries - .filter(e => e.date.startsWith(monthStart)) - .reduce((s, e) => s + Number(e.durationMinutes), 0); - const monthHrs = (monthMins / 60).toFixed(1); - - // unique books touched (all time) - const allBooks = new Set( - motalahEntries.flatMap(e => e.bookIds || []) - ); - - $("#motalah-today-mins").textContent = todayMins || "0"; - $("#motalah-streak").textContent = streak; - $("#motalah-total-hrs").textContent = monthHrs; - $("#motalah-books-count").textContent = allBooks.size; - - // sessions list (20 most recent) - const wrap = $("#motalah-sessions-wrap"); - const sorted = [...motalahEntries].sort((a, b) => b.date.localeCompare(a.date)).slice(0, 20); - - if (sorted.length === 0) { - wrap.innerHTML = `

No study sessions logged yet.

`; - return; - } - - wrap.innerHTML = sorted.map(e => { - const bookNames = (e.bookIds || []) - .map(id => { - const b = calibreBooks.find(b => String(b.id) === String(id)); - return b ? `${b.title}` : ""; - }).join(""); - - const note = e.notes - ? ` — ${e.notes}` - : ""; - - return ` -
- ${e.date} - ${e.durationMinutes} min - ${bookNames || ""}${note} -
- `; - }).join(""); - } - - // Modal - const motalahModal = $("#motalah-modal"); - const motalahForm = $("#motalah-form"); - const motalahDateEl = $("#motalah-date"); - const motalahDurEl = $("#motalah-duration"); - const motalahBooksEl = $("#motalah-books"); - const motalahNotesEl = $("#motalah-notes"); - - // ── Book search UI ──────────────────────────────────────── - // selectedBookIds: Set of ids currently picked in the modal - let selectedBookIds = new Set(); - - const bookSearchInput = document.getElementById("motalah-book-search"); - const bookSearchResults = document.getElementById("book-search-results"); - const selectedChipsWrap = document.getElementById("selected-books-chips"); - - function bookById(id) { - return calibreBooks.find(b => String(b.id) === String(id)); - } - - function renderSelectedChips() { - selectedChipsWrap.innerHTML = [...selectedBookIds].map(id => { - const b = bookById(id); - const label = b ? b.title : id; - return ` - ${label} - - `; - }).join(""); - - // Sync hidden select so the existing submit handler still works - motalahBooksEl.innerHTML = [...selectedBookIds] - .map(id => ``).join(""); - } - - function renderBookResults(query) { - const q = query.trim().toLowerCase(); - const sorted = [...calibreBooks].sort((a, b) => a.title.localeCompare(b.title)); - const filtered = q - ? sorted.filter(b => - b.title.toLowerCase().includes(q) || - (b.authors || "").toLowerCase().includes(q) - ) - : sorted; - - if (filtered.length === 0) { - bookSearchResults.innerHTML = `
No books found
`; - } else { - bookSearchResults.innerHTML = filtered.slice(0, 40).map(b => { - const sel = selectedBookIds.has(String(b.id)); - return `
-
${sel ? "✓" : ""}
- ${b.title} - ${b.authors ? `${b.authors}` : ""} -
`; - }).join(""); - } - bookSearchResults.classList.add("open"); - } - - if (bookSearchInput) { - bookSearchInput.addEventListener("input", () => { - if (calibreBooks.length === 0) return; - renderBookResults(bookSearchInput.value); - }); - - bookSearchInput.addEventListener("focus", () => { - if (calibreBooks.length === 0) return; - renderBookResults(bookSearchInput.value); - }); - - bookSearchResults.addEventListener("mousedown", e => { - const item = e.target.closest(".book-result-item"); - if (!item) return; - e.preventDefault(); // prevent input blur before we register click - const id = String(item.dataset.id); - if (selectedBookIds.has(id)) { - selectedBookIds.delete(id); - } else { - selectedBookIds.add(id); - } - renderSelectedChips(); - renderBookResults(bookSearchInput.value); - }); - } - - if (selectedChipsWrap) { - selectedChipsWrap.addEventListener("click", e => { - const btn = e.target.closest("button[data-id]"); - if (!btn) return; - selectedBookIds.delete(String(btn.dataset.id)); - renderSelectedChips(); - // Refresh dropdown if open - if (bookSearchResults.classList.contains("open")) { - renderBookResults(bookSearchInput.value); - } - }); - } - - // Close dropdown on outside click - document.addEventListener("click", e => { - if (!e.target.closest(".book-search-wrap")) { - bookSearchResults.classList.remove("open"); - } - }); - - function populateBookSelect() { - // no-op — book search handles population dynamically - } - - function openMotalahModal() { - motalahForm.reset(); - motalahDateEl.value = isoDate(); - selectedBookIds = new Set(); - bookSearchInput.value = ""; - bookSearchResults.classList.remove("open"); - bookSearchResults.innerHTML = ""; - renderSelectedChips(); - motalahModal.showModal(); - } - if (motalahForm) { - motalahForm.addEventListener("submit", async e => { - e.preventDefault(); - const selectedBooks = [...selectedBookIds]; - const payload = { - date: motalahDateEl.value, - durationMinutes: parseInt(motalahDurEl.value), - bookIds: selectedBooks, - notes: motalahNotesEl.value.trim() || null, - }; - try { - const r = await fetch(MOTALAH_API, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - if (!r.ok) throw new Error(await r.text()); - const saved = await r.json(); - motalahEntries.unshift(saved); - motalahModal.close(); - renderMotalah(); - } catch (err) { - alert("Error saving session: " + err.message); - } - }); - } - - $("#motalah-cancel").addEventListener("click", () => motalahModal.close()); - $("#log-motalah-btn").addEventListener("click", openMotalahModal); - - // ── State ──────────────────────────────────────────────── - let allEntries = []; - let todayMap = {}; - let meetingLog = []; - let chart = null; - - // ── Boot ───────────────────────────────────────────────── - document.addEventListener("DOMContentLoaded", async () => { - setTodayLabel(); - await Promise.all([loadAll(), loadMotalah()]); - renderToday(); - renderNafl(); - renderMeetingPanel(); - renderKhatm(); - renderHistory(); - renderChart(); - renderMotalah(); - renderHeatmap(); - bindControls(); - }); - - function setTodayLabel() { - $("#today-date").textContent = new Date().toLocaleDateString("en-GB", { - weekday: "long", day: "numeric", month: "long", year: "numeric", - }); - } - - async function loadAll() { - try { - const r = await fetch(`${API}/entries`); - if (!r.ok) throw new Error(r.status); - allEntries = await r.json(); - buildTodayMap(); - buildMeetingLog(); - } catch (e) { - console.error("Wird API error:", e); - } - } - - function buildTodayMap() { - const today = isoDate(); - todayMap = {}; - for (const e of allEntries) { - if (e.date !== today) continue; - if (WIRD_META[e.wirdType]?.type === "rating") { - const existing = todayMap[e.wirdType]; - if (!existing || e.createdAt > existing.createdAt) { - todayMap[e.wirdType] = Number(e.value); - } - } else { - todayMap[e.wirdType] = (todayMap[e.wirdType] || 0) + Number(e.value); - } - } - } - - function buildMeetingLog() { - meetingLog = allEntries - .filter(e => e.wirdType === "shaykh_meeting" && Number(e.value) >= 1) - .sort((a, b) => b.date.localeCompare(a.date)); - } - - async function postEntry(payload) { - const r = await fetch(`${API}/entries`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - if (!r.ok) throw new Error(await r.text()); - return r.json(); - } - - async function deleteEntry(id) { - const r = await fetch(`${API}/entries/${id}`, { method: "DELETE" }); - if (!r.ok) throw new Error(await r.text()); - } - - // ════════════════════════════════════════════════════════ - // TODAY PANEL - // ════════════════════════════════════════════════════════ - - function renderToday() { - const wrap = $("#wird-cards"); - wrap.innerHTML = ""; - let done = 0; - const types = Object.keys(DAILY_WIRD); - - for (const type of types) { - const meta = DAILY_WIRD[type]; - const val = todayMap[type] || 0; - const cls = statusClass(type, val); - if (cls === "done" || cls === "over") done++; - - const card = document.createElement("div"); - card.className = `wird-card ${cls}`; - card.dataset.type = type; - - if (meta.type === "rating") { - card.innerHTML = buildRatingCard(meta, val); - } else { - const pct = Math.min((val / meta.target) * 100, 100); - card.innerHTML = ` -
${meta.label}
-
- ${fmtValue(type, val)} - / ${meta.target.toLocaleString()} ${meta.unit} -
-
-
-
- - `; - } - - card.querySelector(".card-log-btn").addEventListener("click", () => openModal(type)); - wrap.appendChild(card); - } - - const pct = Math.round((done / types.length) * 100); - $("#today-progress-fill").style.width = pct + "%"; - $("#today-progress-label").textContent = `${done} / ${types.length} complete`; - } - - function buildRatingCard(meta, val) { - const pips = [1, 2, 3, 4, 5].map(n => { - const filled = val >= n; - const isMin = n === meta.target; - return ``; - }).join(""); - const lbl = val ? `${RATING_LABELS[val]} (${val}/5)` : "not logged"; - return ` -
${meta.label}
-
${pips}
-
${lbl}
- - `; - } - - // ════════════════════════════════════════════════════════ - // LOG MODAL (existing — unchanged logic) - // ════════════════════════════════════════════════════════ - - const modal = $("#log-modal"); - const form = $("#log-form"); - const typeEl = $("#log-type"); - const valEl = $("#log-value"); - const dateEl = $("#log-date"); - const notesEl = $("#log-notes"); - const ratingEl = $("#log-rating-value"); - - let selectedRating = 0; - $$("#modal-pips .modal-pip").forEach(btn => { - btn.addEventListener("click", () => { - selectedRating = parseInt(btn.dataset.val); - ratingEl.value = selectedRating; - $$("#modal-pips .modal-pip").forEach(b => { - b.classList.toggle("selected", parseInt(b.dataset.val) <= selectedRating); - }); - }); - }); - - function openModal(preType = null) { - form.reset(); - selectedRating = 0; - ratingEl.value = ""; - $$("#modal-pips .modal-pip").forEach(b => b.classList.remove("selected")); - dateEl.value = isoDate(); - if (preType) typeEl.value = preType; - updateModalFields(); - modal.showModal(); - } - - function openMeetingModal() { - form.reset(); - dateEl.value = isoDate(); - typeEl.value = "shaykh_meeting"; - updateModalFields(); - modal.showModal(); - } - - function updateModalFields() { - const type = typeEl.value; - const isRating = type === "wuqoof_qalbi"; - const isMeeting = type === "shaykh_meeting"; - $("#value-group").style.display = (!isRating && !isMeeting) ? "" : "none"; - $("#rating-group").style.display = isRating ? "" : "none"; - $("#shaykh-group").style.display = isMeeting ? "" : "none"; - if (!isRating && !isMeeting) { - const meta = DAILY_WIRD[type]; - if (meta) valEl.placeholder = `Target: ${meta.target} ${meta.unit}`; - } - } - - if (typeEl){ - typeEl.addEventListener("change", updateModalFields); - } - $("#modal-cancel").addEventListener("click", () => modal.close()); - - if (form){ - form.addEventListener("submit", async (e) => { - e.preventDefault(); - const type = typeEl.value; - const isRating = type === "wuqoof_qalbi"; - const isMtg = type === "shaykh_meeting"; - - if (isRating && !ratingEl.value) { alert("Please select a rating."); return; } - - const payload = { - wirdType: type, - date: dateEl.value, - value: isRating ? Number(ratingEl.value) : isMtg ? 1 : Number(valEl.value), - notes: notesEl.value.trim() || null, - }; - - try { - const saved = await postEntry(payload); - allEntries.push(saved); - buildTodayMap(); - buildMeetingLog(); - renderToday(); - renderNafl(); - renderMeetingPanel(); - renderHistory(); - renderChart(); - renderHeatmap(); - modal.close(); - } catch (err) { - alert("Could not save entry: " + err.message); - } - }); - } - // ════════════════════════════════════════════════════════ - // NAFL PRAYERS - // Stored in wird_entries: wirdType = "salatul_tawbah" | - // "salatul_hajaat" | "tahajjud", value = 1 when prayed. - // Toggle = delete existing today entry, or post new one. - // ════════════════════════════════════════════════════════ - - function renderNafl() { - const today = isoDate(); - - // Build a map of which nafl are done today and their entry id - const naflToday = {}; // { wirdType: { id, done: true } } - for (const e of allEntries) { - if (e.date !== today) continue; - if (!NAFL_WIRD[e.wirdType]) continue; - if (Number(e.value) >= 1) naflToday[e.wirdType] = e.id; - } - - const done = Object.keys(naflToday).length; - const total = Object.keys(NAFL_WIRD).length; - - // Render each card - for (const [type, meta] of Object.entries(NAFL_WIRD)) { - const card = document.getElementById(`nafl-${type}`); - if (!card) continue; - const isDone = !!naflToday[type]; - card.classList.toggle("nafl-done", isDone); - const btn = card.querySelector(".nafl-toggle"); - btn.textContent = isDone ? "✓ prayed" : "Mark as prayed"; - btn.dataset.type = type; - btn.dataset.entryId = naflToday[type] || ""; - } - - // Progress bar — track width = 120px for 3/3 - const barPx = Math.round((done / total) * 120); - $("#nafl-completion-bar").style.width = barPx + "px"; - $("#nafl-completion-label").textContent = `${done} / ${total} today`; - } - - // Single delegated listener on the nafl-cards container - $("#nafl-cards").addEventListener("click", async e => { - const btn = e.target.closest(".nafl-toggle"); - if (!btn) return; - - const type = btn.dataset.type; - const entryId = btn.dataset.entryId; - const today = isoDate(); - - btn.disabled = true; - try { - if (entryId) { - // Toggle OFF — delete - await deleteEntry(entryId); - allEntries = allEntries.filter(en => String(en.id) !== String(entryId)); - } else { - // Toggle ON — post - const saved = await postEntry({ wirdType: type, date: today, value: 1, notes: null }); - allEntries.push(saved); - } - buildTodayMap(); - renderNafl(); - renderHeatmap(); - } catch (err) { - alert("Could not update: " + err.message); - } finally { - btn.disabled = false; - } - }); - - // ════════════════════════════════════════════════════════ - // MEETING PANEL (unchanged) - // ════════════════════════════════════════════════════════ - - function renderMeetingPanel() { - const today = isoDate(); - const last = meetingLog[0] || null; - const lastDate = last ? last.date : null; - const nextDate = lastDate ? addDays(lastDate, MEETING_CYCLE_DAYS) : null; - const daysToNext = nextDate ? daysBetween(today, nextDate) : null; - - let statusHtml = ""; - if (!lastDate) { - statusHtml = `No meetings logged yet`; - } else if (daysToNext > 0) { - statusHtml = `Next due in ${daysToNext} day${daysToNext !== 1 ? "s" : ""} — ${fmtDisplay(nextDate)}`; - } else if (daysToNext === 0) { - statusHtml = `Due today`; - } else { - const ov = Math.abs(daysToNext); - statusHtml = `${ov} day${ov !== 1 ? "s" : ""} overdue — expected ${fmtDisplay(nextDate)}`; - } - - const cycles = buildCycleInsights(); - const cycleRows = cycles.map(c => ` - - ${fmtDisplay(c.start)} → ${fmtDisplay(c.end)} - ${c.met - ? `✓ met — ${fmtDisplay(c.metOn)}` - : c.ongoing - ? `ongoing` - : `✗ missed`} - - ${c.met ? `day ${c.dayOfCycle}` : "—"} - - `).join(""); - - $("#meeting-panel-body").innerHTML = ` -
-
- Last meeting - ${lastDate ? fmtDisplay(lastDate) : "—"} -
-
- Next expected - ${nextDate ? fmtDisplay(nextDate) : "—"} -
-
${statusHtml}
-
-
-

3-week cycles

- ${cycles.length === 0 - ? `

Log at least one meeting to see cycle insights.

` - : ` - - ${cycleRows} -
Cycle windowStatusWhen
`} -
- `; - } - - function buildCycleInsights() { - if (meetingLog.length === 0) return []; - const dates = [...meetingLog].reverse().map(e => e.date); - const today = isoDate(); - const cycles = []; - let start = dates[0]; - while (start <= today) { - const end = addDays(start, MEETING_CYCLE_DAYS - 1); - const hit = dates.find(d => d >= start && d <= end); - cycles.push({ - start, end, - met: !!hit, - metOn: hit || null, - dayOfCycle: hit ? daysBetween(start, hit) + 1 : null, - ongoing: end > today, - }); - start = addDays(end, 1); - } - return cycles.reverse(); - } - - // ════════════════════════════════════════════════════════ - // KHATM TRACKER - // Stored in wird_entries: wirdType="khatm", value=1. - // notes field carries JSON: {"type":"tilawah"} or plain text. - // ════════════════════════════════════════════════════════ - - function getKhatmEntries() { - return [...allEntries] - .filter(e => e.wirdType === "khatm" && Number(e.value) >= 1) - .sort((a, b) => a.date.localeCompare(b.date)); // asc for numbering - } - - function renderKhatm() { - const entries = getKhatmEntries(); - const total = entries.length; - - const thisYear = entries.filter( - e => e.date.startsWith(String(new Date().getFullYear())) - ).length; - - let avgDays = "—"; - if (entries.length >= 2) { - let gap = 0; - for (let i = 1; i < entries.length; i++) - gap += daysBetween(entries[i - 1].date, entries[i].date); - avgDays = Math.round(gap / (entries.length - 1)); - } - - const last = entries[entries.length - 1]; - - $("#khatm-total").textContent = total || "0"; - $("#khatm-this-year").textContent = thisYear || "0"; - $("#khatm-avg-days").textContent = avgDays; - $("#khatm-last").textContent = last ? fmtDisplay(last.date) : "—"; - - const listEl = $("#khatm-list"); - if (entries.length === 0) { - listEl.innerHTML = `

No completions logged yet.

`; - return; - } - - // Render newest first - const display = [...entries].reverse(); - listEl.innerHTML = display.map(e => { - const num = entries.findIndex(x => x.id === e.id) + 1; - const milestone = num % 10 === 0; - const prevEntry = entries[num - 2]; // ascending, so num-2 is previous - const gapTxt = prevEntry ? `+${daysBetween(prevEntry.date, e.date)}d` : "first"; - - // Parse recitation type out of notes if stored as JSON - let recType = "", noteText = e.notes || ""; - try { - const parsed = JSON.parse(e.notes); - recType = parsed.type || ""; - noteText = parsed.notes || ""; - } catch (_) { /* notes is plain text */ } - - const typeBadge = recType - ? `${KHATM_TYPES[recType] || recType}` - : ""; - const note = noteText - ? `${noteText}` - : ""; - - return ` -
-
${num}
- ${fmtDisplay(e.date)} - ${typeBadge} - ${gapTxt} - ${note} -
- `; - }).join(""); - } - - // Khatm modal - const khatmModal = $("#khatm-modal"); - const khatmForm = $("#khatm-form"); - const khatmDateEl = $("#khatm-date"); - const khatmTypeEl = $("#khatm-recitation"); - const khatmNotesEl = $("#khatm-notes"); - - $("#log-khatm-btn").addEventListener("click", () => { - khatmForm.reset(); - khatmDateEl.value = isoDate(); - khatmModal.showModal(); - }); - $("#khatm-cancel").addEventListener("click", () => khatmModal.close()); - - khatmForm.addEventListener("submit", async e => { - e.preventDefault(); - // Pack recitation type + notes into the notes field as JSON - const recType = khatmTypeEl.value || ""; - const noteText = khatmNotesEl.value.trim(); - const notes = (recType || noteText) - ? JSON.stringify({ type: recType || undefined, notes: noteText || undefined }) - : null; - - try { - const saved = await postEntry({ - wirdType: "khatm", - date: khatmDateEl.value, - value: 1, - notes, - }); - allEntries.push(saved); - khatmModal.close(); - renderKhatm(); - renderHeatmap(); - } catch (err) { - alert("Error saving khatm: " + err.message); - } - }); - - // ════════════════════════════════════════════════════════ - // HISTORY TABLE (filter out nafl + khatm to keep it clean, - // or you can remove the filter if you want them shown) - // ════════════════════════════════════════════════════════ - - function renderHistory() { - const tbody = $("#history-body"); - tbody.innerHTML = ""; - - 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)); - - if (sorted.length === 0) { - tbody.innerHTML = `No entries yet.`; - return; - } - - for (const e of sorted) { - const meta = WIRD_META[e.wirdType] || {}; - const cls = statusClass(e.wirdType, e.value); - const badgeTxt = cls === "over" ? (meta.type === "rating" ? "flourishing" : "above") - : cls === "done" ? (meta.type === "rating" ? "present" : "met") - : cls === "miss" ? "below" - : "—"; - const tr = document.createElement("tr"); - tr.innerHTML = ` - ${e.date} - ${meta.label || e.wirdType} - ${fmtValue(e.wirdType, e.value)} - ${badgeTxt} - ${e.notes || ""} - `; - tbody.appendChild(tr); - } - } - - // ════════════════════════════════════════════════════════ - // TREND CHART (unchanged) - // ════════════════════════════════════════════════════════ - - function renderChart() { - const type = $("#trend-type").value; - const days = parseInt($("#trend-range").value); - const meta = WIRD_META[type]; - const isRating = meta.type === "rating"; - const now = new Date(); - const labels = []; - const vals = []; - - const isMotalah = type === "motalah"; - for (let i = days - 1; i >= 0; i--) { - const d = new Date(now); - d.setDate(d.getDate() - i); - const iso = isoDate(d); - labels.push(iso.slice(5)); - if (isMotalah) { - vals.push(motalahEntries.filter(e => e.date === iso) - .reduce((s, e) => s + Number(e.durationMinutes), 0)); - } else { - const dayEntries = allEntries.filter(e => e.date === iso && e.wirdType === type); - if (isRating) { - const last = dayEntries.sort((a, b) => a.createdAt > b.createdAt ? 1 : -1).pop(); - vals.push(last ? Number(last.value) : 0); - } else { - vals.push(dayEntries.reduce((s, e) => s + Number(e.value), 0)); - } - } - } - - const nonZero = vals.filter(v => v > 0); - const avg = nonZero.length - ? (isRating - ? (nonZero.reduce((a, b) => a + b, 0) / nonZero.length).toFixed(1) - : Math.round(nonZero.reduce((a, b) => a + b, 0) / nonZero.length)) - : 0; - const max = nonZero.length ? Math.max(...nonZero) : 0; - const streak = calcStreak(type); - - if (isRating) { - $("#trend-stats").innerHTML = ` -
${avg}avg rating
-
${RATING_LABELS[max] || "—"}best day
-
${streak}dstreak ≥ ${meta.target}
-
${nonZero.length}days logged
- `; - } else { - $("#trend-stats").innerHTML = ` -
${Number(avg).toLocaleString()}avg / day
-
${Number(max).toLocaleString()}best day
-
${streak}dcurrent streak
-
${meta.target.toLocaleString()}daily target
- `; - } - - const cs = getComputedStyle(document.documentElement); - const accent = cs.getPropertyValue("--wird-accent").trim() || "#7c5c3a"; - const gridColor = cs.getPropertyValue("--border").trim() || "#d7d7d7"; - const tickColor = cs.getPropertyValue("--muted").trim() || "#666"; - const ctx = $("#trend-chart").getContext("2d"); - if (chart) chart.destroy(); - - chart = new Chart(ctx, { - type: "line", - data: { - labels, - datasets: [ - { - label: meta.label, - data: vals, - borderColor: accent, - backgroundColor: accent + "22", - fill: true, tension: .35, pointRadius: 3, pointHoverRadius: 5, - }, - { - label: "Target", - data: vals.map(() => meta.target), - borderColor: "rgba(74,124,89,.5)", - borderDash: [5, 4], - pointRadius: 0, fill: false, tension: 0, - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: true, - plugins: { - legend: { display: false }, - tooltip: { - callbacks: { - label: ctx => { - const v = ctx.parsed.y; - if (isRating && ctx.datasetIndex === 0) - return `${meta.label}: ${RATING_LABELS[v] || v} (${v}/5)`; - return `${ctx.dataset.label}: ${v}`; - }, - }, - }, - }, - scales: { - x: { - grid: { color: gridColor + "22" }, - ticks: { color: tickColor, font: { family: "monospace", size: 10 }, maxTicksLimit: 10 }, - }, - y: { - grid: { color: gridColor + "22" }, - ticks: { - color: tickColor, - font: { family: "monospace", size: 10 }, - ...(isRating ? { - min: 0, max: 5, stepSize: 1, - callback: v => RATING_LABELS[v] || (v === 0 ? "" : v), - } : {}), - }, - beginAtZero: true, - ...(isRating ? { min: 0, max: 5 } : {}), - }, - }, - }, - }); - } - - function calcStreak(type) { - const meta = WIRD_META[type]; - let streak = 0; - const now = new Date(); - for (let i = 0; i < 365; i++) { - const d = new Date(now); - d.setDate(d.getDate() - i); - const iso = isoDate(d); - let total; - if (type === "motalah") { - total = motalahEntries.filter(e => e.date === iso) - .reduce((s, e) => s + Number(e.durationMinutes), 0); - } else { - const entries = allEntries.filter(e => e.date === iso && e.wirdType === type); - if (meta && meta.type === "rating") { - const last = entries.sort((a, b) => a.createdAt > b.createdAt ? 1 : -1).pop(); - total = last ? Number(last.value) : 0; - } else { - total = entries.reduce((s, e) => s + Number(e.value), 0); - } - } - const target = type === "motalah" ? MOTALAH_TARGET : (meta ? meta.target : 1); - if (total >= target) streak++; - else if (i > 0) break; - } - return streak; - } - - // ════════════════════════════════════════════════════════ - // HEATMAP - // Reads allEntries directly. No extra API call. - // ════════════════════════════════════════════════════════ - - function renderHeatmap() { - const type = $("#heatmap-type").value; - const today = new Date(); - const todayIso = isoDate(today); - - // Anchor: Sunday on or before today, then go back (HEATMAP_WEEKS-1) full weeks - const anchor = new Date(today); - anchor.setDate(anchor.getDate() - anchor.getDay()); - const startDate = new Date(anchor); - startDate.setDate(startDate.getDate() - (HEATMAP_WEEKS - 1) * 7); - - const scoreMap = buildHeatmapScores(type, isoDate(startDate), todayIso); - - // ── Month labels ────────────────────────────────────── - const monthLabelEl = $("#heatmap-month-labels"); - monthLabelEl.innerHTML = ""; - let lastMonth = null; - // 15px per col (12px cell + 3px gap); labels are position:absolute - for (let w = 0; w < HEATMAP_WEEKS; w++) { - const d = new Date(startDate); - d.setDate(d.getDate() + w * 7); - const mo = d.toLocaleDateString("en-GB", { month: "short" }); - if (mo !== lastMonth) { - const lbl = document.createElement("span"); - lbl.className = "hm-month-lbl"; - lbl.style.left = w * 15 + "px"; - lbl.textContent = mo; - monthLabelEl.appendChild(lbl); - lastMonth = mo; - } - } - - // ── Grid ───────────────────────────────────────────── - const gridEl = $("#heatmap-grid"); - gridEl.innerHTML = ""; - - // Day-of-week label column - const dowCol = document.createElement("div"); - dowCol.className = "hm-dow-col"; - ["", "Mon", "", "Wed", "", "Fri", ""].forEach(lbl => { - const el = document.createElement("div"); - el.className = "hm-dow-lbl"; - el.textContent = lbl; - dowCol.appendChild(el); - }); - gridEl.appendChild(dowCol); - - for (let w = 0; w < HEATMAP_WEEKS; w++) { - const col = document.createElement("div"); - col.className = "hm-col"; - for (let d = 0; d < 7; d++) { - const cellDate = new Date(startDate); - cellDate.setDate(cellDate.getDate() + w * 7 + d); - const iso = isoDate(cellDate); - - const cell = document.createElement("div"); - cell.className = "hm-cell"; - - if (iso > todayIso) { - cell.classList.add("hm-empty"); - } else { - const score = scoreMap[iso] || 0; - cell.dataset.level = scoreToLevel(score, type); - if (iso === todayIso) cell.classList.add("hm-today"); - cell.title = buildTooltip(iso, score, type); - } - col.appendChild(cell); - } - gridEl.appendChild(col); - } - - // ── Stats ───────────────────────────────────────────── - renderHeatmapStats(scoreMap, type, isoDate(startDate), todayIso); - } - - function buildHeatmapScores(type, startIso, endIso) { - // Returns { "2025-01-14": score } where score is a 0–1+ fraction - // (against the relevant target) so scoreToLevel() is consistent. - const map = {}; - - if (type === "__all__") { - // Fraction of DAILY_WIRD targets met each day - const dayCounts = {}; // { date: { type: total } } - for (const e of allEntries) { - if (e.date < startIso || e.date > endIso) continue; - if (!DAILY_WIRD[e.wirdType]) continue; - if (!dayCounts[e.date]) dayCounts[e.date] = {}; - const meta = DAILY_WIRD[e.wirdType]; - if (meta.type === "rating") { - dayCounts[e.date][e.wirdType] = Math.max( - dayCounts[e.date][e.wirdType] || 0, Number(e.value) - ); - } else { - dayCounts[e.date][e.wirdType] = (dayCounts[e.date][e.wirdType] || 0) + Number(e.value); - } - } - const total = Object.keys(DAILY_WIRD).length; - for (const [date, typeMap] of Object.entries(dayCounts)) { - let met = 0; - for (const [t, val] of Object.entries(typeMap)) { - if (val >= DAILY_WIRD[t].target) met++; - } - map[date] = met / total; - } - return map; - } - - // Nafl: fraction of 3 prayers done - if (type === "nafl") { - for (const e of allEntries) { - if (!NAFL_WIRD[e.wirdType]) continue; - if (e.date < startIso || e.date > endIso) continue; - if (Number(e.value) >= 1) map[e.date] = (map[e.date] || 0) + (1 / 3); - } - return map; - } - - // Khatm: binary - if (type === "khatm") { - for (const e of allEntries) { - if (e.wirdType !== "khatm") continue; - if (e.date < startIso || e.date > endIso) continue; - if (Number(e.value) >= 1) map[e.date] = 1; - } - return map; - } - - // Motalah: use motalahEntries, normalised against MOTALAH_TARGET - if (type === "motalah") { - for (const e of motalahEntries) { - if (e.date < startIso || e.date > endIso) continue; - map[e.date] = (map[e.date] || 0) + Number(e.durationMinutes); - } - for (const d of Object.keys(map)) map[d] = map[d] / MOTALAH_TARGET; - return map; - } - - // Single wird type — normalised against target - const meta = WIRD_META[type]; - for (const e of allEntries) { - if (e.wirdType !== type) continue; - if (e.date < startIso || e.date > endIso) continue; - if (meta && meta.type === "rating") { - map[e.date] = Math.max(map[e.date] || 0, Number(e.value)); - } else { - map[e.date] = (map[e.date] || 0) + Number(e.value); - } - } - if (meta && meta.target) { - for (const d of Object.keys(map)) map[d] = map[d] / meta.target; - } - return map; - } - - function scoreToLevel(score, type) { - if (type === "khatm") return score > 0 ? 4 : 0; - if (score <= 0) return 0; - if (score < 0.4) return 1; - if (score < 0.75) return 2; - if (score < 1.0) return 3; - return 4; // met or exceeded - } - - function buildTooltip(iso, score, type) { - const label = fmtDisplay(iso); - if (score <= 0) return `${label}: nothing logged`; - if (type === "__all__") return `${label}: ${Math.round(score * 100)}% of targets met`; - if (type === "nafl") return `${label}: ${Math.round(score * 3)} / 3 prayers`; - if (type === "khatm") return `${label}: khatm completed ✓`; - if (type === "motalah") return `${label}: ${Math.round(score * MOTALAH_TARGET)} min`; - const meta = WIRD_META[type]; - if (!meta) return label; - if (meta.type === "rating") { - const r = Math.round(score); - return `${label}: ${RATING_LABELS[r] || r}/5`; - } - const raw = score * (meta.target || 1); - return `${label}: ${Math.round(raw).toLocaleString()} ${meta.unit}`; - } - - function renderHeatmapStats(scoreMap, type, startIso, endIso) { - const el = $("#heatmap-stats"); - const daysLogged = Object.values(scoreMap).filter(v => v > 0).length; - const daysTarget = Object.values(scoreMap).filter(v => v >= 1).length; - - // Longest streak of any-activity - let longest = 0, cur = 0; - const s = new Date(startIso), e = new Date(endIso); - for (let d = new Date(s); d <= e; d.setDate(d.getDate() + 1)) { - if ((scoreMap[isoDate(d)] || 0) > 0) { cur++; longest = Math.max(longest, cur); } - else cur = 0; - } - - const chips = [ - { val: daysLogged, lbl: "days with activity" }, - { val: `${longest}d`, lbl: "longest streak" }, - ]; - if (type !== "khatm") { - chips.push({ val: daysTarget, lbl: type === "nafl" ? "days all prayers done" : "days target met" }); - } - - el.innerHTML = chips.map(c => - `
- ${c.val} - ${c.lbl} -
` - ).join(""); - } - - // ════════════════════════════════════════════════════════ - // BIND CONTROLS - // ════════════════════════════════════════════════════════ - - function bindControls() { - $("#open-log-btn").addEventListener("click", () => openModal()); - $("#log-meeting-btn").addEventListener("click", openMeetingModal); - $("#trend-type").addEventListener("change", renderChart); - $("#trend-range").addEventListener("change", renderChart); - $("#heatmap-type").addEventListener("change", renderHeatmap); - } - -})(); diff --git a/assets/scripts/zhd.js b/assets/scripts/zhd.js new file mode 100755 index 0000000..3b38234 --- /dev/null +++ b/assets/scripts/zhd.js @@ -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
  • elements containing 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
  • 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