Files
TaroTime/app/ui-holidays-render.js
T
2026-04-24 01:20:21 -07:00

302 lines
11 KiB
JavaScript

/* ui-holidays-render.js - Render/search helpers for the holiday repository */
(function () {
"use strict";
function planetLabel(planetId, context) {
const { state, cap } = context;
if (!planetId) {
return "Planet";
}
const planet = state.planetsById.get(planetId);
if (!planet) {
return cap(planetId);
}
return `${planet.symbol || ""} ${planet.name || cap(planetId)}`.trim();
}
function zodiacLabel(signId, context) {
const { state, cap } = context;
if (!signId) {
return "Zodiac";
}
const sign = state.signsById.get(signId);
if (!sign) {
return cap(signId);
}
return `${sign.symbol || ""} ${sign.name || cap(signId)}`.trim();
}
function godLabel(godId, godName, context) {
const { state, cap } = context;
if (godName) {
return godName;
}
if (!godId) {
return "Deity";
}
const god = state.godsById.get(godId);
return god?.name || cap(godId);
}
function hebrewLabel(hebrewLetterId, context) {
const { state, cap } = context;
if (!hebrewLetterId) {
return "Hebrew Letter";
}
const letter = state.hebrewById.get(hebrewLetterId);
if (!letter) {
return cap(hebrewLetterId);
}
return `${letter.char || ""} ${letter.name || cap(hebrewLetterId)}`.trim();
}
function computeDigitalRoot(value) {
let current = Math.abs(Math.trunc(Number(value)));
if (!Number.isFinite(current)) {
return null;
}
while (current >= 10) {
current = String(current)
.split("")
.reduce((sum, digit) => sum + Number(digit), 0);
}
return current;
}
function buildInlineNavButton(label, nav, attrs = {}) {
const dataAttrs = Object.entries(attrs)
.map(([key, value]) => `data-${key}="${value}"`)
.join(" ");
return `<button class="detail-inline-link" data-nav="${nav}" ${dataAttrs}>${label}</button>`;
}
function buildAssociationButtons(associations, context) {
const { getDisplayTarotName, resolveTarotTrumpNumber } = context;
if (!associations || typeof associations !== "object") {
return '<div class="planet-text">--</div>';
}
const rows = [];
if (associations.planetId) {
rows.push(`<div class="planet-text detail-inline-value">Planet ${buildInlineNavButton(planetLabel(associations.planetId, context), "planet", { "planet-id": associations.planetId })}</div>`);
}
if (associations.zodiacSignId) {
rows.push(`<div class="planet-text detail-inline-value">Zodiac ${buildInlineNavButton(zodiacLabel(associations.zodiacSignId, context), "zodiac", { "sign-id": associations.zodiacSignId })}</div>`);
}
if (Number.isFinite(Number(associations.numberValue))) {
const rawNumber = Math.trunc(Number(associations.numberValue));
if (rawNumber >= 0) {
const numberValue = computeDigitalRoot(rawNumber);
if (numberValue != null) {
const label = rawNumber === numberValue
? `Number ${numberValue}`
: `Number ${numberValue} (from ${rawNumber})`;
rows.push(`<div class="planet-text detail-inline-value">Number ${buildInlineNavButton(label, "number", { "number-value": numberValue })}</div>`);
}
}
}
if (associations.tarotCard) {
const trumpNumber = resolveTarotTrumpNumber(associations.tarotCard);
const explicitTrumpNumber = Number(associations.tarotTrumpNumber);
const tarotTrumpNumber = Number.isFinite(explicitTrumpNumber) ? explicitTrumpNumber : trumpNumber;
const tarotLabel = getDisplayTarotName(associations.tarotCard, tarotTrumpNumber);
rows.push(`<div class="planet-text detail-inline-value">Tarot ${buildInlineNavButton(tarotLabel, "tarot-card", { "card-name": associations.tarotCard, "trump-number": tarotTrumpNumber ?? "" })}</div>`);
}
if (associations.godId || associations.godName) {
const label = godLabel(associations.godId, associations.godName, context);
rows.push(`<div class="planet-text detail-inline-value">Deity ${buildInlineNavButton(label, "god", { "god-id": associations.godId || "", "god-name": associations.godName || label })}</div>`);
}
if (associations.hebrewLetterId) {
rows.push(`<div class="planet-text detail-inline-value">Hebrew ${buildInlineNavButton(hebrewLabel(associations.hebrewLetterId, context), "alphabet", { alphabet: "hebrew", "hebrew-letter-id": associations.hebrewLetterId })}</div>`);
}
if (associations.kabbalahPathNumber != null) {
rows.push(`<div class="planet-text detail-inline-value">Kabbalah ${buildInlineNavButton(`Path ${associations.kabbalahPathNumber}`, "kabbalah", { "path-no": associations.kabbalahPathNumber })}</div>`);
}
if (associations.iChingPlanetaryInfluence) {
rows.push(`<div class="planet-text detail-inline-value">I Ching ${buildInlineNavButton(associations.iChingPlanetaryInfluence, "iching", { "planetary-influence": associations.iChingPlanetaryInfluence })}</div>`);
}
if (!rows.length) {
return '<div class="planet-text">--</div>';
}
return rows.join("");
}
function associationSearchText(associations, context) {
const { getTarotCardSearchAliases } = context;
if (!associations || typeof associations !== "object") {
return "";
}
const tarotAliases = associations.tarotCard && typeof getTarotCardSearchAliases === "function"
? getTarotCardSearchAliases(associations.tarotCard, { trumpNumber: associations.tarotTrumpNumber })
: [];
return [
associations.planetId,
associations.zodiacSignId,
associations.numberValue,
associations.tarotCard,
associations.tarotTrumpNumber,
...tarotAliases,
associations.godId,
associations.godName,
associations.hebrewLetterId,
associations.kabbalahPathNumber,
associations.iChingPlanetaryInfluence
].filter(Boolean).join(" ");
}
function holidaySearchText(holiday, context) {
const { normalizeSearchValue } = context;
return normalizeSearchValue([
holiday?.name,
holiday?.kind,
holiday?.date,
holiday?.dateRange,
holiday?.dateText,
holiday?.monthDayStart,
holiday?.calendarId,
holiday?.description,
associationSearchText(holiday?.associations, context)
].filter(Boolean).join(" "));
}
function renderList(context) {
const {
elements,
state,
filterBySource,
normalizeSourceFilter,
calendarLabel,
monthLabelForCalendar,
selectByHolidayId
} = context;
const { listEl, countEl } = elements;
if (!listEl) {
return;
}
listEl.innerHTML = "";
state.filteredHolidays.forEach((holiday) => {
const isSelected = holiday.id === state.selectedHolidayId;
const itemEl = document.createElement("div");
itemEl.className = `planet-list-item${isSelected ? " is-selected" : ""}`;
itemEl.setAttribute("role", "option");
itemEl.setAttribute("aria-selected", isSelected ? "true" : "false");
itemEl.dataset.holidayId = holiday.id;
const sourceCalendar = calendarLabel(holiday.calendarId);
const sourceMonth = monthLabelForCalendar(holiday.calendarId, holiday.monthId);
const sourceDate = holiday?.dateText || holiday?.date || holiday?.dateRange || "--";
itemEl.innerHTML = `
<div class="planet-list-name">${holiday?.name || holiday?.id || "Holiday"}</div>
<div class="planet-list-meta">${sourceCalendar} - ${sourceMonth} - ${sourceDate}</div>
`;
itemEl.addEventListener("click", () => {
selectByHolidayId(holiday.id, elements);
});
listEl.appendChild(itemEl);
});
if (countEl) {
const sourceFiltered = filterBySource(state.holidays);
const activeFilter = normalizeSourceFilter(state.selectedSource);
const sourceLabel = activeFilter === "all"
? ""
: ` (${calendarLabel(activeFilter)})`;
countEl.textContent = state.searchQuery
? `${state.filteredHolidays.length} of ${sourceFiltered.length} holidays${sourceLabel}`
: `${sourceFiltered.length} holidays${sourceLabel}`;
}
}
function renderHolidayDetail(holiday, context) {
const {
state,
calendarLabel,
monthLabelForCalendar,
resolveHolidayGregorianDate,
formatGregorianReferenceDate,
formatCalendarDateFromGregorian
} = context;
const gregorianDate = resolveHolidayGregorianDate(holiday);
const gregorianRef = formatGregorianReferenceDate(gregorianDate);
const hebrewRef = formatCalendarDateFromGregorian(gregorianDate, "hebrew");
const islamicRef = formatCalendarDateFromGregorian(gregorianDate, "islamic");
const confidence = String(holiday?.conversionConfidence || holiday?.datePrecision || "approximate").toLowerCase();
const confidenceLabel = (!(gregorianDate instanceof Date) || Number.isNaN(gregorianDate.getTime()))
? "unresolved"
: (confidence === "exact" ? "exact" : "approximate");
const monthName = monthLabelForCalendar(holiday?.calendarId, holiday?.monthId);
const holidayDate = holiday?.dateText || holiday?.date || holiday?.dateRange || "--";
const sourceMonthLink = holiday?.monthId
? `<div class="planet-text detail-inline-value">Source month ${buildInlineNavButton(`${calendarLabel(holiday?.calendarId)} ${monthName}`, "calendar-month", { "calendar-id": holiday.calendarId || "", "month-id": holiday.monthId })}</div>`
: "";
return `
<div class="planet-meta-grid">
<div class="planet-meta-card">
<strong>Holiday Facts</strong>
<div class="planet-text">
<dl class="alpha-dl">
<dt>Source Calendar</dt><dd>${calendarLabel(holiday?.calendarId)}</dd>
<dt>Source Month</dt><dd>${monthName}</dd>
<dt>Source Date</dt><dd>${holidayDate}</dd>
<dt>Reference Year</dt><dd>${state.selectedYear}</dd>
<dt>Conversion</dt><dd>${confidenceLabel}</dd>
</dl>
</div>
</div>
<div class="planet-meta-card">
<strong>Cross-Calendar Dates</strong>
<div class="planet-text">
<dl class="alpha-dl">
<dt>Gregorian</dt><dd>${gregorianRef}</dd>
<dt>Hebrew</dt><dd>${hebrewRef}</dd>
<dt>Islamic</dt><dd>${islamicRef}</dd>
</dl>
</div>
</div>
<div class="planet-meta-card">
<strong>Description</strong>
<div class="planet-text">${holiday?.description || "--"}</div>
${sourceMonthLink}
</div>
<div class="planet-meta-card">
<strong>Associations</strong>
${buildAssociationButtons(holiday?.associations, context)}
</div>
</div>
`;
}
window.HolidayRenderUi = {
holidaySearchText,
renderList,
renderHolidayDetail
};
})();