/* ui-gods.js — Divine Pantheons section * Individual deity browser: Greek, Roman, Egyptian, Hebrew divine names, Archangels. * Kabbalah paths are shown only as a reference at the bottom of each detail view. */ (() => { // ── State ────────────────────────────────────────────────────────────────── const state = { initialized: false, gods: [], godsByName: new Map(), monthRefsByGodId: new Map(), filteredGods: [], selectedId: null, activeTab: "greek", searchQuery: "" }; let listEl, detailNameEl, detailSubEl, detailBodyEl, countEl, searchInputEl, searchClearEl; // ── Tab definitions ──────────────────────────────────────────────────────── const TABS = [ { id: "greek", label: "Greek", emoji: "🏛️" }, { id: "roman", label: "Roman", emoji: "🦅" }, { id: "egyptian", label: "Egyptian", emoji: "𓂀" }, { id: "hebrew", label: "God Names", emoji: "✡️" }, { id: "archangel", label: "Archangels", emoji: "☀️" }, { id: "all", label: "All", emoji: "∞" }, ]; const PANTHEON_LABEL = { greek: "Greek", roman: "Roman", egyptian: "Egyptian", hebrew: "God Names", archangel: "Archangel" }; function normalizeName(value) { return String(value || "") .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase() .replace(/[^a-z0-9]+/g, " ") .trim(); } function tokenizeEquivalent(value) { return String(value || "") .replace(/\([^)]*\)/g, " ") .split(/\/|,|;|\bor\b|\band\b|·|—|–/i) .map((token) => token.trim()) .filter(Boolean); } function findEquivalentTarget(equivalent, currentGodId) { const tokens = tokenizeEquivalent(equivalent); for (const token of tokens) { const matches = state.godsByName.get(normalizeName(token)); if (!matches?.length) continue; const target = matches.find((x) => x.id !== currentGodId); if (target) return target; } return null; } function buildMonthReferencesByGod(referenceData) { const map = new Map(); const months = Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : []; const holidays = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : []; const monthById = new Map(months.map((month) => [month.id, month])); function parseMonthDayToken(value) { const text = String(value || "").trim(); const match = text.match(/^(\d{1,2})-(\d{1,2})$/); if (!match) { return null; } const monthNo = Number(match[1]); const dayNo = Number(match[2]); if (!Number.isInteger(monthNo) || !Number.isInteger(dayNo) || monthNo < 1 || monthNo > 12 || dayNo < 1 || dayNo > 31) { return null; } return { month: monthNo, day: dayNo }; } function parseMonthDayTokensFromText(value) { const text = String(value || ""); const matches = [...text.matchAll(/(\d{1,2})-(\d{1,2})/g)]; return matches .map((match) => ({ month: Number(match[1]), day: Number(match[2]) })) .filter((token) => Number.isInteger(token.month) && Number.isInteger(token.day) && token.month >= 1 && token.month <= 12 && token.day >= 1 && token.day <= 31); } function toDateToken(token, year) { if (!token) { return null; } return new Date(year, token.month - 1, token.day, 12, 0, 0, 0); } function splitMonthDayRangeByMonth(startToken, endToken) { const startDate = toDateToken(startToken, 2025); const endBase = toDateToken(endToken, 2025); if (!startDate || !endBase) { return []; } const wrapsYear = endBase.getTime() < startDate.getTime(); const endDate = wrapsYear ? toDateToken(endToken, 2026) : endBase; if (!endDate) { return []; } const segments = []; let cursor = new Date(startDate); while (cursor.getTime() <= endDate.getTime()) { const monthEnd = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0, 12, 0, 0, 0); const segmentEnd = monthEnd.getTime() < endDate.getTime() ? monthEnd : endDate; segments.push({ monthNo: cursor.getMonth() + 1, startDay: cursor.getDate(), endDay: segmentEnd.getDate() }); cursor = new Date(segmentEnd.getFullYear(), segmentEnd.getMonth(), segmentEnd.getDate() + 1, 12, 0, 0, 0); } return segments; } function tokenToString(monthNo, dayNo) { return `${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`; } function formatRangeLabel(monthName, startDay, endDay) { if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) { return monthName; } if (startDay === endDay) { return `${monthName} ${startDay}`; } return `${monthName} ${startDay}-${endDay}`; } function resolveRangeForMonth(month, options = {}) { const monthOrder = Number(month?.order); const monthStart = parseMonthDayToken(month?.start); const monthEnd = parseMonthDayToken(month?.end); if (!Number.isFinite(monthOrder) || !monthStart || !monthEnd) { return { startToken: String(month?.start || "").trim() || null, endToken: String(month?.end || "").trim() || null, label: month?.name || month?.id || "", isFullMonth: true }; } let startToken = parseMonthDayToken(options.startToken); let endToken = parseMonthDayToken(options.endToken); if (!startToken || !endToken) { const tokens = parseMonthDayTokensFromText(options.rawDateText); if (tokens.length >= 2) { startToken = tokens[0]; endToken = tokens[1]; } else if (tokens.length === 1) { startToken = tokens[0]; endToken = tokens[0]; } } if (!startToken || !endToken) { startToken = monthStart; endToken = monthEnd; } const segments = splitMonthDayRangeByMonth(startToken, endToken); const segment = segments.find((entry) => entry.monthNo === monthOrder) || null; const useStart = segment ? { month: monthOrder, day: segment.startDay } : startToken; const useEnd = segment ? { month: monthOrder, day: segment.endDay } : endToken; const startText = tokenToString(useStart.month, useStart.day); const endText = tokenToString(useEnd.month, useEnd.day); const isFullMonth = startText === month.start && endText === month.end; return { startToken: startText, endToken: endText, label: isFullMonth ? (month.name || month.id) : formatRangeLabel(month.name || month.id, useStart.day, useEnd.day), isFullMonth }; } function pushRef(godId, month, options = {}) { if (!godId || !month?.id) return; const key = String(godId).trim().toLowerCase(); if (!key) return; if (!map.has(key)) { map.set(key, []); } const rows = map.get(key); const range = resolveRangeForMonth(month, options); const rowKey = `${month.id}|${range.startToken || ""}|${range.endToken || ""}`; if (rows.some((entry) => entry.key === rowKey)) { return; } rows.push({ id: month.id, name: month.name || month.id, order: Number.isFinite(Number(month.order)) ? Number(month.order) : 999, label: range.label, startToken: range.startToken, endToken: range.endToken, isFullMonth: range.isFullMonth, key: rowKey }); } months.forEach((month) => { pushRef(month?.associations?.godId, month); const events = Array.isArray(month?.events) ? month.events : []; events.forEach((event) => { pushRef(event?.associations?.godId, month, { rawDateText: event?.dateRange || event?.date || "" }); }); }); holidays.forEach((holiday) => { const month = monthById.get(holiday?.monthId); if (month) { pushRef(holiday?.associations?.godId, month, { rawDateText: holiday?.dateRange || holiday?.date || "" }); } }); map.forEach((rows, key) => { const preciseMonthIds = new Set( rows .filter((entry) => !entry.isFullMonth) .map((entry) => entry.id) ); const filtered = rows.filter((entry) => { if (!entry.isFullMonth) { return true; } return !preciseMonthIds.has(entry.id); }); filtered.sort((left, right) => { if (left.order !== right.order) { return left.order - right.order; } const startLeft = parseMonthDayToken(left.startToken); const startRight = parseMonthDayToken(right.startToken); const dayLeft = startLeft ? startLeft.day : 999; const dayRight = startRight ? startRight.day : 999; if (dayLeft !== dayRight) { return dayLeft - dayRight; } return String(left.label || left.name || "").localeCompare(String(right.label || right.name || "")); }); map.set(key, filtered); }); return map; } // ── Filter ───────────────────────────────────────────────────────────────── function applyFilter() { const q = state.searchQuery.toLowerCase(); const tab = state.activeTab; state.filteredGods = state.gods.filter((g) => { if (tab !== "all" && g.pantheon !== tab) return false; if (!q) return true; const hay = [ g.name, g.epithet, g.role, ...(g.domains || []), ...(g.parents || []), ...(g.siblings || []), ...(g.children || []), ...(g.symbols || []), ...(g.equivalents || []), g.meaning, g.description, g.myth, ].filter(Boolean).join(" ").toLowerCase(); return hay.includes(q); }); const hasSelected = state.filteredGods.some((g) => g.id === state.selectedId); if (!hasSelected) { state.selectedId = state.filteredGods[0]?.id || null; } renderList(); renderDetail(state.selectedId); } // ── List ─────────────────────────────────────────────────────────────────── function renderList() { if (!listEl) return; if (countEl) countEl.textContent = `${state.filteredGods.length} deities`; if (!state.filteredGods.length) { listEl.innerHTML = `
No matches
`; return; } listEl.innerHTML = state.filteredGods.map((g) => { const isActive = state.selectedId === g.id; const tag = PANTHEON_LABEL[g.pantheon] || g.pantheon; return `
${g.name} ${tag}
${g.role || g.epithet || "—"}
`; }).join(""); listEl.querySelectorAll(".gods-list-item").forEach((el) => { el.addEventListener("click", () => selectGod(el.dataset.id)); el.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") selectGod(el.dataset.id); }); }); } // ── Detail ───────────────────────────────────────────────────────────────── function renderDetail(id) { if (!detailNameEl) return; const g = state.gods.find((x) => x.id === id); if (!g) { detailNameEl.textContent = "—"; detailSubEl.textContent = "Select a deity to explore"; detailBodyEl.innerHTML = ""; return; } detailNameEl.textContent = g.name; detailSubEl.textContent = g.epithet || g.role || ""; const cards = []; // ── Role & Domains ── if (g.role || g.domains?.length) { const domHtml = g.domains?.length ? `
${g.domains.map(d => `${d}`).join("")}
` : ""; cards.push(`
⚡ Role & Domains
${g.role ? `
${g.role}
` : ""} ${domHtml}
`); } // ── Family ── const hasFamily = [g.parents, g.siblings, g.consorts, g.children].some(a => a?.length); if (hasFamily) { const rows = [ g.parents?.length ? `
Parents${g.parents.join(", ")}
` : "", g.siblings?.length ? `
Siblings${g.siblings.join(", ")}
` : "", g.consorts?.length ? `
Consort(s)${g.consorts.join(", ")}
` : "", g.children?.length ? `
Children${g.children.join(", ")}
` : "", ].filter(Boolean).join(""); cards.push(`
👨‍👩‍👧 Family
${rows}
`); } // ── Symbols & Sacred ── if (g.symbols?.length || g.sacredAnimals?.length || g.sacredPlaces?.length) { const rows = [ g.symbols?.length ? `
Symbols${g.symbols.join(", ")}
` : "", g.sacredAnimals?.length ? `
Sacred animals${g.sacredAnimals.join(", ")}
` : "", g.sacredPlaces?.length ? `
Sacred places${g.sacredPlaces.join(", ")}
` : "", ].filter(Boolean).join(""); cards.push(`
🔱 Sacred & Symbols
${rows}
`); } // ── Hebrew Name (divine names / archangels) ── if (g.hebrew) { const title = g.pantheon === "archangel" ? "☀️ Angelic Name" : "✡️ Hebrew Name"; cards.push(`
${title}
${g.hebrew} ${g.name}
${g.meaning ? `
${g.meaning}
` : ""} ${g.sephirah ? `
Governs Sephirah ${g.sephirah}
` : ""}
`); } // ── Equivalents ── if (g.equivalents?.length) { const equivalentHtml = g.equivalents.map((equivalent) => { const target = findEquivalentTarget(equivalent, g.id); if (target) { return ``; } return `${equivalent}`; }).join(""); cards.push(`
⟺ Equivalents
${equivalentHtml}
`); } const monthRefs = state.monthRefsByGodId.get(String(g.id || "").toLowerCase()) || []; if (monthRefs.length) { const monthButtons = monthRefs.map((month) => `` ).join(""); cards.push(`
📅 Calendar Months
Linked month correspondences for ${g.name}
${monthButtons}
`); } // ── Description ── if (g.description) { cards.push(`
📖 Description
${g.description}
`); } // ── Myth ── if (g.myth) { cards.push(`
📜 Myth
${g.myth}
`); } // ── Poem ── if (g.poem) { cards.push(`
✍️ Poem
${g.poem}
`); } // ── Kabbalah reference (small, at bottom) ── if (g.kabbalahPaths?.length) { const btnHtml = g.kabbalahPaths.map(p => `` ).join(""); cards.push(`
🌳 Kabbalah Reference
Paths: ${g.kabbalahPaths.join(", ")}
${btnHtml}
`); } detailBodyEl.innerHTML = `
${cards.join("")}
`; // Attach nav button listeners detailBodyEl.querySelectorAll(".gods-nav-btn[data-event]").forEach((btn) => { btn.addEventListener("click", () => { const evtName = btn.dataset.event; const detail = {}; Object.entries(btn.dataset).forEach(([key, val]) => { if (key === "event") return; const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); detail[camel] = isNaN(Number(val)) || val === "" ? val : Number(val); }); document.dispatchEvent(new CustomEvent(evtName, { detail })); }); }); detailBodyEl.querySelectorAll(".gods-equivalent-link[data-god-id]").forEach((btn) => { btn.addEventListener("click", () => { const godId = btn.dataset.godId; if (godId) selectGod(godId); }); }); } // ── Tabs ─────────────────────────────────────────────────────────────────── function renderTabs() { const tabsEl = document.getElementById("gods-tabs"); if (!tabsEl) return; tabsEl.innerHTML = TABS.map((t) => `` ).join(""); tabsEl.querySelectorAll(".gods-tab-btn").forEach((btn) => { btn.addEventListener("click", () => { state.activeTab = btn.dataset.tab; renderTabs(); applyFilter(); }); }); } // ── Public: select god by id ─────────────────────────────────────────────── function selectGod(id) { const g = state.gods.find((x) => x.id === id); if (!g) return false; if (g && state.activeTab !== "all" && g.pantheon !== state.activeTab) { state.activeTab = "all"; renderTabs(); } state.selectedId = id; applyFilter(); requestAnimationFrame(() => { const item = listEl?.querySelector(`[data-id="${id}"]`); if (item) item.scrollIntoView({ block: "nearest", behavior: "smooth" }); }); return true; } function selectById(godId) { return selectGod(godId); } function selectByName(name) { const tokens = tokenizeEquivalent(name); for (const token of tokens) { const matches = state.godsByName.get(normalizeName(token)); const target = matches?.[0]; if (target?.id) { return selectGod(target.id); } } return false; } // ── Public: navigate here from kabbalah (find first god on that path) ────── function selectByPathNo(pathNo) { const no = Number(pathNo); const g = state.gods.find((x) => x.kabbalahPaths?.includes(no)); if (g) return selectGod(g.id); return false; } // ── Init ─────────────────────────────────────────────────────────────────── function ensureGodsSection(magickDataset, referenceData = null) { if (referenceData) { state.monthRefsByGodId = buildMonthReferencesByGod(referenceData); } if (state.initialized) { if (state.selectedId) { renderDetail(state.selectedId); } return; } state.initialized = true; listEl = document.getElementById("gods-list"); detailNameEl = document.getElementById("gods-detail-name"); detailSubEl = document.getElementById("gods-detail-sub"); detailBodyEl = document.getElementById("gods-detail-body"); countEl = document.getElementById("gods-count"); searchInputEl = document.getElementById("gods-search-input"); searchClearEl = document.getElementById("gods-search-clear"); const godsData = magickDataset?.grouped?.["gods"]; if (!godsData?.gods?.length) { if (listEl) listEl.innerHTML = `
Failed to load gods data.
`; return; } state.gods = godsData.gods; state.godsByName = state.gods.reduce((map, god) => { const key = normalizeName(god.name); const row = map.get(key) || []; row.push(god); map.set(key, row); return map; }, new Map()); if (searchInputEl) { searchInputEl.addEventListener("input", () => { state.searchQuery = searchInputEl.value; if (searchClearEl) searchClearEl.disabled = !state.searchQuery; applyFilter(); }); } if (searchClearEl) { searchClearEl.disabled = true; searchClearEl.addEventListener("click", () => { state.searchQuery = ""; if (searchInputEl) searchInputEl.value = ""; searchClearEl.disabled = true; applyFilter(); }); } renderTabs(); applyFilter(); } // ── Expose public API ────────────────────────────────────────────────────── window.GodsSectionUi = { ensureGodsSection, selectByPathNo, selectById, selectByName }; })();