Wave changes

This commit is contained in:
2026-03-27 15:40:37 +00:00
parent 7fbe850756
commit 529acd4fcc
241 changed files with 8664 additions and 1256 deletions

0
.gitignore vendored Executable file → Normal file
View File

0
.packages/archives/elpa/archive-contents Executable file → Normal file
View File

0
.packages/archives/elpa/archive-contents.signed Executable file → Normal file
View File

0
.packages/archives/melpa/archive-contents Executable file → Normal file
View File

0
.packages/htmlize-20250724.1703/htmlize-autoloads.el Executable file → Normal file
View File

0
.packages/htmlize-20250724.1703/htmlize-pkg.el Executable file → Normal file
View File

0
.packages/htmlize-20250724.1703/htmlize.el Executable file → Normal file
View File

0
.packages/htmlize-20250724.1703/htmlize.elc Executable file → Normal file
View File

5
.projectile Normal file
View File

@@ -0,0 +1,5 @@
-.venv/
-output/
-backup/
-*~
-.packages/

10
Makefile Executable file → Normal file
View File

@@ -4,7 +4,7 @@ VENV := .venv
PY := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
all: fix-permissions clean-output build search clean-venv
all: clean-output build search clean-venv
build:
@echo "Building project..."
@@ -34,7 +34,11 @@ norm:
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:
@@ -45,4 +49,6 @@ help:
@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 help - Show this help message"
@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!"

View File

@@ -0,0 +1,54 @@
.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!"

0
README.md Executable file → Normal file
View File

0
assets/New Document.rnote Executable file → Normal file
View File

0
assets/emacs-calibre.rnote Executable file → Normal file
View File

0
assets/icons/apple-touch-icon.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

0
assets/icons/icons8-favicon-50.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

0
assets/icons/icons8-film-tape-100.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

0
assets/icons/icons8-z-key-64.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

0
assets/images/1_to_2.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 929 KiB

After

Width:  |  Height:  |  Size: 929 KiB

View File

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 291 KiB

View File

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 508 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

View File

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

0
assets/images/gr.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

0
assets/images/hzone/patpat.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 203 KiB

0
assets/images/org-roam-ui-graph.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 296 KiB

0
assets/images/reviews/02-12-25-retros.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 284 KiB

0
assets/images/reviews/07-12-25-daily-retros.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

0
assets/images/reviews/15-11-2025-guac.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

0
assets/images/reviews/16-11-25-daily-retros.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

0
assets/images/reviews/23-11-2025-netdata.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

0
assets/images/reviews/23-11-2025-reader.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

0
assets/images/reviews/23-11-2025-vnc.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 783 KiB

After

Width:  |  Height:  |  Size: 783 KiB

0
assets/images/reviews/23-11-25-daily-retros.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

0
assets/images/reviews/30-11-2025-acutal.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

0
assets/images/reviews/30-11-25-daily-retros.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

0
assets/images/reviews/PDT-bruno-vehicle-web-api.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

0
assets/images/reviews/c-sharp-vehicle-breakpoints.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 438 KiB

After

Width:  |  Height:  |  Size: 438 KiB

View File

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 254 KiB

View File

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 230 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

0
assets/images/reviews/timesheets/timesheet-01-02.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

0
assets/images/reviews/timesheets/timesheet-08-02.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

0
assets/images/syntax/assets.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

0
assets/images/syntax/filebrowser.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 564 KiB

After

Width:  |  Height:  |  Size: 564 KiB

0
assets/images/syntax/not_clutter.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

0
assets/rnote/07-12-2025-retrospectives.rnote Executable file → Normal file
View File

0
assets/rnote/09-11-2025-nginx-architecture.rnote Executable file → Normal file
View File

0
assets/rnote/09-11-2025-retrospectives.rnote Executable file → Normal file
View File

0
assets/rnote/10-08-2025-weekly-review.rnote Executable file → Normal file
View File

0
assets/rnote/2025-08-08-emacs.rnote Executable file → Normal file
View File

0
assets/rnote/23-11-2025-retrospectives.rnote Executable file → Normal file
View File

0
assets/rnote/30-11-2025-retrospectives.rnote Executable file → Normal file
View File

0
assets/rnote/retros-template.rnote Executable file → Normal file
View File

0
assets/scripts/bigger-picture.min.js vendored Executable file → Normal file
View File

0
assets/scripts/comments.js Executable file → Normal file
View File

0
assets/scripts/competency-status-board.js Executable file → Normal file
View File

0
assets/scripts/gallery-init.js Executable file → Normal file
View File

0
assets/scripts/lunr.js Executable file → Normal file
View File

2811
assets/scripts/mermaid.min.js vendored Normal file

File diff suppressed because one or more lines are too long

0
assets/scripts/notes.js Executable file → Normal file
View File

61
assets/scripts/script.js Executable file → Normal file
View File

@@ -225,3 +225,64 @@ document.addEventListener("DOMContentLoaded", () => {
});
// Make Mermaid diagrams zoomable
window.addEventListener('load', function() {
// Initialize Mermaid (if not already)
mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
// Find all Mermaid divs
document.querySelectorAll('.mermaid').forEach(el => {
// Decode HTML entities
el.innerHTML = el.innerHTML
.replace(/>/g, '>')
.replace(/&lt;/g, '<')
.replace(/&amp;/g, '&');
// Wrap in container if not already wrapped
if (!el.parentElement.classList.contains('mermaid-container')) {
const container = document.createElement('div');
container.classList.add('mermaid-container');
// Add zoom controls
const controls = document.createElement('div');
controls.classList.add('mermaid-zoom-controls');
controls.innerHTML = `
<button class="zoom-in">+</button>
<button class="zoom-out">-</button>
`;
container.appendChild(controls);
// Move the mermaid diagram into container
container.appendChild(el.cloneNode(true));
el.replaceWith(container);
// Select the SVG inside
const svg = container.querySelector('.mermaid svg');
let scale = 1;
// Zoom functions
controls.querySelector('.zoom-in').addEventListener('click', () => {
scale += 0.1;
svg.style.transform = `scale(${scale})`;
});
controls.querySelector('.zoom-out').addEventListener('click', () => {
scale = Math.max(0.1, scale - 0.1);
svg.style.transform = `scale(${scale})`;
});
// Optional: scroll wheel zoom
container.addEventListener('wheel', (e) => {
if (e.ctrlKey) { // zoom only with ctrl+wheel
e.preventDefault();
if (e.deltaY < 0) scale += 0.05;
else scale = Math.max(0.1, scale - 0.05);
svg.style.transform = `scale(${scale})`;
}
});
}
});
// Finally, render Mermaid diagrams
mermaid.run({ querySelector: '.mermaid' });
});

0
assets/scripts/search.js Executable file → Normal file
View File

0
assets/scripts/svg-pan-zoom.min.js vendored Executable file → Normal file
View File

View File

@@ -174,8 +174,8 @@
const motalahForm = $("#motalah-form");
const motalahDateEl = $("#motalah-date");
const motalahDurEl = $("#motalah-duration");
const motalahBooksEl= $("#motalah-books");
const motalahNotesEl= $("#motalah-notes");
const motalahBooksEl = $("#motalah-books");
const motalahNotesEl = $("#motalah-notes");
// ── Book search UI ────────────────────────────────────────
// selectedBookIds: Set of ids currently picked in the modal
@@ -229,6 +229,7 @@
bookSearchResults.classList.add("open");
}
if (bookSearchInput) {
bookSearchInput.addEventListener("input", () => {
if (calibreBooks.length === 0) return;
renderBookResults(bookSearchInput.value);
@@ -252,7 +253,9 @@
renderSelectedChips();
renderBookResults(bookSearchInput.value);
});
}
if (selectedChipsWrap) {
selectedChipsWrap.addEventListener("click", e => {
const btn = e.target.closest("button[data-id]");
if (!btn) return;
@@ -263,6 +266,7 @@
renderBookResults(bookSearchInput.value);
}
});
}
// Close dropdown on outside click
document.addEventListener("click", e => {
@@ -285,7 +289,7 @@
renderSelectedChips();
motalahModal.showModal();
}
if (motalahForm) {
motalahForm.addEventListener("submit", async e => {
e.preventDefault();
const selectedBooks = [...selectedBookIds];
@@ -310,6 +314,7 @@
alert("Error saving session: " + err.message);
}
});
}
$("#motalah-cancel").addEventListener("click", () => motalahModal.close());
$("#log-motalah-btn").addEventListener("click", openMotalahModal);
@@ -323,14 +328,14 @@
// ── Boot ─────────────────────────────────────────────────
document.addEventListener("DOMContentLoaded", async () => {
setTodayLabel();
await Promise.all([loadAll(), loadMotalah()]); // ← change this line
await Promise.all([loadAll(), loadMotalah()]);
renderToday();
renderNafl();
renderMeetingPanel();
renderKhatm();
renderHistory();
renderChart();
renderMotalah(); // ← add this
renderMotalah();
renderHeatmap();
bindControls();
});
@@ -437,7 +442,7 @@
}
function buildRatingCard(meta, val) {
const pips = [1,2,3,4,5].map(n => {
const pips = [1, 2, 3, 4, 5].map(n => {
const filled = val >= n;
const isMin = n === meta.target;
return `<span class="card-pip ${filled ? "filled" : ""} ${isMin ? "min-marker" : ""}"
@@ -507,9 +512,12 @@
}
}
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;
@@ -541,7 +549,7 @@
alert("Could not save entry: " + err.message);
}
});
}
// ════════════════════════════════════════════════════════
// NAFL PRAYERS
// Stored in wird_entries: wirdType = "salatul_tawbah" |
@@ -717,7 +725,7 @@
if (entries.length >= 2) {
let gap = 0;
for (let i = 1; i < entries.length; i++)
gap += daysBetween(entries[i-1].date, entries[i].date);
gap += daysBetween(entries[i - 1].date, entries[i].date);
avgDays = Math.round(gap / (entries.length - 1));
}
@@ -871,7 +879,7 @@
} 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();
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));
@@ -882,8 +890,8 @@
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))
? (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);
@@ -927,7 +935,7 @@
label: "Target",
data: vals.map(() => meta.target),
borderColor: "rgba(74,124,89,.5)",
borderDash: [5,4],
borderDash: [5, 4],
pointRadius: 0, fill: false, tension: 0,
},
],
@@ -986,7 +994,7 @@
} 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();
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);
@@ -1021,8 +1029,7 @@
const monthLabelEl = $("#heatmap-month-labels");
monthLabelEl.innerHTML = "";
let lastMonth = null;
// 15px per col (12px cell + 3px gap)
let colsPassed = 0;
// 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);
@@ -1030,10 +1037,9 @@
if (mo !== lastMonth) {
const lbl = document.createElement("span");
lbl.className = "hm-month-lbl";
lbl.style.paddingLeft = (w - colsPassed) * 15 + "px";
lbl.style.left = w * 15 + "px";
lbl.textContent = mo;
monthLabelEl.appendChild(lbl);
colsPassed = w;
lastMonth = mo;
}
}
@@ -1226,5 +1232,415 @@
$("#trend-range").addEventListener("change", renderChart);
$("#heatmap-type").addEventListener("change", renderHeatmap);
}
/* ============================================================
wird-export.js
Client-side PDF export for the Wird Tracker.
Depends on: jsPDF + jspdf-autotable (loaded via CDN).
All data is read from allEntries / motalahEntries already
held in memory by wird-tracker.js — no extra API calls.
HOW TO INTEGRATE
─────────────────
1. Paste this block INSIDE the existing IIFE in
wird-tracker.js, just before the closing })();
2. Add the HTML snippet (wird-export-html.html) to the
template and the CDN <script> tags after chart.js.
============================================================ */
// ── Export modal elements ─────────────────────────────────
const exportModal = document.getElementById("export-modal");
const exportForm = document.getElementById("export-form");
const exportFromEl = document.getElementById("export-from");
const exportToEl = document.getElementById("export-to");
const exportBtnLabel = document.getElementById("export-btn-label");
const exportSubmitBtn = document.getElementById("export-submit");
// ── Open / close ──────────────────────────────────────────
document.getElementById("export-pdf-btn").addEventListener("click", () => {
const today = isoDate();
const thirtyDaysAgo = (() => {
const d = new Date(); d.setDate(d.getDate() - 30); return isoDate(d);
})();
exportFromEl.value = thirtyDaysAgo;
exportToEl.value = today;
exportModal.showModal();
});
document.getElementById("export-cancel").addEventListener("click", () => exportModal.close());
// ── Preset buttons ────────────────────────────────────────
document.querySelectorAll(".export-preset").forEach(btn => {
btn.addEventListener("click", () => {
const days = parseInt(btn.dataset.preset);
const to = new Date();
const from = new Date();
if (days === 365) {
// "This year" = Jan 1 of current year
from.setMonth(0, 1);
} else {
from.setDate(from.getDate() - (days - 1));
}
exportFromEl.value = isoDate(from);
exportToEl.value = isoDate(to);
// Visual feedback on active preset
document.querySelectorAll(".export-preset").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
});
});
// Clear active preset when user manually changes dates
[exportFromEl, exportToEl].forEach(el => {
el.addEventListener("change", () => {
document.querySelectorAll(".export-preset").forEach(b => b.classList.remove("active"));
});
});
// ── Generate PDF ──────────────────────────────────────────
exportForm.addEventListener("submit", async e => {
e.preventDefault();
const fromIso = exportFromEl.value;
const toIso = exportToEl.value;
if (!fromIso || !toIso || fromIso > toIso) {
alert("Please select a valid date range.");
return;
}
const includeNafl = document.getElementById("export-include-nafl").checked;
const includeKhatm = document.getElementById("export-include-khatm").checked;
const includeMotalah = document.getElementById("export-include-motalah").checked;
// Loading state
exportSubmitBtn.disabled = true;
exportBtnLabel.textContent = "Generating…";
try {
await generatePDF({ fromIso, toIso, includeNafl, includeKhatm, includeMotalah });
exportModal.close();
} catch (err) {
console.error("PDF export error:", err);
alert("Could not generate PDF: " + err.message);
} finally {
exportSubmitBtn.disabled = false;
exportBtnLabel.textContent = "Download PDF";
}
});
// ── Core PDF generation ───────────────────────────────────
async function generatePDF({ fromIso, toIso, includeNafl, includeKhatm, includeMotalah }) {
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
const PAGE_W = 210;
const MARGIN = 16;
const COL_W = PAGE_W - MARGIN * 2;
const ACCENT = [124, 92, 58]; // --wird-accent (light mode safe)
const MUTED = [130, 130, 130];
const FG = [30, 30, 30];
const BORDER = [220, 215, 208];
// ── Cover / header ─────────────────────────────────────
doc.setFillColor(...ACCENT);
doc.rect(0, 0, PAGE_W, 28, "F");
doc.setFont("helvetica", "bold");
doc.setFontSize(16);
doc.setTextColor(255, 255, 255);
doc.text("Wird Tracker — Export", MARGIN, 16);
doc.setFont("helvetica", "normal");
doc.setFontSize(8);
doc.setTextColor(255, 255, 255, 0.75);
doc.text(
`${fmtDisplay(fromIso)}${fmtDisplay(toIso)} · generated ${fmtDisplay(isoDate())}`,
MARGIN, 23
);
let cursorY = 38;
// ── Helper: section heading ────────────────────────────
function sectionHeading(title, y) {
doc.setFillColor(...BORDER);
doc.rect(MARGIN, y, COL_W, 7, "F");
doc.setFont("helvetica", "bold");
doc.setFontSize(8.5);
doc.setTextColor(...ACCENT);
doc.text(title.toUpperCase(), MARGIN + 3, y + 5);
return y + 10;
}
// ── Helper: summary stat row ───────────────────────────
function statRow(label, value, y) {
doc.setFont("helvetica", "normal");
doc.setFontSize(8.5);
doc.setTextColor(...MUTED);
doc.text(label, MARGIN + 3, y);
doc.setFont("helvetica", "bold");
doc.setTextColor(...FG);
doc.text(String(value), MARGIN + 60, y);
return y + 6;
}
// ── Helper: autoTable wrapper ──────────────────────────
function addTable(columns, rows, startY) {
doc.autoTable({
startY,
head: [columns.map(c => c.header)],
body: rows,
margin: { left: MARGIN, right: MARGIN },
styles: {
font: "helvetica",
fontSize: 8,
cellPadding: 2.5,
textColor: FG,
lineColor: BORDER,
lineWidth: 0.2,
overflow: "linebreak",
},
headStyles: {
fillColor: BORDER,
textColor: ACCENT,
fontStyle: "bold",
fontSize: 7.5,
},
alternateRowStyles: { fillColor: [250, 248, 245] },
columnStyles: columns.reduce((acc, c, i) => {
if (c.width) acc[i] = { cellWidth: c.width };
return acc;
}, {}),
});
return doc.lastAutoTable.finalY + 8;
}
// ── 1. Daily Wirds summary ─────────────────────────────
cursorY = sectionHeading("Daily Awrād", cursorY);
// Build per-day totals for the range
const dayMap = {}; // { "2025-01-14": { durood: 300, istighfar: 100, ... } }
for (const e of allEntries) {
if (e.date < fromIso || e.date > toIso) continue;
if (!WIRD_META[e.wirdType]) continue;
if (!includeNafl && NAFL_WIRD[e.wirdType]) continue;
if (!includeKhatm && e.wirdType === "khatm") continue;
if (!dayMap[e.date]) dayMap[e.date] = {};
const meta = WIRD_META[e.wirdType];
if (meta && meta.type === "rating") {
dayMap[e.date][e.wirdType] = Math.max(
dayMap[e.date][e.wirdType] || 0, Number(e.value)
);
} else {
dayMap[e.date][e.wirdType] = (dayMap[e.date][e.wirdType] || 0) + Number(e.value);
}
}
// Build summary stats
const daysInRange = Object.keys(dayMap).length;
const dailyTypes = Object.keys(DAILY_WIRD);
const daysAllMet = Object.values(dayMap).filter(dm =>
dailyTypes.every(t => (dm[t] || 0) >= DAILY_WIRD[t].target)
).length;
cursorY = statRow("Date range", `${fmtDisplay(fromIso)}${fmtDisplay(toIso)}`, cursorY);
cursorY = statRow("Days with entries", daysInRange, cursorY);
cursorY = statRow("Days all targets met", daysAllMet, cursorY);
cursorY += 3;
// Per-wird totals table
const summaryRows = dailyTypes.map(type => {
const meta = DAILY_WIRD[type];
const values = Object.values(dayMap).map(dm => dm[type] || 0).filter(v => v > 0);
const total = meta.type === "rating"
? (values.length ? (values.reduce((a,b)=>a+b,0)/values.length).toFixed(1) + " avg" : "—")
: values.reduce((a,b)=>a+b,0).toLocaleString();
const days = values.length;
const target = meta.type === "rating"
? `${meta.target} / 5`
: `${meta.target.toLocaleString()} ${meta.unit}`;
const metDays = meta.type === "rating"
? values.filter(v => v >= meta.target).length
: values.filter(v => v >= meta.target).length;
return [meta.label, total, target, `${metDays} / ${days}`, days ? `${Math.round(metDays/days*100)}%` : "—"];
});
cursorY = addTable(
[
{ header: "Wird", width: 42 },
{ header: "Total / Avg", width: 30 },
{ header: "Target", width: 35 },
{ header: "Days Met", width: 25 },
{ header: "Hit %", width: 20 },
],
summaryRows,
cursorY
);
// ── 2. Detailed daily log ──────────────────────────────
if (doc.lastAutoTable.finalY > 200) {
doc.addPage();
cursorY = 18;
}
cursorY = sectionHeading("Detailed Daily Log", cursorY);
const logEntries = [...allEntries]
.filter(e => {
if (e.date < fromIso || e.date > toIso) return false;
if (!WIRD_META[e.wirdType]) return false;
if (!includeNafl && NAFL_WIRD[e.wirdType]) return false;
if (!includeKhatm && e.wirdType === "khatm") return false;
return true;
})
.sort((a, b) => b.date.localeCompare(a.date));
const HIDE_IN_LOG = new Set();
if (!includeNafl) Object.keys(NAFL_WIRD).forEach(k => HIDE_IN_LOG.add(k));
if (!includeKhatm) HIDE_IN_LOG.add("khatm");
const logRows = logEntries
.filter(e => !HIDE_IN_LOG.has(e.wirdType))
.map(e => {
const meta = WIRD_META[e.wirdType];
const cls = statusClass(e.wirdType, Number(e.value));
const vs = cls === "over" ? "above" : cls === "done" ? "met" : cls === "miss" ? "below" : "—";
return [
e.date,
meta ? meta.label : e.wirdType,
fmtValue(e.wirdType, e.value),
vs,
e.notes ? e.notes.replace(/\n/g, " ").slice(0, 60) : "",
];
});
cursorY = addTable(
[
{ header: "Date", width: 22 },
{ header: "Wird", width: 40 },
{ header: "Amount", width: 35 },
{ header: "vs Target", width: 22 },
{ header: "Notes", width: null },
],
logRows.length ? logRows : [["—", "No entries in range", "", "", ""]],
cursorY
);
// ── 3. Nafl section ────────────────────────────────────
if (includeNafl) {
if (doc.lastAutoTable.finalY > 200) { doc.addPage(); cursorY = 18; }
else { cursorY = doc.lastAutoTable.finalY + 6; }
cursorY = sectionHeading("Nafl Prayers", cursorY);
const naflRows = allEntries
.filter(e => NAFL_WIRD[e.wirdType] && e.date >= fromIso && e.date <= toIso && Number(e.value) >= 1)
.sort((a,b) => b.date.localeCompare(a.date))
.map(e => [e.date, NAFL_WIRD[e.wirdType]?.label || e.wirdType, "✓ prayed"]);
cursorY = addTable(
[
{ header: "Date", width: 28 },
{ header: "Prayer", width: 60 },
{ header: "Status", width: 30 },
],
naflRows.length ? naflRows : [["—", "No nafl prayers in range", ""]],
cursorY
);
}
// ── 4. Khatm section ───────────────────────────────────
if (includeKhatm) {
if (doc.lastAutoTable.finalY > 200) { doc.addPage(); cursorY = 18; }
else { cursorY = doc.lastAutoTable.finalY + 6; }
cursorY = sectionHeading("Khatm — Qurʾān Completions", cursorY);
const khatmEntries = allEntries
.filter(e => e.wirdType === "khatm" && e.date >= fromIso && e.date <= toIso && Number(e.value) >= 1)
.sort((a,b) => b.date.localeCompare(a.date));
const khatmRows = khatmEntries.map(e => {
let recType = "", noteText = e.notes || "";
try {
const parsed = JSON.parse(e.notes);
recType = KHATM_TYPES[parsed.type] || parsed.type || "";
noteText = parsed.notes || "";
} catch (_) {}
return [e.date, recType || "—", noteText.slice(0, 60) || "—"];
});
cursorY = addTable(
[
{ header: "Date", width: 28 },
{ header: "Type", width: 40 },
{ header: "Notes", width: null },
],
khatmRows.length ? khatmRows : [["—", "No khatms in range", ""]],
cursorY
);
}
// ── 5. Mutālaʿah section ──────────────────────────────
if (includeMotalah) {
if (doc.lastAutoTable.finalY > 200) { doc.addPage(); cursorY = 18; }
else { cursorY = doc.lastAutoTable.finalY + 6; }
cursorY = sectionHeading("Mutālaʿah — Study Sessions", cursorY);
const mInRange = motalahEntries
.filter(e => e.date >= fromIso && e.date <= toIso)
.sort((a,b) => b.date.localeCompare(a.date));
const totalMins = mInRange.reduce((s,e) => s + Number(e.durationMinutes), 0);
cursorY = statRow("Sessions", mInRange.length, cursorY);
cursorY = statRow("Total study time", `${totalMins} min (${(totalMins/60).toFixed(1)} hrs)`, cursorY);
cursorY += 3;
const motalahRows = mInRange.map(e => {
const bookList = (e.bookIds || [])
.map(id => {
const b = calibreBooks.find(b => String(b.id) === String(id));
return b ? b.title : String(id);
}).join(", ");
return [
e.date,
`${e.durationMinutes} min`,
bookList.slice(0, 50) || "—",
(e.notes || "").slice(0, 50) || "—",
];
});
cursorY = addTable(
[
{ header: "Date", width: 22 },
{ header: "Duration", width: 22 },
{ header: "Books", width: 70 },
{ header: "Notes", width: null },
],
motalahRows.length ? motalahRows : [["—", "No sessions in range", "", ""]],
cursorY
);
}
// ── Footer on every page ──────────────────────────────
const pageCount = doc.internal.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.setFont("helvetica", "normal");
doc.setFontSize(7);
doc.setTextColor(...MUTED);
doc.text(
`Wird Tracker · Page ${i} of ${pageCount}`,
PAGE_W / 2, 292,
{ align: "center" }
);
}
// ── Save ───────────────────────────────────────────────
const filename = `wird-${fromIso}-to-${toIso}.pdf`;
doc.save(filename);
}
})();

File diff suppressed because it is too large Load Diff

0
assets/sketchnotes/07-12-2025-retrospectives.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 568 KiB

After

Width:  |  Height:  |  Size: 568 KiB

0
assets/sketchnotes/09-11-2025-nginx-architecture.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

0
assets/sketchnotes/09-11-2025-retrospectives.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1024 KiB

After

Width:  |  Height:  |  Size: 1024 KiB

0
assets/sketchnotes/10-08-2025-weekly-review.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Some files were not shown because too many files have changed in this diff Show More