/* 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 = `