moved to API
This commit is contained in:
132
app/app-config.js
Normal file
132
app/app-config.js
Normal file
@@ -0,0 +1,132 @@
|
||||
(function () {
|
||||
const apiBaseUrlStorageKey = "tarot-time-api-base-url";
|
||||
const apiKeyStorageKey = "tarot-time-api-key";
|
||||
const defaultApiBaseUrl = "";
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function normalizeApiKey(value) {
|
||||
return String(value || "").trim();
|
||||
}
|
||||
|
||||
function normalizeConnectionSettings(settings) {
|
||||
return {
|
||||
apiBaseUrl: normalizeBaseUrl(settings?.apiBaseUrl),
|
||||
apiKey: normalizeApiKey(settings?.apiKey)
|
||||
};
|
||||
}
|
||||
|
||||
function persistConnectionSettings(settings) {
|
||||
let didPersist = true;
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search || "");
|
||||
const queryValue = String(params.get("apiBaseUrl") || "").trim();
|
||||
const queryApiKey = String(params.get("apiKey") || params.get("api_key") || "").trim();
|
||||
|
||||
if (queryValue) {
|
||||
window.localStorage.setItem(apiBaseUrlStorageKey, normalizeBaseUrl(queryValue));
|
||||
}
|
||||
|
||||
if (queryApiKey) {
|
||||
window.localStorage.setItem(apiKeyStorageKey, normalizeApiKey(queryApiKey));
|
||||
}
|
||||
|
||||
if (settings.apiBaseUrl) {
|
||||
window.localStorage.setItem(apiBaseUrlStorageKey, settings.apiBaseUrl);
|
||||
} else {
|
||||
window.localStorage.removeItem(apiBaseUrlStorageKey);
|
||||
}
|
||||
|
||||
if (settings.apiKey) {
|
||||
window.localStorage.setItem(apiKeyStorageKey, settings.apiKey);
|
||||
} else {
|
||||
window.localStorage.removeItem(apiKeyStorageKey);
|
||||
}
|
||||
} catch (_error) {
|
||||
didPersist = false;
|
||||
}
|
||||
|
||||
return didPersist;
|
||||
}
|
||||
|
||||
function readConfiguredConnectionSettings() {
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search || "");
|
||||
const queryValue = String(params.get("apiBaseUrl") || "").trim();
|
||||
const queryApiKey = String(params.get("apiKey") || params.get("api_key") || "").trim();
|
||||
|
||||
if (queryValue) {
|
||||
window.localStorage.setItem(apiBaseUrlStorageKey, normalizeBaseUrl(queryValue));
|
||||
}
|
||||
|
||||
if (queryApiKey) {
|
||||
window.localStorage.setItem(apiKeyStorageKey, normalizeApiKey(queryApiKey));
|
||||
}
|
||||
|
||||
const storedBaseUrl = String(window.localStorage.getItem(apiBaseUrlStorageKey) || "").trim();
|
||||
const storedApiKey = String(window.localStorage.getItem(apiKeyStorageKey) || "").trim();
|
||||
return normalizeConnectionSettings({
|
||||
apiBaseUrl: storedBaseUrl,
|
||||
apiKey: storedApiKey
|
||||
});
|
||||
} catch (_error) {
|
||||
return normalizeConnectionSettings({
|
||||
apiBaseUrl: "",
|
||||
apiKey: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const initialConnectionSettings = readConfiguredConnectionSettings();
|
||||
|
||||
window.TarotAppConfig = {
|
||||
...(window.TarotAppConfig || {}),
|
||||
apiBaseUrl: initialConnectionSettings.apiBaseUrl,
|
||||
apiKey: initialConnectionSettings.apiKey,
|
||||
getApiBaseUrl() {
|
||||
return normalizeBaseUrl(this.apiBaseUrl);
|
||||
},
|
||||
getApiKey() {
|
||||
return normalizeApiKey(this.apiKey);
|
||||
},
|
||||
isConnectionConfigured() {
|
||||
return Boolean(this.getApiBaseUrl());
|
||||
},
|
||||
getConnectionSettings() {
|
||||
return {
|
||||
apiBaseUrl: this.getApiBaseUrl(),
|
||||
apiKey: this.getApiKey()
|
||||
};
|
||||
},
|
||||
updateConnectionSettings(nextSettings = {}) {
|
||||
const previous = this.getConnectionSettings();
|
||||
const current = normalizeConnectionSettings({
|
||||
...previous,
|
||||
...nextSettings
|
||||
});
|
||||
const didPersist = persistConnectionSettings(current);
|
||||
|
||||
this.apiBaseUrl = current.apiBaseUrl;
|
||||
this.apiKey = current.apiKey;
|
||||
|
||||
if (previous.apiBaseUrl !== current.apiBaseUrl || previous.apiKey !== current.apiKey) {
|
||||
document.dispatchEvent(new CustomEvent("connection:updated", {
|
||||
detail: {
|
||||
previous,
|
||||
current: { ...current }
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
didPersist
|
||||
};
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -77,7 +77,7 @@
|
||||
clearInterval(nowInterval);
|
||||
}
|
||||
|
||||
const tick = () => {
|
||||
const tick = async () => {
|
||||
if (!referenceData || !currentGeo || renderInProgress) {
|
||||
return;
|
||||
}
|
||||
@@ -91,12 +91,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
config.services.updateNowPanel?.(referenceData, currentGeo, config.nowElements, currentTimeFormat);
|
||||
config.calendarVisualsUi?.applyDynamicNowIndicatorVisual?.(now);
|
||||
try {
|
||||
await config.services.updateNowPanel?.(referenceData, currentGeo, config.nowElements, currentTimeFormat);
|
||||
config.calendarVisualsUi?.applyDynamicNowIndicatorVisual?.(now);
|
||||
} catch (_error) {
|
||||
}
|
||||
};
|
||||
|
||||
tick();
|
||||
nowInterval = setInterval(tick, 1000);
|
||||
void tick();
|
||||
nowInterval = setInterval(() => {
|
||||
void tick();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function renderWeek() {
|
||||
@@ -138,7 +143,7 @@
|
||||
|
||||
applyCenteredWeekWindow(anchorDate);
|
||||
|
||||
const events = config.services.buildWeekEvents?.(currentGeo, referenceData, anchorDate) || [];
|
||||
const events = await config.services.buildWeekEvents?.(currentGeo, referenceData, anchorDate) || [];
|
||||
config.calendar?.clear?.();
|
||||
config.calendar?.createEvents?.(events);
|
||||
config.calendarVisualsUi?.applySunRulerGradient?.(anchorDate);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
(function () {
|
||||
const dataService = window.TarotDataService || {};
|
||||
const {
|
||||
DAY_IN_MS,
|
||||
toTitleCase,
|
||||
@@ -11,83 +12,24 @@
|
||||
const BACKFILL_DAYS = 4;
|
||||
const FORECAST_DAYS = 7;
|
||||
|
||||
function buildWeekEvents(geo, referenceData, anchorDate) {
|
||||
const baseDate = anchorDate || new Date();
|
||||
const events = [];
|
||||
let runningId = 1;
|
||||
|
||||
for (let offset = -BACKFILL_DAYS; offset <= FORECAST_DAYS; offset += 1) {
|
||||
const date = new Date(baseDate.getTime() + offset * DAY_IN_MS);
|
||||
const hours = calcPlanetaryHoursForDayAndLocation(date, geo);
|
||||
const moonIllum = window.SunCalc.getMoonIllumination(date);
|
||||
const moonPhase = getMoonPhaseName(moonIllum.phase);
|
||||
const sunInfo = getDecanForDate(date, referenceData.signs, referenceData.decansBySign);
|
||||
|
||||
const moonTarot = referenceData.planets.luna?.tarot?.majorArcana || "The High Priestess";
|
||||
events.push({
|
||||
id: `moon-${offset}`,
|
||||
calendarId: "astrology",
|
||||
category: "allday",
|
||||
title: `Moon: ${moonPhase} (${Math.round(moonIllum.fraction * 100)}%) · ${moonTarot}`,
|
||||
start: new Date(date.getFullYear(), date.getMonth(), date.getDate()),
|
||||
end: new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59),
|
||||
isReadOnly: true
|
||||
});
|
||||
|
||||
if (sunInfo?.sign) {
|
||||
const rulerPlanet = referenceData.planets[sunInfo.sign.rulingPlanetId];
|
||||
const bodyParts = [
|
||||
`${sunInfo.sign.symbol} ${toTitleCase(sunInfo.sign.element)} ${toTitleCase(sunInfo.sign.modality)}`,
|
||||
rulerPlanet ? `Sign ruler: ${rulerPlanet.symbol} ${rulerPlanet.name}` : `Sign ruler: ${sunInfo.sign.rulingPlanetId}`
|
||||
];
|
||||
|
||||
if (sunInfo.decan) {
|
||||
const decanRuler = referenceData.planets[sunInfo.decan.rulerPlanetId];
|
||||
const decanText = decanRuler
|
||||
? `Decan ${sunInfo.decan.index}: ${sunInfo.decan.tarotMinorArcana} (${decanRuler.symbol} ${decanRuler.name})`
|
||||
: `Decan ${sunInfo.decan.index}: ${sunInfo.decan.tarotMinorArcana}`;
|
||||
bodyParts.unshift(decanText);
|
||||
}
|
||||
|
||||
events.push({
|
||||
id: `sun-${offset}`,
|
||||
calendarId: "astrology",
|
||||
category: "allday",
|
||||
title: `Sun in ${sunInfo.sign.name} · ${sunInfo.sign.tarot.majorArcana}`,
|
||||
body: bodyParts.join("\n"),
|
||||
start: new Date(date.getFullYear(), date.getMonth(), date.getDate()),
|
||||
end: new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59),
|
||||
isReadOnly: true
|
||||
});
|
||||
}
|
||||
|
||||
for (const hour of hours) {
|
||||
const planet = referenceData.planets[hour.planetId];
|
||||
if (!planet) continue;
|
||||
|
||||
const calendarId = PLANET_CALENDAR_IDS.has(hour.planetId)
|
||||
? `planet-${hour.planetId}`
|
||||
: "planetary";
|
||||
|
||||
events.push({
|
||||
id: `ph-${runningId++}`,
|
||||
calendarId,
|
||||
category: "time",
|
||||
title: `${planet.symbol} ${planet.name} · ${planet.tarot.majorArcana}`,
|
||||
body: `${planet.weekday} current · ${hour.isDaylight ? "Day" : "Night"} hour\n${planet.magickTypes}`,
|
||||
raw: {
|
||||
planetSymbol: planet.symbol,
|
||||
planetName: planet.name,
|
||||
tarotName: planet.tarot.majorArcana
|
||||
},
|
||||
start: hour.start,
|
||||
end: hour.end,
|
||||
isReadOnly: true
|
||||
});
|
||||
}
|
||||
function normalizeApiEvent(event) {
|
||||
if (!event || typeof event !== "object") {
|
||||
return event;
|
||||
}
|
||||
|
||||
return events;
|
||||
return {
|
||||
...event,
|
||||
start: event.start ? new Date(event.start) : event.start,
|
||||
end: event.end ? new Date(event.end) : event.end
|
||||
};
|
||||
}
|
||||
|
||||
async function buildWeekEvents(geo, referenceData, anchorDate) {
|
||||
const payload = await dataService.fetchWeekEvents?.(geo, anchorDate);
|
||||
const events = Array.isArray(payload?.events)
|
||||
? payload.events
|
||||
: (Array.isArray(payload) ? payload : []);
|
||||
return events.map((event) => normalizeApiEvent(event));
|
||||
}
|
||||
|
||||
window.TarotEventBuilder = {
|
||||
|
||||
@@ -107,8 +107,6 @@
|
||||
quality: 82
|
||||
};
|
||||
|
||||
const DECK_REGISTRY_PATH = "asset/tarot deck/decks.json";
|
||||
|
||||
let deckManifestSources = buildDeckManifestSources();
|
||||
|
||||
const manifestCache = new Map();
|
||||
@@ -116,6 +114,21 @@
|
||||
const cardBackThumbnailCache = new Map();
|
||||
let activeDeckId = DEFAULT_DECK_ID;
|
||||
|
||||
function getApiBaseUrl() {
|
||||
return String(window.TarotDataService?.getApiBaseUrl?.() || window.TarotAppConfig?.apiBaseUrl || "")
|
||||
.trim()
|
||||
.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function rewriteBasePathForApi(basePath) {
|
||||
const normalizedBasePath = String(basePath || "").trim();
|
||||
if (!normalizedBasePath) {
|
||||
return normalizedBasePath;
|
||||
}
|
||||
|
||||
return window.TarotDataService?.toApiAssetUrl?.(normalizedBasePath) || normalizedBasePath;
|
||||
}
|
||||
|
||||
function canonicalMajorName(cardName) {
|
||||
return String(cardName || "")
|
||||
.trim()
|
||||
@@ -256,6 +269,46 @@
|
||||
return /^(https?:)?\/\//i.test(String(pathValue || ""));
|
||||
}
|
||||
|
||||
function joinAssetPath(basePath, relativePath) {
|
||||
const normalizedBasePath = String(basePath || "").trim().replace(/\/+$/, "");
|
||||
const normalizedRelativePath = String(relativePath || "")
|
||||
.trim()
|
||||
.replace(/^\.\//, "")
|
||||
.replace(/^\/+/, "");
|
||||
|
||||
if (!normalizedBasePath) {
|
||||
return normalizedRelativePath;
|
||||
}
|
||||
|
||||
if (!normalizedRelativePath) {
|
||||
return normalizedBasePath;
|
||||
}
|
||||
|
||||
if (!isRemoteAssetPath(normalizedBasePath)) {
|
||||
return `${normalizedBasePath}/${normalizedRelativePath}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(normalizedBasePath);
|
||||
const encodedRelativePath = normalizedRelativePath
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.map((segment) => {
|
||||
try {
|
||||
return encodeURIComponent(decodeURIComponent(segment));
|
||||
} catch {
|
||||
return encodeURIComponent(segment);
|
||||
}
|
||||
})
|
||||
.join("/");
|
||||
|
||||
url.pathname = `${url.pathname.replace(/\/+$/, "")}/${encodedRelativePath}`;
|
||||
return url.toString();
|
||||
} catch {
|
||||
return `${normalizedBasePath}/${normalizedRelativePath}`;
|
||||
}
|
||||
}
|
||||
|
||||
function toDeckAssetPath(manifest, relativeOrAbsolutePath) {
|
||||
const normalizedPath = String(relativeOrAbsolutePath || "").trim();
|
||||
if (!normalizedPath) {
|
||||
@@ -266,7 +319,7 @@
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
return `${manifest.basePath}/${normalizedPath.replace(/^\.\//, "")}`;
|
||||
return joinAssetPath(manifest.basePath, normalizedPath);
|
||||
}
|
||||
|
||||
function resolveDeckCardBackPath(manifest) {
|
||||
@@ -364,7 +417,7 @@
|
||||
const id = String(entry?.id || "").trim().toLowerCase();
|
||||
const basePath = String(entry?.basePath || "").trim().replace(/\/$/, "");
|
||||
const manifestPath = String(entry?.manifestPath || "").trim();
|
||||
if (!id || !basePath || !manifestPath) {
|
||||
if (!id || !manifestPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -382,14 +435,35 @@
|
||||
}
|
||||
|
||||
function buildDeckManifestSources() {
|
||||
const registry = readManifestJsonSync(DECK_REGISTRY_PATH);
|
||||
const registryDecks = Array.isArray(registry)
|
||||
? registry
|
||||
: (Array.isArray(registry?.decks) ? registry.decks : null);
|
||||
if (!window.TarotDataService?.isApiEnabled?.() && !getApiBaseUrl()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const registryUrl = window.TarotDataService?.buildApiUrl?.("/api/v1/decks/options") || `${getApiBaseUrl()}/api/v1/decks/options`;
|
||||
if (!registryUrl) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const registry = readManifestJsonSync(registryUrl);
|
||||
const registryDecks = Array.isArray(registry?.decks)
|
||||
? registry.decks.map((entry) => ({
|
||||
id: entry?.id,
|
||||
label: entry?.label,
|
||||
manifestPath: window.TarotDataService?.buildApiUrl?.(`/api/v1/decks/${encodeURIComponent(String(entry?.id || "").trim().toLowerCase())}/manifest`) || `${getApiBaseUrl()}/api/v1/decks/${encodeURIComponent(String(entry?.id || "").trim().toLowerCase())}/manifest`
|
||||
}))
|
||||
: [];
|
||||
|
||||
return toDeckSourceMap(registryDecks);
|
||||
}
|
||||
|
||||
function resetConnectionCaches() {
|
||||
deckManifestSources = buildDeckManifestSources();
|
||||
manifestCache.clear();
|
||||
cardBackCache.clear();
|
||||
cardBackThumbnailCache.clear();
|
||||
setActiveDeck(activeDeckId);
|
||||
}
|
||||
|
||||
function getDeckManifestSources(forceRefresh = false) {
|
||||
if (forceRefresh || !deckManifestSources || Object.keys(deckManifestSources).length === 0) {
|
||||
deckManifestSources = buildDeckManifestSources();
|
||||
@@ -442,9 +516,9 @@
|
||||
return {
|
||||
id: source.id,
|
||||
label: String(rawManifest.label || source.label || source.id),
|
||||
basePath: String(source.basePath || "").replace(/\/$/, ""),
|
||||
basePath: rewriteBasePathForApi(String(rawManifest.basePath || source.basePath || "").replace(/\/$/, "")),
|
||||
cardBack: String(rawManifest.cardBack || "").trim(),
|
||||
cardBackPath: String(source.cardBackPath || "").trim(),
|
||||
cardBackPath: String(rawManifest.cardBackPath || source.cardBackPath || "").trim(),
|
||||
thumbnails: normalizeThumbnailConfig(rawManifest.thumbnails, source.thumbnailRoot),
|
||||
majors: rawManifest.majors || {},
|
||||
minors: rawManifest.minors || {},
|
||||
@@ -689,17 +763,17 @@
|
||||
}
|
||||
|
||||
if (variant === "thumbnail") {
|
||||
return resolveDeckThumbnailPath(manifest, relativePath) || `${manifest.basePath}/${relativePath}`;
|
||||
return resolveDeckThumbnailPath(manifest, relativePath) || toDeckAssetPath(manifest, relativePath);
|
||||
}
|
||||
|
||||
return `${manifest.basePath}/${relativePath}`;
|
||||
return toDeckAssetPath(manifest, relativePath);
|
||||
}
|
||||
|
||||
function resolveTarotCardImage(cardName, optionsOrDeckId) {
|
||||
const { resolvedDeckId } = resolveDeckOptions(optionsOrDeckId);
|
||||
const activePath = resolveWithDeck(resolvedDeckId, cardName);
|
||||
if (activePath) {
|
||||
return encodeURI(activePath);
|
||||
return activePath;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -709,7 +783,7 @@
|
||||
const { resolvedDeckId } = resolveDeckOptions(optionsOrDeckId);
|
||||
const thumbnailPath = resolveWithDeck(resolvedDeckId, cardName, "thumbnail");
|
||||
if (thumbnailPath) {
|
||||
return encodeURI(thumbnailPath);
|
||||
return thumbnailPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -720,7 +794,7 @@
|
||||
|
||||
if (cardBackCache.has(resolvedDeckId)) {
|
||||
const cachedPath = cardBackCache.get(resolvedDeckId);
|
||||
return cachedPath ? encodeURI(cachedPath) : null;
|
||||
return cachedPath || null;
|
||||
}
|
||||
|
||||
const manifest = getDeckManifest(resolvedDeckId);
|
||||
@@ -728,7 +802,7 @@
|
||||
cardBackCache.set(resolvedDeckId, activeBackPath || null);
|
||||
|
||||
if (activeBackPath) {
|
||||
return encodeURI(activeBackPath);
|
||||
return activeBackPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -739,7 +813,7 @@
|
||||
|
||||
if (cardBackThumbnailCache.has(resolvedDeckId)) {
|
||||
const cachedPath = cardBackThumbnailCache.get(resolvedDeckId);
|
||||
return cachedPath ? encodeURI(cachedPath) : null;
|
||||
return cachedPath || null;
|
||||
}
|
||||
|
||||
const manifest = getDeckManifest(resolvedDeckId);
|
||||
@@ -747,7 +821,7 @@
|
||||
const thumbnailPath = resolveDeckThumbnailPath(manifest, relativeBackPath) || resolveDeckCardBackPath(manifest);
|
||||
cardBackThumbnailCache.set(resolvedDeckId, thumbnailPath || null);
|
||||
|
||||
return thumbnailPath ? encodeURI(thumbnailPath) : null;
|
||||
return thumbnailPath || null;
|
||||
}
|
||||
|
||||
function resolveDisplayNameWithDeck(deckId, cardName, trumpNumber) {
|
||||
@@ -852,6 +926,8 @@
|
||||
setActiveDeck(nextDeck);
|
||||
});
|
||||
|
||||
document.addEventListener("connection:updated", resetConnectionCaches);
|
||||
|
||||
window.TarotCardImages = {
|
||||
resolveTarotCardImage,
|
||||
resolveTarotCardThumbnail,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
(function () {
|
||||
let magickManifestCache = null;
|
||||
let magickDataCache = null;
|
||||
let deckOptionsCache = null;
|
||||
const deckManifestCache = new Map();
|
||||
let quizCategoriesCache = null;
|
||||
const quizTemplatesCache = new Map();
|
||||
|
||||
const DATA_ROOT = "data";
|
||||
const MAGICK_ROOT = DATA_ROOT;
|
||||
@@ -92,8 +96,23 @@
|
||||
pluto: "Pluto"
|
||||
};
|
||||
|
||||
function buildRequestHeaders() {
|
||||
const apiKey = getApiKey();
|
||||
return apiKey
|
||||
? {
|
||||
"x-api-key": apiKey
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async function fetchJson(path) {
|
||||
const response = await fetch(path);
|
||||
if (!path) {
|
||||
throw new Error("API connection is not configured.");
|
||||
}
|
||||
|
||||
const response = await fetch(path, {
|
||||
headers: buildRequestHeaders()
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load ${path} (${response.status})`);
|
||||
}
|
||||
@@ -112,6 +131,98 @@
|
||||
cursor[pathParts[pathParts.length - 1]] = value;
|
||||
}
|
||||
|
||||
function normalizeApiBaseUrl(value) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function getApiBaseUrl() {
|
||||
return normalizeApiBaseUrl(
|
||||
window.TarotAppConfig?.getApiBaseUrl?.() || window.TarotAppConfig?.apiBaseUrl || ""
|
||||
);
|
||||
}
|
||||
|
||||
function getApiKey() {
|
||||
return String(window.TarotAppConfig?.getApiKey?.() || window.TarotAppConfig?.apiKey || "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isApiEnabled() {
|
||||
return Boolean(getApiBaseUrl());
|
||||
}
|
||||
|
||||
function encodePathSegments(pathValue) {
|
||||
return String(pathValue || "")
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.map((segment) => {
|
||||
try {
|
||||
return encodeURIComponent(decodeURIComponent(segment));
|
||||
} catch {
|
||||
return encodeURIComponent(segment);
|
||||
}
|
||||
})
|
||||
.join("/");
|
||||
}
|
||||
|
||||
function buildApiUrl(path, query = {}) {
|
||||
const apiBaseUrl = getApiBaseUrl();
|
||||
if (!apiBaseUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const url = new URL(path, `${apiBaseUrl}/`);
|
||||
Object.entries(query || {}).forEach(([key, value]) => {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedValue = String(value).trim();
|
||||
if (!normalizedValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
url.searchParams.set(key, normalizedValue);
|
||||
});
|
||||
|
||||
const apiKey = getApiKey();
|
||||
if (apiKey && !url.searchParams.has("api_key")) {
|
||||
url.searchParams.set("api_key", apiKey);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function toApiAssetUrl(assetPath) {
|
||||
const apiBaseUrl = getApiBaseUrl();
|
||||
const normalizedAssetPath = String(assetPath || "")
|
||||
.trim()
|
||||
.replace(/^\/+/, "")
|
||||
.replace(/^asset\//i, "");
|
||||
|
||||
if (!apiBaseUrl || !normalizedAssetPath) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const url = new URL(`/api/v1/assets/${encodePathSegments(normalizedAssetPath)}`, `${apiBaseUrl}/`);
|
||||
const apiKey = getApiKey();
|
||||
if (apiKey) {
|
||||
url.searchParams.set("api_key", apiKey);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function resetCaches() {
|
||||
magickManifestCache = null;
|
||||
magickDataCache = null;
|
||||
deckOptionsCache = null;
|
||||
quizCategoriesCache = null;
|
||||
deckManifestCache.clear();
|
||||
quizTemplatesCache.clear();
|
||||
}
|
||||
|
||||
function normalizeTarotName(value) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
@@ -207,7 +318,7 @@
|
||||
return magickManifestCache;
|
||||
}
|
||||
|
||||
magickManifestCache = await fetchJson(`${MAGICK_ROOT}/MANIFEST.json`);
|
||||
magickManifestCache = await fetchJson(buildApiUrl("/api/v1/bootstrap/magick-manifest"));
|
||||
return magickManifestCache;
|
||||
}
|
||||
|
||||
@@ -216,163 +327,185 @@
|
||||
return magickDataCache;
|
||||
}
|
||||
|
||||
const manifest = await loadMagickManifest();
|
||||
const files = Array.isArray(manifest?.files) ? manifest.files : [];
|
||||
const jsonFiles = files.filter((file) => file.endsWith(".json"));
|
||||
|
||||
const entries = await Promise.all(
|
||||
jsonFiles.map(async (relativePath) => {
|
||||
const data = await fetchJson(`${MAGICK_ROOT}/${relativePath}`);
|
||||
return [relativePath, data];
|
||||
})
|
||||
);
|
||||
|
||||
const grouped = {};
|
||||
entries.forEach(([relativePath, data]) => {
|
||||
const noExtensionPath = relativePath.replace(/\.json$/i, "");
|
||||
const pathParts = noExtensionPath.split("/").filter(Boolean);
|
||||
if (!pathParts.length) {
|
||||
return;
|
||||
}
|
||||
buildObjectPath(grouped, pathParts, data);
|
||||
});
|
||||
|
||||
magickDataCache = {
|
||||
manifest,
|
||||
grouped,
|
||||
files: Object.fromEntries(entries)
|
||||
};
|
||||
|
||||
magickDataCache = await fetchJson(buildApiUrl("/api/v1/bootstrap/magick-dataset"));
|
||||
return magickDataCache;
|
||||
}
|
||||
|
||||
async function loadReferenceData() {
|
||||
const { groupDecansBySign } = window.TarotCalc;
|
||||
const [
|
||||
planetsJson,
|
||||
signsJson,
|
||||
decansJson,
|
||||
sabianJson,
|
||||
planetScienceJson,
|
||||
gematriaCiphersJson,
|
||||
iChingJson,
|
||||
calendarMonthsJson,
|
||||
celestialHolidaysJson,
|
||||
calendarHolidaysJson,
|
||||
astronomyCyclesJson,
|
||||
tarotDatabaseJson,
|
||||
hebrewCalendarJson,
|
||||
islamicCalendarJson,
|
||||
wheelOfYearJson
|
||||
] = await Promise.all([
|
||||
fetchJson(`${DATA_ROOT}/planetary-correspondences.json`),
|
||||
fetchJson(`${DATA_ROOT}/signs.json`),
|
||||
fetchJson(`${DATA_ROOT}/decans.json`),
|
||||
fetchJson(`${DATA_ROOT}/sabian-symbols.json`),
|
||||
fetchJson(`${DATA_ROOT}/planet-science.json`),
|
||||
fetchJson(`${DATA_ROOT}/gematria-ciphers.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/i-ching.json`),
|
||||
fetchJson(`${DATA_ROOT}/calendar-months.json`),
|
||||
fetchJson(`${DATA_ROOT}/celestial-holidays.json`),
|
||||
fetchJson(`${DATA_ROOT}/calendar-holidays.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/astronomy-cycles.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/tarot-database.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/hebrew-calendar.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/islamic-calendar.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/wheel-of-year.json`).catch(() => ({}))
|
||||
]);
|
||||
return fetchJson(buildApiUrl("/api/v1/bootstrap/reference-data"));
|
||||
}
|
||||
|
||||
const planets = planetsJson.planets || {};
|
||||
const signs = signsJson.signs || [];
|
||||
const decans = decansJson.decans || [];
|
||||
const sabianSymbols = Array.isArray(sabianJson?.symbols) ? sabianJson.symbols : [];
|
||||
const planetScience = Array.isArray(planetScienceJson?.planets)
|
||||
? planetScienceJson.planets
|
||||
: [];
|
||||
const gematriaCiphers = gematriaCiphersJson && typeof gematriaCiphersJson === "object"
|
||||
? gematriaCiphersJson
|
||||
: {};
|
||||
const iChing = {
|
||||
trigrams: Array.isArray(iChingJson?.trigrams) ? iChingJson.trigrams : [],
|
||||
hexagrams: Array.isArray(iChingJson?.hexagrams) ? iChingJson.hexagrams : [],
|
||||
correspondences: {
|
||||
meta: iChingJson?.correspondences?.meta && typeof iChingJson.correspondences.meta === "object"
|
||||
? iChingJson.correspondences.meta
|
||||
: {},
|
||||
tarotToTrigram: Array.isArray(iChingJson?.correspondences?.tarotToTrigram)
|
||||
? iChingJson.correspondences.tarotToTrigram
|
||||
: []
|
||||
}
|
||||
};
|
||||
async function fetchWeekEvents(geo, anchorDate = new Date()) {
|
||||
return fetchJson(buildApiUrl("/api/v1/calendar/week-events", {
|
||||
latitude: geo?.latitude,
|
||||
longitude: geo?.longitude,
|
||||
date: anchorDate instanceof Date ? anchorDate.toISOString() : anchorDate
|
||||
}));
|
||||
}
|
||||
|
||||
const calendarMonths = Array.isArray(calendarMonthsJson?.months)
|
||||
? calendarMonthsJson.months.map((month) => enrichCalendarMonth(month))
|
||||
: [];
|
||||
async function fetchNowSnapshot(geo, timestamp = new Date()) {
|
||||
return fetchJson(buildApiUrl("/api/v1/now", {
|
||||
latitude: geo?.latitude,
|
||||
longitude: geo?.longitude,
|
||||
date: timestamp instanceof Date ? timestamp.toISOString() : timestamp
|
||||
}));
|
||||
}
|
||||
|
||||
const celestialHolidays = Array.isArray(celestialHolidaysJson?.holidays)
|
||||
? celestialHolidaysJson.holidays.map((holiday) => enrichCelestialHoliday(holiday))
|
||||
: [];
|
||||
async function loadTarotCards(filters = {}) {
|
||||
return fetchJson(buildApiUrl("/api/v1/tarot/cards", {
|
||||
q: filters?.query,
|
||||
arcana: filters?.arcana,
|
||||
suit: filters?.suit
|
||||
}));
|
||||
}
|
||||
|
||||
const calendarHolidays = Array.isArray(calendarHolidaysJson?.holidays)
|
||||
? calendarHolidaysJson.holidays.map((holiday) => enrichCalendarHoliday(holiday))
|
||||
: [];
|
||||
async function pullTarotSpread(spreadId, options = {}) {
|
||||
const normalizedSpreadId = String(spreadId || "").trim() || "three-card";
|
||||
return fetchJson(buildApiUrl(`/api/v1/tarot/spreads/${encodeURIComponent(normalizedSpreadId)}/pull`, {
|
||||
seed: options?.seed
|
||||
}));
|
||||
}
|
||||
|
||||
const astronomyCycles = astronomyCyclesJson && typeof astronomyCyclesJson === "object"
|
||||
? astronomyCyclesJson
|
||||
: {};
|
||||
|
||||
const tarotDatabase = tarotDatabaseJson && typeof tarotDatabaseJson === "object"
|
||||
? tarotDatabaseJson
|
||||
: {};
|
||||
|
||||
const sourceMeanings = tarotDatabase.meanings && typeof tarotDatabase.meanings === "object"
|
||||
? tarotDatabase.meanings
|
||||
: {};
|
||||
|
||||
if (!sourceMeanings.majorByTrumpNumber || typeof sourceMeanings.majorByTrumpNumber !== "object") {
|
||||
sourceMeanings.majorByTrumpNumber = {};
|
||||
async function loadDeckOptions(forceRefresh = false) {
|
||||
if (!forceRefresh && deckOptionsCache) {
|
||||
return deckOptionsCache;
|
||||
}
|
||||
|
||||
const existingByCardName = sourceMeanings.byCardName && typeof sourceMeanings.byCardName === "object"
|
||||
? sourceMeanings.byCardName
|
||||
: {};
|
||||
deckOptionsCache = await fetchJson(buildApiUrl("/api/v1/decks/options"));
|
||||
return deckOptionsCache;
|
||||
}
|
||||
|
||||
sourceMeanings.byCardName = existingByCardName;
|
||||
async function loadDeckManifest(deckId, forceRefresh = false) {
|
||||
const normalizedDeckId = String(deckId || "").trim().toLowerCase();
|
||||
if (!normalizedDeckId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
tarotDatabase.meanings = sourceMeanings;
|
||||
if (!forceRefresh && deckManifestCache.has(normalizedDeckId)) {
|
||||
return deckManifestCache.get(normalizedDeckId);
|
||||
}
|
||||
|
||||
const hebrewCalendar = hebrewCalendarJson && typeof hebrewCalendarJson === "object"
|
||||
? hebrewCalendarJson
|
||||
: {};
|
||||
const islamicCalendar = islamicCalendarJson && typeof islamicCalendarJson === "object"
|
||||
? islamicCalendarJson
|
||||
: {};
|
||||
const wheelOfYear = wheelOfYearJson && typeof wheelOfYearJson === "object"
|
||||
? wheelOfYearJson
|
||||
: {};
|
||||
const manifest = await fetchJson(buildApiUrl(`/api/v1/decks/${encodeURIComponent(normalizedDeckId)}/manifest`));
|
||||
deckManifestCache.set(normalizedDeckId, manifest);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
return {
|
||||
planets,
|
||||
signs,
|
||||
decansBySign: groupDecansBySign(decans),
|
||||
sabianSymbols,
|
||||
planetScience,
|
||||
gematriaCiphers,
|
||||
iChing,
|
||||
calendarMonths,
|
||||
celestialHolidays,
|
||||
calendarHolidays,
|
||||
astronomyCycles,
|
||||
tarotDatabase,
|
||||
hebrewCalendar,
|
||||
islamicCalendar,
|
||||
wheelOfYear
|
||||
async function loadQuizCategories(forceRefresh = false) {
|
||||
if (!forceRefresh && quizCategoriesCache) {
|
||||
return quizCategoriesCache;
|
||||
}
|
||||
|
||||
quizCategoriesCache = await fetchJson(buildApiUrl("/api/v1/quiz/categories"));
|
||||
return quizCategoriesCache;
|
||||
}
|
||||
|
||||
async function loadQuizTemplates(query = {}, forceRefresh = false) {
|
||||
const categoryId = String(query?.categoryId || "").trim();
|
||||
const cacheKey = categoryId || "__all__";
|
||||
|
||||
if (!forceRefresh && quizTemplatesCache.has(cacheKey)) {
|
||||
return quizTemplatesCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const templates = await fetchJson(buildApiUrl("/api/v1/quiz/templates", {
|
||||
categoryId
|
||||
}));
|
||||
quizTemplatesCache.set(cacheKey, templates);
|
||||
return templates;
|
||||
}
|
||||
|
||||
async function pullQuizQuestion(query = {}) {
|
||||
return fetchJson(buildApiUrl("/api/v1/quiz/questions/pull", {
|
||||
categoryId: query?.categoryId,
|
||||
templateKey: query?.templateKey,
|
||||
difficulty: query?.difficulty,
|
||||
seed: query?.seed,
|
||||
includeAnswer: query?.includeAnswer
|
||||
}));
|
||||
}
|
||||
|
||||
async function probeConnection() {
|
||||
const apiBaseUrl = getApiBaseUrl();
|
||||
if (!apiBaseUrl) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "missing-base-url",
|
||||
message: "Enter an API Base URL to load TaroTime."
|
||||
};
|
||||
}
|
||||
|
||||
const requestOptions = {
|
||||
headers: buildRequestHeaders()
|
||||
};
|
||||
|
||||
try {
|
||||
const healthResponse = await fetch(buildApiUrl("/api/v1/health"), requestOptions);
|
||||
if (!healthResponse.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "health-check-failed",
|
||||
message: `The API responded with ${healthResponse.status} during the health check.`
|
||||
};
|
||||
}
|
||||
|
||||
const health = await healthResponse.json().catch(() => null);
|
||||
const protectedResponse = await fetch(buildApiUrl("/api/v1/decks/options"), requestOptions);
|
||||
|
||||
if (protectedResponse.status === 401 || protectedResponse.status === 403) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "auth-required",
|
||||
message: health?.apiKeyRequired
|
||||
? "The API requires a valid API key."
|
||||
: "The API rejected this connection."
|
||||
};
|
||||
}
|
||||
|
||||
if (!protectedResponse.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "protected-route-failed",
|
||||
message: `The API responded with ${protectedResponse.status} when loading protected data.`
|
||||
};
|
||||
}
|
||||
|
||||
const decksPayload = await protectedResponse.json().catch(() => null);
|
||||
return {
|
||||
ok: true,
|
||||
reason: "connected",
|
||||
message: "Connected.",
|
||||
health,
|
||||
deckCount: Array.isArray(decksPayload?.decks) ? decksPayload.decks.length : null
|
||||
};
|
||||
} catch (_error) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "network-error",
|
||||
message: "Unable to reach the API. Check the URL and make sure the server is running."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.TarotDataService = {
|
||||
buildApiUrl,
|
||||
fetchNowSnapshot,
|
||||
fetchWeekEvents,
|
||||
getApiBaseUrl,
|
||||
getApiKey,
|
||||
isApiEnabled,
|
||||
loadDeckManifest,
|
||||
loadDeckOptions,
|
||||
loadQuizCategories,
|
||||
loadQuizTemplates,
|
||||
loadTarotCards,
|
||||
loadReferenceData,
|
||||
loadMagickManifest,
|
||||
loadMagickDataset
|
||||
loadMagickDataset,
|
||||
probeConnection,
|
||||
pullQuizQuestion,
|
||||
pullTarotSpread,
|
||||
toApiAssetUrl
|
||||
};
|
||||
|
||||
document.addEventListener("connection:updated", resetCaches);
|
||||
})();
|
||||
|
||||
114
app/styles.css
114
app/styles.css
@@ -73,6 +73,100 @@
|
||||
.settings-trigger[aria-pressed="true"] {
|
||||
background: #3f3f46;
|
||||
}
|
||||
body.connection-gated {
|
||||
overflow: hidden;
|
||||
}
|
||||
.connection-gate {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 120;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(245, 158, 11, 0.18), transparent 34%),
|
||||
linear-gradient(180deg, rgba(9, 9, 11, 0.84), rgba(9, 9, 11, 0.96));
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.connection-gate[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.connection-gate-card {
|
||||
width: min(460px, calc(100vw - 32px));
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(245, 158, 11, 0.28);
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, rgba(24, 24, 27, 0.96), rgba(9, 9, 11, 0.98));
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.connection-gate-eyebrow {
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: #fbbf24;
|
||||
}
|
||||
.connection-gate-title {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
line-height: 1.1;
|
||||
color: #fafaf9;
|
||||
}
|
||||
.connection-gate-copy {
|
||||
margin: 12px 0 18px;
|
||||
color: #d4d4d8;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.connection-gate-fields {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.connection-gate-status {
|
||||
min-height: 20px;
|
||||
margin-top: 14px;
|
||||
font-size: 13px;
|
||||
color: #d4d4d8;
|
||||
}
|
||||
.connection-gate-status[data-tone="error"] {
|
||||
color: #fca5a5;
|
||||
}
|
||||
.connection-gate-status[data-tone="success"] {
|
||||
color: #86efac;
|
||||
}
|
||||
.connection-gate-status[data-tone="pending"] {
|
||||
color: #fcd34d;
|
||||
}
|
||||
.connection-gate-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.connection-gate-actions button {
|
||||
padding: 9px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #3f3f46;
|
||||
background: #27272a;
|
||||
color: #f4f4f5;
|
||||
cursor: pointer;
|
||||
}
|
||||
.connection-gate-actions button:hover {
|
||||
background: #3f3f46;
|
||||
}
|
||||
#connection-gate-connect {
|
||||
border-color: #d97706;
|
||||
background: linear-gradient(180deg, #f59e0b, #d97706);
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
}
|
||||
#connection-gate-connect:hover {
|
||||
background: linear-gradient(180deg, #fbbf24, #ea580c);
|
||||
}
|
||||
#tarot-section {
|
||||
height: calc(100vh - 61px);
|
||||
background: #18181b;
|
||||
@@ -3509,6 +3603,9 @@
|
||||
color: #f4f4f5;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.settings-field input[type="password"] {
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.settings-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
@@ -3528,6 +3625,23 @@
|
||||
.settings-popup-header button:hover {
|
||||
background: #3f3f46;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.connection-gate {
|
||||
padding: 16px;
|
||||
}
|
||||
.connection-gate-card {
|
||||
padding: 18px;
|
||||
}
|
||||
.connection-gate-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
.connection-gate-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
.connection-gate-actions button {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
#month-strip {
|
||||
height: 28px;
|
||||
background: #18181b;
|
||||
|
||||
@@ -258,7 +258,7 @@
|
||||
|
||||
function enochianGlyphUrl(letter) {
|
||||
const code = enochianGlyphCode(letter);
|
||||
return code ? `asset/img/enochian/char(${code}).png` : "";
|
||||
return code ? (window.TarotDataService?.toApiAssetUrl?.(`img/enochian/char(${code}).png`) || "") : "";
|
||||
}
|
||||
|
||||
function enochianGlyphImageHtml(letter, className) {
|
||||
|
||||
@@ -564,7 +564,7 @@
|
||||
<dt>Element / Planet</dt><dd>${letter.elementOrPlanet || "—"}</dd>
|
||||
<dt>Tarot</dt><dd>${letter.tarot || "—"}</dd>
|
||||
<dt>Numerology</dt><dd>${letter.numerology || "—"}</dd>
|
||||
<dt>Glyph Source</dt><dd>Local cache: asset/img/enochian (sourced from dCode set)</dd>
|
||||
<dt>Glyph Source</dt><dd>API asset: img/enochian (sourced from dCode set)</dd>
|
||||
<dt>Position</dt><dd>#${letter.index} of 21</dd>
|
||||
</dl>
|
||||
`));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
let config = {
|
||||
getAlphabets: () => null,
|
||||
getGematriaDb: () => null,
|
||||
getGematriaElements: () => ({
|
||||
cipherEl: null,
|
||||
inputEl: null,
|
||||
@@ -24,6 +25,10 @@
|
||||
return config.getAlphabets?.() || null;
|
||||
}
|
||||
|
||||
function getConfiguredGematriaDb() {
|
||||
return config.getGematriaDb?.() || null;
|
||||
}
|
||||
|
||||
function getElements() {
|
||||
return config.getGematriaElements?.() || {
|
||||
cipherEl: null,
|
||||
@@ -169,14 +174,20 @@
|
||||
return state.loadingPromise;
|
||||
}
|
||||
|
||||
state.loadingPromise = fetch("data/gematria-ciphers.json")
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load gematria ciphers (${response.status})`);
|
||||
state.loadingPromise = Promise.resolve()
|
||||
.then(async () => {
|
||||
const configuredDb = getConfiguredGematriaDb();
|
||||
if (configuredDb) {
|
||||
return configuredDb;
|
||||
}
|
||||
return response.json();
|
||||
|
||||
const referenceData = await window.TarotDataService?.loadReferenceData?.();
|
||||
return referenceData?.gematriaCiphers || null;
|
||||
})
|
||||
.then((db) => {
|
||||
if (!db) {
|
||||
throw new Error("Gematria cipher data unavailable from API.");
|
||||
}
|
||||
state.db = sanitizeGematriaDb(db);
|
||||
return state.db;
|
||||
})
|
||||
@@ -342,6 +353,11 @@
|
||||
...config,
|
||||
...nextConfig
|
||||
};
|
||||
|
||||
const configuredDb = getConfiguredGematriaDb();
|
||||
if (configuredDb) {
|
||||
state.db = sanitizeGematriaDb(configuredDb);
|
||||
}
|
||||
}
|
||||
|
||||
window.AlphabetGematriaUi = {
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
signPlacementById: new Map(),
|
||||
planetPlacementById: new Map(),
|
||||
pathPlacementByNo: new Map()
|
||||
}
|
||||
},
|
||||
gematriaDb: null
|
||||
};
|
||||
|
||||
function arabicDisplayName(letter) {
|
||||
@@ -86,6 +87,7 @@
|
||||
function ensureGematriaCalculator() {
|
||||
alphabetGematriaUi.init?.({
|
||||
getAlphabets: () => state.alphabets,
|
||||
getGematriaDb: () => state.gematriaDb,
|
||||
getGematriaElements
|
||||
});
|
||||
alphabetGematriaUi.ensureCalculator?.();
|
||||
@@ -403,6 +405,8 @@
|
||||
state.monthRefsByHebrewId = buildMonthReferencesByHebrew(referenceData, state.alphabets);
|
||||
}
|
||||
|
||||
state.gematriaDb = referenceData?.gematriaCiphers || null;
|
||||
|
||||
if (state.initialized) {
|
||||
ensureGematriaCalculator();
|
||||
syncFilterControls();
|
||||
|
||||
171
app/ui-now.js
171
app/ui-now.js
@@ -1,6 +1,8 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const dataService = window.TarotDataService || {};
|
||||
|
||||
const {
|
||||
DAY_IN_MS,
|
||||
getDateKey,
|
||||
@@ -25,62 +27,65 @@
|
||||
let moonCountdownCache = null;
|
||||
let decanCountdownCache = null;
|
||||
|
||||
function updateNowPanel(referenceData, geo, elements, timeFormat = "minutes") {
|
||||
if (!referenceData || !geo || !elements) {
|
||||
return { dayKey: getDateKey(new Date()), skyRefreshKey: "" };
|
||||
function renderNowStatsFromSnapshot(elements, stats) {
|
||||
if (elements.nowStatsPlanetsEl) {
|
||||
elements.nowStatsPlanetsEl.replaceChildren();
|
||||
|
||||
const planetPositions = Array.isArray(stats?.planetPositions) ? stats.planetPositions : [];
|
||||
if (!planetPositions.length) {
|
||||
elements.nowStatsPlanetsEl.textContent = "--";
|
||||
} else {
|
||||
planetPositions.forEach((position) => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "now-stats-planet";
|
||||
item.textContent = String(position?.label || "").trim() || "--";
|
||||
elements.nowStatsPlanetsEl.appendChild(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const dayKey = getDateKey(now);
|
||||
if (elements.nowStatsSabianEl) {
|
||||
const sunSabianSymbol = stats?.sunSabianSymbol || null;
|
||||
const moonSabianSymbol = stats?.moonSabianSymbol || null;
|
||||
const sunLine = sunSabianSymbol?.phrase
|
||||
? `Sun Sabian ${sunSabianSymbol.absoluteDegree}: ${sunSabianSymbol.phrase}`
|
||||
: "Sun Sabian: --";
|
||||
const moonLine = moonSabianSymbol?.phrase
|
||||
? `Moon Sabian ${moonSabianSymbol.absoluteDegree}: ${moonSabianSymbol.phrase}`
|
||||
: "Moon Sabian: --";
|
||||
elements.nowStatsSabianEl.textContent = `${sunLine}\n${moonLine}`;
|
||||
}
|
||||
}
|
||||
|
||||
const todayHours = calcPlanetaryHoursForDayAndLocation(now, geo);
|
||||
const yesterday = new Date(now.getTime() - DAY_IN_MS);
|
||||
const yesterdayHours = calcPlanetaryHoursForDayAndLocation(yesterday, geo);
|
||||
const tomorrow = new Date(now.getTime() + DAY_IN_MS);
|
||||
const tomorrowHours = calcPlanetaryHoursForDayAndLocation(tomorrow, geo);
|
||||
const allHours = [...yesterdayHours, ...todayHours, ...tomorrowHours].sort(
|
||||
(a, b) => a.start.getTime() - b.start.getTime()
|
||||
);
|
||||
const currentHour = allHours.find((entry) => now >= entry.start && now < entry.end);
|
||||
const currentHourSkyKey = currentHour
|
||||
? `${currentHour.planetId}-${currentHour.start.toISOString()}`
|
||||
: "no-hour";
|
||||
function applyNowSnapshot(elements, snapshot, timeFormat) {
|
||||
const timestamp = snapshot?.timestamp ? new Date(snapshot.timestamp) : new Date();
|
||||
const dayKey = String(snapshot?.dayKey || getDateKey(timestamp));
|
||||
const currentHour = snapshot?.currentHour || null;
|
||||
|
||||
if (currentHour) {
|
||||
const planet = referenceData.planets[currentHour.planetId];
|
||||
elements.nowHourEl.textContent = planet
|
||||
? `${planet.symbol} ${planet.name}`
|
||||
: currentHour.planetId;
|
||||
if (currentHour?.planet) {
|
||||
elements.nowHourEl.textContent = `${currentHour.planet.symbol} ${currentHour.planet.name}`;
|
||||
if (elements.nowHourTarotEl) {
|
||||
const hourCardName = planet?.tarot?.majorArcana || "";
|
||||
const hourTrumpNumber = planet?.tarot?.number;
|
||||
const hourCardName = currentHour.planet?.tarot?.majorArcana || "";
|
||||
const hourTrumpNumber = currentHour.planet?.tarot?.number;
|
||||
elements.nowHourTarotEl.textContent = hourCardName
|
||||
? nowUiHelpers.getDisplayTarotName(hourCardName, hourTrumpNumber)
|
||||
: "--";
|
||||
}
|
||||
|
||||
const msLeft = Math.max(0, currentHour.end.getTime() - now.getTime());
|
||||
elements.nowCountdownEl.textContent = nowUiHelpers.formatCountdown(msLeft, timeFormat);
|
||||
elements.nowCountdownEl.textContent = nowUiHelpers.formatCountdown(currentHour.msRemaining, timeFormat);
|
||||
|
||||
if (elements.nowHourNextEl) {
|
||||
const nextHour = allHours.find(
|
||||
(entry) => entry.start.getTime() >= currentHour.end.getTime() - 1000
|
||||
);
|
||||
if (nextHour) {
|
||||
const nextPlanet = referenceData.planets[nextHour.planetId];
|
||||
elements.nowHourNextEl.textContent = nextPlanet
|
||||
? `> ${nextPlanet.name}`
|
||||
: `> ${nextHour.planetId}`;
|
||||
} else {
|
||||
elements.nowHourNextEl.textContent = "> --";
|
||||
}
|
||||
const nextPlanet = currentHour.nextHourPlanet;
|
||||
elements.nowHourNextEl.textContent = nextPlanet
|
||||
? `> ${nextPlanet.name}`
|
||||
: "> --";
|
||||
}
|
||||
|
||||
nowUiHelpers.setNowCardImage(
|
||||
elements.nowHourCardEl,
|
||||
planet?.tarot?.majorArcana,
|
||||
currentHour.planet?.tarot?.majorArcana,
|
||||
"Current planetary hour card",
|
||||
planet?.tarot?.number
|
||||
currentHour.planet?.tarot?.number
|
||||
);
|
||||
} else {
|
||||
elements.nowHourEl.textContent = "--";
|
||||
@@ -94,28 +99,27 @@
|
||||
nowUiHelpers.setNowCardImage(elements.nowHourCardEl, null, "Current planetary hour card");
|
||||
}
|
||||
|
||||
const moonIllum = window.SunCalc.getMoonIllumination(now);
|
||||
const moonPhase = getMoonPhaseName(moonIllum.phase);
|
||||
const moonTarot = referenceData.planets.luna?.tarot?.majorArcana || "The High Priestess";
|
||||
elements.nowMoonEl.textContent = `${moonPhase} (${Math.round(moonIllum.fraction * 100)}%)`;
|
||||
elements.nowMoonTarotEl.textContent = nowUiHelpers.getDisplayTarotName(moonTarot, referenceData.planets.luna?.tarot?.number);
|
||||
const moon = snapshot?.moon || null;
|
||||
const illuminationFraction = Number(moon?.illuminationFraction || 0);
|
||||
const moonTarot = moon?.tarot?.majorArcana || "The High Priestess";
|
||||
elements.nowMoonEl.textContent = moon
|
||||
? `${moon.phase} (${Math.round(illuminationFraction * 100)}%)`
|
||||
: "--";
|
||||
elements.nowMoonTarotEl.textContent = moon
|
||||
? nowUiHelpers.getDisplayTarotName(moonTarot, moon?.tarot?.number)
|
||||
: "--";
|
||||
nowUiHelpers.setNowCardImage(
|
||||
elements.nowMoonCardEl,
|
||||
moonTarot,
|
||||
moon?.tarot?.majorArcana,
|
||||
"Current moon phase card",
|
||||
referenceData.planets.luna?.tarot?.number
|
||||
moon?.tarot?.number
|
||||
);
|
||||
|
||||
if (!moonCountdownCache || moonCountdownCache.fromPhase !== moonPhase || now >= moonCountdownCache.changeAt) {
|
||||
moonCountdownCache = nowUiHelpers.findNextMoonPhaseTransition(now);
|
||||
}
|
||||
|
||||
if (elements.nowMoonCountdownEl) {
|
||||
if (moonCountdownCache?.changeAt) {
|
||||
const remaining = moonCountdownCache.changeAt.getTime() - now.getTime();
|
||||
elements.nowMoonCountdownEl.textContent = nowUiHelpers.formatCountdown(remaining, timeFormat);
|
||||
if (moon?.countdown) {
|
||||
elements.nowMoonCountdownEl.textContent = nowUiHelpers.formatCountdown(moon.countdown.msRemaining, timeFormat);
|
||||
if (elements.nowMoonNextEl) {
|
||||
elements.nowMoonNextEl.textContent = `> ${moonCountdownCache.nextPhase}`;
|
||||
elements.nowMoonNextEl.textContent = `> ${moon.countdown.nextPhase}`;
|
||||
}
|
||||
} else {
|
||||
elements.nowMoonCountdownEl.textContent = "--";
|
||||
@@ -125,45 +129,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
const sunInfo = getDecanForDate(now, referenceData.signs, referenceData.decansBySign);
|
||||
const decanSkyKey = sunInfo?.sign
|
||||
? `${sunInfo.sign.id}-${sunInfo.decan?.index || 1}`
|
||||
: "no-decan";
|
||||
if (sunInfo?.sign) {
|
||||
const signStartDate = nowUiHelpers.getSignStartDate(now, sunInfo.sign);
|
||||
const daysSinceSignStart = (now.getTime() - signStartDate.getTime()) / DAY_IN_MS;
|
||||
const signDegree = Math.min(29.9, Math.max(0, daysSinceSignStart));
|
||||
const signMajorName = nowUiHelpers.getDisplayTarotName(sunInfo.sign.tarot.majorArcana, sunInfo.sign.tarot.trumpNumber);
|
||||
elements.nowDecanEl.textContent = `${sunInfo.sign.symbol} ${sunInfo.sign.name} · ${signMajorName} (${signDegree.toFixed(1)}°)`;
|
||||
const decanInfo = snapshot?.decan || null;
|
||||
if (decanInfo?.sign) {
|
||||
const signMajorName = nowUiHelpers.getDisplayTarotName(
|
||||
decanInfo.sign?.tarot?.majorArcana,
|
||||
decanInfo.sign?.tarot?.number
|
||||
);
|
||||
const signDegree = Number.isFinite(Number(decanInfo.signDegree))
|
||||
? Number(decanInfo.signDegree).toFixed(1)
|
||||
: "0.0";
|
||||
|
||||
const currentDecanKey = `${sunInfo.sign.id}-${sunInfo.decan?.index || 1}`;
|
||||
if (!decanCountdownCache || decanCountdownCache.key !== currentDecanKey || now >= decanCountdownCache.changeAt) {
|
||||
decanCountdownCache = nowUiHelpers.findNextDecanTransition(now, referenceData.signs, referenceData.decansBySign);
|
||||
}
|
||||
elements.nowDecanEl.textContent = `${decanInfo.sign.symbol} ${decanInfo.sign.name} · ${signMajorName} (${signDegree}°)`;
|
||||
|
||||
if (sunInfo.decan) {
|
||||
const decanCardName = sunInfo.decan.tarotMinorArcana;
|
||||
elements.nowDecanTarotEl.textContent = nowUiHelpers.getDisplayTarotName(decanCardName);
|
||||
nowUiHelpers.setNowCardImage(elements.nowDecanCardEl, sunInfo.decan.tarotMinorArcana, "Current decan card");
|
||||
if (decanInfo.decan?.tarotMinorArcana) {
|
||||
elements.nowDecanTarotEl.textContent = nowUiHelpers.getDisplayTarotName(decanInfo.decan.tarotMinorArcana);
|
||||
nowUiHelpers.setNowCardImage(elements.nowDecanCardEl, decanInfo.decan.tarotMinorArcana, "Current decan card");
|
||||
} else {
|
||||
const signTarotName = sunInfo.sign.tarot?.majorArcana || "--";
|
||||
const signTarotName = decanInfo.sign?.tarot?.majorArcana || "--";
|
||||
elements.nowDecanTarotEl.textContent = signTarotName === "--"
|
||||
? "--"
|
||||
: nowUiHelpers.getDisplayTarotName(signTarotName, sunInfo.sign.tarot?.trumpNumber);
|
||||
: nowUiHelpers.getDisplayTarotName(signTarotName, decanInfo.sign?.tarot?.number);
|
||||
nowUiHelpers.setNowCardImage(
|
||||
elements.nowDecanCardEl,
|
||||
sunInfo.sign.tarot?.majorArcana,
|
||||
decanInfo.sign?.tarot?.majorArcana,
|
||||
"Current decan card",
|
||||
sunInfo.sign.tarot?.trumpNumber
|
||||
decanInfo.sign?.tarot?.number
|
||||
);
|
||||
}
|
||||
|
||||
if (elements.nowDecanCountdownEl) {
|
||||
if (decanCountdownCache?.changeAt) {
|
||||
const remaining = decanCountdownCache.changeAt.getTime() - now.getTime();
|
||||
elements.nowDecanCountdownEl.textContent = nowUiHelpers.formatCountdown(remaining, timeFormat);
|
||||
if (decanInfo.countdown) {
|
||||
elements.nowDecanCountdownEl.textContent = nowUiHelpers.formatCountdown(decanInfo.countdown.msRemaining, timeFormat);
|
||||
if (elements.nowDecanNextEl) {
|
||||
elements.nowDecanNextEl.textContent = `> ${nowUiHelpers.getDisplayTarotName(decanCountdownCache.nextLabel)}`;
|
||||
elements.nowDecanNextEl.textContent = `> ${nowUiHelpers.getDisplayTarotName(decanInfo.countdown.nextLabel)}`;
|
||||
}
|
||||
} else {
|
||||
elements.nowDecanCountdownEl.textContent = "--";
|
||||
@@ -184,14 +182,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
nowUiHelpers.updateNowStats(referenceData, elements, now);
|
||||
renderNowStatsFromSnapshot(elements, snapshot?.stats || {});
|
||||
|
||||
return {
|
||||
dayKey,
|
||||
skyRefreshKey: `${currentHourSkyKey}|${decanSkyKey}|${moonPhase}`
|
||||
skyRefreshKey: String(snapshot?.skyRefreshKey || "")
|
||||
};
|
||||
}
|
||||
|
||||
async function updateNowPanel(referenceData, geo, elements, timeFormat = "minutes") {
|
||||
if (!referenceData || !geo || !elements) {
|
||||
return { dayKey: getDateKey(new Date()), skyRefreshKey: "" };
|
||||
}
|
||||
|
||||
const snapshot = await dataService.fetchNowSnapshot?.(geo, new Date());
|
||||
return applyNowSnapshot(elements, snapshot || {}, timeFormat);
|
||||
}
|
||||
|
||||
window.TarotNowUi = {
|
||||
updateNowPanel
|
||||
};
|
||||
|
||||
102
app/ui-quiz.js
102
app/ui-quiz.js
@@ -2,6 +2,8 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const dataService = window.TarotDataService || {};
|
||||
|
||||
const state = {
|
||||
initialized: false,
|
||||
scoreCorrect: 0,
|
||||
@@ -15,6 +17,7 @@
|
||||
runRetrySet: new Set(),
|
||||
currentQuestion: null,
|
||||
answeredCurrent: false,
|
||||
loadingQuestion: false,
|
||||
autoAdvanceTimer: null,
|
||||
autoAdvanceDelayMs: 1500
|
||||
};
|
||||
@@ -239,49 +242,15 @@
|
||||
};
|
||||
}
|
||||
|
||||
function instantiateQuestion(template) {
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prompt = String(resolveDifficultyValue(template.promptByDifficulty) || "").trim();
|
||||
const answer = normalizeOption(resolveDifficultyValue(template.answerByDifficulty));
|
||||
const pool = toUniqueOptionList(resolveDifficultyValue(template.poolByDifficulty) || []);
|
||||
|
||||
if (!prompt || !answer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const built = buildOptions(answer, pool);
|
||||
if (!built) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key: template.key,
|
||||
categoryId: template.categoryId,
|
||||
category: template.category,
|
||||
prompt,
|
||||
answer,
|
||||
options: built.options,
|
||||
correctIndex: built.correctIndex
|
||||
};
|
||||
async function buildQuestionBank(referenceData, magickDataset) {
|
||||
const payload = await dataService.loadQuizTemplates?.();
|
||||
return Array.isArray(payload?.templates)
|
||||
? payload.templates
|
||||
: (Array.isArray(payload) ? payload : []);
|
||||
}
|
||||
|
||||
function buildQuestionBank(referenceData, magickDataset) {
|
||||
if (typeof quizQuestionBank.buildQuestionBank !== "function") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return quizQuestionBank.buildQuestionBank(
|
||||
referenceData,
|
||||
magickDataset,
|
||||
DYNAMIC_CATEGORY_REGISTRY
|
||||
);
|
||||
}
|
||||
|
||||
function refreshQuestionBank(referenceData, magickDataset) {
|
||||
state.questionBank = buildQuestionBank(referenceData, magickDataset);
|
||||
async function refreshQuestionBank(referenceData, magickDataset) {
|
||||
state.questionBank = await buildQuestionBank(referenceData, magickDataset);
|
||||
state.templateByKey = new Map(state.questionBank.map((template) => [template.key, template]));
|
||||
|
||||
const hasTemplate = (key) => state.templateByKey.has(key);
|
||||
@@ -380,7 +349,7 @@
|
||||
|| "Category";
|
||||
}
|
||||
|
||||
function startRun(resetScore = false) {
|
||||
async function startRun(resetScore = false) {
|
||||
clearAutoAdvanceTimer();
|
||||
|
||||
if (resetScore) {
|
||||
@@ -425,7 +394,7 @@
|
||||
state.answeredCurrent = true;
|
||||
|
||||
updateScoreboard();
|
||||
showNextQuestion();
|
||||
await showNextQuestion();
|
||||
}
|
||||
|
||||
function popNextTemplateFromRun() {
|
||||
@@ -450,6 +419,7 @@
|
||||
}
|
||||
|
||||
function renderRunCompleteState() {
|
||||
state.loadingQuestion = false;
|
||||
state.currentQuestion = null;
|
||||
state.answeredCurrent = true;
|
||||
questionTypeEl.textContent = getRunLabel();
|
||||
@@ -479,6 +449,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
state.loadingQuestion = false;
|
||||
state.currentQuestion = question;
|
||||
state.answeredCurrent = false;
|
||||
|
||||
@@ -505,6 +476,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
state.loadingQuestion = false;
|
||||
state.currentQuestion = null;
|
||||
state.answeredCurrent = true;
|
||||
|
||||
@@ -514,9 +486,25 @@
|
||||
feedbackEl.textContent = "Try Random/All or switch to another category.";
|
||||
}
|
||||
|
||||
function showNextQuestion() {
|
||||
async function createQuestionFromTemplate(template) {
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dataService.pullQuizQuestion?.({
|
||||
templateKey: template.key,
|
||||
difficulty: getActiveDifficulty(),
|
||||
includeAnswer: true
|
||||
});
|
||||
}
|
||||
|
||||
async function showNextQuestion() {
|
||||
clearAutoAdvanceTimer();
|
||||
|
||||
if (state.loadingQuestion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPending = state.runUnseenKeys.length + state.runRetryKeys.length;
|
||||
if (totalPending <= 0) {
|
||||
if (state.questionBank.length) {
|
||||
@@ -528,13 +516,21 @@
|
||||
}
|
||||
|
||||
const maxAttempts = totalPending + 1;
|
||||
state.loadingQuestion = true;
|
||||
feedbackEl.textContent = "Loading question...";
|
||||
|
||||
for (let index = 0; index < maxAttempts; index += 1) {
|
||||
const template = popNextTemplateFromRun();
|
||||
if (!template) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const question = instantiateQuestion(template);
|
||||
let question = null;
|
||||
try {
|
||||
question = await createQuestionFromTemplate(template);
|
||||
} catch (_error) {
|
||||
question = null;
|
||||
}
|
||||
if (question) {
|
||||
renderQuestion(question);
|
||||
return;
|
||||
@@ -589,21 +585,21 @@
|
||||
|
||||
categoryEl.addEventListener("change", () => {
|
||||
state.selectedCategory = String(categoryEl.value || "random");
|
||||
startRun(true);
|
||||
void startRun(true);
|
||||
});
|
||||
|
||||
difficultyEl.addEventListener("change", () => {
|
||||
state.selectedDifficulty = String(difficultyEl.value || "normal").toLowerCase();
|
||||
syncDifficultyControl();
|
||||
startRun(true);
|
||||
void startRun(true);
|
||||
});
|
||||
|
||||
resetEl.addEventListener("click", () => {
|
||||
startRun(true);
|
||||
void startRun(true);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureQuizSection(referenceData, magickDataset) {
|
||||
async function ensureQuizSection(referenceData, magickDataset) {
|
||||
ensureQuizSection._referenceData = referenceData;
|
||||
ensureQuizSection._magickDataset = magickDataset;
|
||||
|
||||
@@ -618,23 +614,23 @@
|
||||
updateScoreboard();
|
||||
}
|
||||
|
||||
refreshQuestionBank(referenceData, magickDataset);
|
||||
await refreshQuestionBank(referenceData, magickDataset);
|
||||
const categoryAdjusted = renderCategoryOptions();
|
||||
syncDifficultyControl();
|
||||
|
||||
if (categoryAdjusted) {
|
||||
startRun(false);
|
||||
await startRun(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasRunPending = state.runUnseenKeys.length > 0 || state.runRetryKeys.length > 0;
|
||||
if (!state.currentQuestion && !hasRunPending) {
|
||||
startRun(false);
|
||||
await startRun(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.currentQuestion && hasRunPending) {
|
||||
showNextQuestion();
|
||||
await showNextQuestion();
|
||||
}
|
||||
|
||||
updateScoreboard();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
onSettingsApplied: null,
|
||||
onSyncSkyBackground: null,
|
||||
onStatus: null,
|
||||
onConnectionSaved: null,
|
||||
onReopenActiveSection: null,
|
||||
getActiveSection: null,
|
||||
onRenderWeek: null
|
||||
@@ -30,10 +31,37 @@
|
||||
timeFormatEl: document.getElementById("time-format"),
|
||||
birthDateEl: document.getElementById("birth-date"),
|
||||
tarotDeckEl: document.getElementById("tarot-deck"),
|
||||
apiBaseUrlEl: document.getElementById("api-base-url"),
|
||||
apiKeyEl: document.getElementById("api-key"),
|
||||
saveSettingsEl: document.getElementById("save-settings"),
|
||||
useLocationEl: document.getElementById("use-location")
|
||||
};
|
||||
}
|
||||
function getConnectionSettings() {
|
||||
return window.TarotAppConfig?.getConnectionSettings?.() || {
|
||||
apiBaseUrl: String(window.TarotAppConfig?.apiBaseUrl || "").trim(),
|
||||
apiKey: String(window.TarotAppConfig?.apiKey || "").trim()
|
||||
};
|
||||
}
|
||||
|
||||
function syncConnectionInputs() {
|
||||
const { apiBaseUrlEl, apiKeyEl } = getElements();
|
||||
const connectionSettings = getConnectionSettings();
|
||||
|
||||
if (apiBaseUrlEl) {
|
||||
apiBaseUrlEl.value = String(connectionSettings.apiBaseUrl || "");
|
||||
}
|
||||
|
||||
if (apiKeyEl) {
|
||||
apiKeyEl.value = String(connectionSettings.apiKey || "");
|
||||
}
|
||||
}
|
||||
|
||||
function hasConnectionChanged(previous, next) {
|
||||
return String(previous?.apiBaseUrl || "").trim() !== String(next?.apiBaseUrl || "").trim()
|
||||
|| String(previous?.apiKey || "").trim() !== String(next?.apiKey || "").trim();
|
||||
}
|
||||
|
||||
|
||||
function setStatus(text) {
|
||||
if (typeof config.onStatus === "function") {
|
||||
@@ -259,6 +287,7 @@
|
||||
function applySettingsToInputs(settings) {
|
||||
const { latEl, lngEl, timeFormatEl, birthDateEl, tarotDeckEl } = getElements();
|
||||
syncTarotDeckInputOptions();
|
||||
syncConnectionInputs();
|
||||
const normalized = normalizeSettings(settings);
|
||||
latEl.value = String(normalized.latitude);
|
||||
lngEl.value = String(normalized.longitude);
|
||||
@@ -292,12 +321,22 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getConnectionSettingsFromInputs() {
|
||||
const { apiBaseUrlEl, apiKeyEl } = getElements();
|
||||
|
||||
return {
|
||||
apiBaseUrl: String(apiBaseUrlEl?.value || "").trim(),
|
||||
apiKey: String(apiKeyEl?.value || "").trim()
|
||||
};
|
||||
}
|
||||
|
||||
function openSettingsPopup() {
|
||||
const { settingsPopupEl, openSettingsEl } = getElements();
|
||||
if (!settingsPopupEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
applySettingsToInputs(loadSavedSettings());
|
||||
settingsPopupEl.hidden = false;
|
||||
if (openSettingsEl) {
|
||||
openSettingsEl.setAttribute("aria-expanded", "true");
|
||||
@@ -319,6 +358,10 @@
|
||||
async function handleSaveSettings() {
|
||||
try {
|
||||
const settings = getSettingsFromInputs();
|
||||
const previousConnectionSettings = getConnectionSettings();
|
||||
const connectionSettings = getConnectionSettingsFromInputs();
|
||||
const connectionChanged = hasConnectionChanged(previousConnectionSettings, connectionSettings);
|
||||
const connectionResult = window.TarotAppConfig?.updateConnectionSettings?.(connectionSettings) || { didPersist: true };
|
||||
const normalized = applySettingsToInputs(settings);
|
||||
syncSky({ latitude: normalized.latitude, longitude: normalized.longitude }, true);
|
||||
const didPersist = saveSettings(normalized);
|
||||
@@ -327,11 +370,13 @@
|
||||
config.onReopenActiveSection?.(config.getActiveSection());
|
||||
}
|
||||
closeSettingsPopup();
|
||||
if (typeof config.onRenderWeek === "function") {
|
||||
if (connectionChanged && typeof config.onConnectionSaved === "function") {
|
||||
await config.onConnectionSaved(connectionResult, connectionSettings);
|
||||
} else if (typeof config.onRenderWeek === "function") {
|
||||
await config.onRenderWeek();
|
||||
}
|
||||
|
||||
if (!didPersist) {
|
||||
if (!didPersist || connectionResult.didPersist === false) {
|
||||
setStatus("Settings applied for this session. Browser storage is unavailable.");
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -419,6 +464,11 @@
|
||||
closeSettingsPopup();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("connection:updated", () => {
|
||||
syncConnectionInputs();
|
||||
syncTarotDeckInputOptions();
|
||||
});
|
||||
}
|
||||
|
||||
function init(nextConfig = {}) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const dataService = window.TarotDataService || {};
|
||||
|
||||
let initialized = false;
|
||||
let activeTarotSpread = null;
|
||||
let activeTarotSpreadDraw = [];
|
||||
let activeTarotSpreadLoading = false;
|
||||
let config = {
|
||||
ensureTarotSection: null,
|
||||
getReferenceData: () => null,
|
||||
@@ -59,22 +62,6 @@
|
||||
return value === "celtic-cross" ? "celtic-cross" : "three-card";
|
||||
}
|
||||
|
||||
function drawNFromDeck(n) {
|
||||
const allCards = window.TarotSectionUi?.getCards?.() || [];
|
||||
if (!allCards.length) return [];
|
||||
|
||||
const shuffled = [...allCards];
|
||||
for (let index = shuffled.length - 1; index > 0; index -= 1) {
|
||||
const swapIndex = Math.floor(Math.random() * (index + 1));
|
||||
[shuffled[index], shuffled[swapIndex]] = [shuffled[swapIndex], shuffled[index]];
|
||||
}
|
||||
|
||||
return shuffled.slice(0, n).map((card) => ({
|
||||
...card,
|
||||
reversed: Math.random() < 0.3
|
||||
}));
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
.replace(/&/g, "&")
|
||||
@@ -88,15 +75,27 @@
|
||||
return spreadId === "celtic-cross" ? CELTIC_CROSS_POSITIONS : THREE_CARD_POSITIONS;
|
||||
}
|
||||
|
||||
function regenerateTarotSpreadDraw() {
|
||||
async function regenerateTarotSpreadDraw() {
|
||||
const normalizedSpread = normalizeTarotSpread(activeTarotSpread);
|
||||
const positions = getSpreadPositions(normalizedSpread);
|
||||
const cards = drawNFromDeck(positions.length);
|
||||
activeTarotSpreadDraw = positions.map((position, index) => ({
|
||||
position,
|
||||
card: cards[index] || null,
|
||||
revealed: false
|
||||
}));
|
||||
|
||||
activeTarotSpreadLoading = true;
|
||||
try {
|
||||
const payload = await dataService.pullTarotSpread?.(normalizedSpread);
|
||||
const apiPositions = Array.isArray(payload?.positions) ? payload.positions : [];
|
||||
activeTarotSpreadDraw = apiPositions.map((entry, index) => ({
|
||||
position: entry?.position || positions[index] || null,
|
||||
card: entry?.card
|
||||
? {
|
||||
...entry.card,
|
||||
reversed: Boolean(entry?.reversed ?? entry.card?.reversed)
|
||||
}
|
||||
: null,
|
||||
revealed: false
|
||||
}));
|
||||
} finally {
|
||||
activeTarotSpreadLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTarotSpreadMeanings() {
|
||||
@@ -158,8 +157,20 @@
|
||||
|| ""
|
||||
).trim();
|
||||
|
||||
if (activeTarotSpreadLoading) {
|
||||
tarotSpreadBoardEl.innerHTML = '<div class="spread-empty">Loading spread from API...</div>';
|
||||
if (tarotSpreadMeaningsEl) {
|
||||
tarotSpreadMeaningsEl.innerHTML = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeTarotSpreadDraw.length) {
|
||||
regenerateTarotSpreadDraw();
|
||||
tarotSpreadBoardEl.innerHTML = '<div class="spread-empty">Loading spread...</div>';
|
||||
if (tarotSpreadMeaningsEl) {
|
||||
tarotSpreadMeaningsEl.innerHTML = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
tarotSpreadBoardEl.className = `tarot-spread-board tarot-spread-board--${isCeltic ? "celtic" : "three"}`;
|
||||
@@ -267,10 +278,14 @@
|
||||
|
||||
function showTarotSpreadView(spreadId = "three-card") {
|
||||
activeTarotSpread = normalizeTarotSpread(spreadId);
|
||||
regenerateTarotSpreadDraw();
|
||||
activeTarotSpreadLoading = true;
|
||||
activeTarotSpreadDraw = [];
|
||||
applyViewState();
|
||||
ensureTarotBrowseData();
|
||||
renderTarotSpread();
|
||||
void regenerateTarotSpreadDraw().then(() => {
|
||||
renderTarotSpread();
|
||||
});
|
||||
}
|
||||
|
||||
function setSpread(spreadId, openTarotSection = false) {
|
||||
@@ -281,8 +296,12 @@
|
||||
}
|
||||
|
||||
function revealAll() {
|
||||
if (activeTarotSpreadLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeTarotSpreadDraw.length) {
|
||||
regenerateTarotSpreadDraw();
|
||||
return;
|
||||
}
|
||||
|
||||
activeTarotSpreadDraw.forEach((entry) => {
|
||||
@@ -376,8 +395,12 @@
|
||||
|
||||
if (tarotSpreadRedrawEl) {
|
||||
tarotSpreadRedrawEl.addEventListener("click", () => {
|
||||
regenerateTarotSpreadDraw();
|
||||
activeTarotSpreadLoading = true;
|
||||
activeTarotSpreadDraw = [];
|
||||
renderTarotSpread();
|
||||
void regenerateTarotSpreadDraw().then(() => {
|
||||
renderTarotSpread();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
285
app/ui-tarot.js
285
app/ui-tarot.js
@@ -1,4 +1,5 @@
|
||||
(function () {
|
||||
const dataService = window.TarotDataService || {};
|
||||
const {
|
||||
resolveTarotCardImage,
|
||||
resolveTarotCardThumbnail,
|
||||
@@ -41,7 +42,8 @@
|
||||
magickDataset: null,
|
||||
referenceData: null,
|
||||
monthRefsByCardId: new Map(),
|
||||
courtCardByDecanId: new Map()
|
||||
courtCardByDecanId: new Map(),
|
||||
loadingPromise: null
|
||||
};
|
||||
|
||||
const TAROT_TRUMP_NUMBER_BY_NAME = {
|
||||
@@ -781,7 +783,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
function ensureTarotSection(referenceData, magickDataset = null) {
|
||||
async function loadCards(referenceData, magickDataset) {
|
||||
const payload = await dataService.loadTarotCards?.();
|
||||
const cards = Array.isArray(payload?.cards)
|
||||
? payload.cards
|
||||
: (Array.isArray(payload) ? payload : []);
|
||||
|
||||
return cards.map((card) => ({
|
||||
...card,
|
||||
id: String(card?.id || "").trim() || cardId(card)
|
||||
}));
|
||||
}
|
||||
|
||||
async function ensureTarotSection(referenceData, magickDataset = null) {
|
||||
state.referenceData = referenceData || state.referenceData;
|
||||
|
||||
if (magickDataset) {
|
||||
@@ -853,151 +867,156 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const databaseBuilder = window.TarotCardDatabase?.buildTarotDatabase;
|
||||
if (typeof databaseBuilder !== "function") {
|
||||
if (state.loadingPromise) {
|
||||
await state.loadingPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
const cards = databaseBuilder(referenceData, magickDataset).map((card) => ({
|
||||
...card,
|
||||
id: cardId(card)
|
||||
}));
|
||||
state.loadingPromise = (async () => {
|
||||
const cards = await loadCards(referenceData, magickDataset);
|
||||
|
||||
state.cards = cards;
|
||||
state.monthRefsByCardId = buildMonthReferencesByCard(referenceData, cards);
|
||||
state.courtCardByDecanId = buildCourtCardByDecanId(cards);
|
||||
state.filteredCards = [...cards];
|
||||
renderList(elements);
|
||||
renderHouseOfCards(elements);
|
||||
syncHouseControls(elements);
|
||||
state.cards = cards;
|
||||
state.monthRefsByCardId = buildMonthReferencesByCard(referenceData, cards);
|
||||
state.courtCardByDecanId = buildCourtCardByDecanId(cards);
|
||||
state.filteredCards = [...cards];
|
||||
renderList(elements);
|
||||
renderHouseOfCards(elements);
|
||||
syncHouseControls(elements);
|
||||
|
||||
if (cards.length > 0) {
|
||||
selectCardById(cards[0].id, elements);
|
||||
}
|
||||
|
||||
elements.tarotCardListEl.addEventListener("click", (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Node)) {
|
||||
return;
|
||||
if (cards.length > 0) {
|
||||
selectCardById(cards[0].id, elements);
|
||||
}
|
||||
|
||||
const button = target instanceof Element
|
||||
? target.closest(".tarot-list-item")
|
||||
: null;
|
||||
|
||||
if (!(button instanceof HTMLButtonElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedId = button.dataset.cardId;
|
||||
if (!selectedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectCardById(selectedId, elements);
|
||||
});
|
||||
|
||||
if (elements.tarotSearchInputEl) {
|
||||
elements.tarotSearchInputEl.addEventListener("input", () => {
|
||||
state.searchQuery = elements.tarotSearchInputEl.value || "";
|
||||
applySearchFilter(elements);
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.tarotSearchClearEl && elements.tarotSearchInputEl) {
|
||||
elements.tarotSearchClearEl.addEventListener("click", () => {
|
||||
elements.tarotSearchInputEl.value = "";
|
||||
state.searchQuery = "";
|
||||
applySearchFilter(elements);
|
||||
elements.tarotSearchInputEl.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.tarotHouseFocusToggleEl) {
|
||||
elements.tarotHouseFocusToggleEl.addEventListener("click", () => {
|
||||
state.houseFocusMode = !state.houseFocusMode;
|
||||
syncHouseControls(elements);
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.tarotHouseTopCardsVisibleEl) {
|
||||
elements.tarotHouseTopCardsVisibleEl.addEventListener("change", () => {
|
||||
state.houseTopCardsVisible = Boolean(elements.tarotHouseTopCardsVisibleEl.checked);
|
||||
renderHouseOfCards(elements);
|
||||
syncHouseControls(elements);
|
||||
});
|
||||
}
|
||||
|
||||
[
|
||||
[elements.tarotHouseTopInfoHebrewEl, "hebrew"],
|
||||
[elements.tarotHouseTopInfoPlanetEl, "planet"],
|
||||
[elements.tarotHouseTopInfoZodiacEl, "zodiac"],
|
||||
[elements.tarotHouseTopInfoTrumpEl, "trump"],
|
||||
[elements.tarotHouseTopInfoPathEl, "path"]
|
||||
].forEach(([checkbox, key]) => {
|
||||
if (!checkbox) {
|
||||
return;
|
||||
}
|
||||
checkbox.addEventListener("change", () => {
|
||||
state.houseTopInfoModes[key] = Boolean(checkbox.checked);
|
||||
renderHouseOfCards(elements);
|
||||
syncHouseControls(elements);
|
||||
});
|
||||
});
|
||||
|
||||
if (elements.tarotHouseBottomCardsVisibleEl) {
|
||||
elements.tarotHouseBottomCardsVisibleEl.addEventListener("change", () => {
|
||||
state.houseBottomCardsVisible = Boolean(elements.tarotHouseBottomCardsVisibleEl.checked);
|
||||
renderHouseOfCards(elements);
|
||||
syncHouseControls(elements);
|
||||
});
|
||||
}
|
||||
|
||||
[
|
||||
[elements.tarotHouseBottomInfoZodiacEl, "zodiac"],
|
||||
[elements.tarotHouseBottomInfoDecanEl, "decan"],
|
||||
[elements.tarotHouseBottomInfoMonthEl, "month"],
|
||||
[elements.tarotHouseBottomInfoRulerEl, "ruler"],
|
||||
[elements.tarotHouseBottomInfoDateEl, "date"]
|
||||
].forEach(([checkbox, key]) => {
|
||||
if (!checkbox) {
|
||||
return;
|
||||
}
|
||||
checkbox.addEventListener("change", () => {
|
||||
state.houseBottomInfoModes[key] = Boolean(checkbox.checked);
|
||||
renderHouseOfCards(elements);
|
||||
syncHouseControls(elements);
|
||||
});
|
||||
});
|
||||
|
||||
if (elements.tarotHouseExportEl) {
|
||||
elements.tarotHouseExportEl.addEventListener("click", () => {
|
||||
exportHouseOfCards(elements, "png");
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.tarotHouseExportWebpEl) {
|
||||
elements.tarotHouseExportWebpEl.addEventListener("click", () => {
|
||||
exportHouseOfCards(elements, "webp");
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.tarotDetailImageEl) {
|
||||
elements.tarotDetailImageEl.addEventListener("click", () => {
|
||||
if (elements.tarotDetailImageEl.style.display === "none" || !state.selectedCardId) {
|
||||
elements.tarotCardListEl.addEventListener("click", (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = buildLightboxCardRequestById(state.selectedCardId);
|
||||
if (!request?.src) {
|
||||
const button = target instanceof Element
|
||||
? target.closest(".tarot-list-item")
|
||||
: null;
|
||||
|
||||
if (!(button instanceof HTMLButtonElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.TarotUiLightbox?.open?.(request);
|
||||
});
|
||||
}
|
||||
const selectedId = button.dataset.cardId;
|
||||
if (!selectedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.initialized = true;
|
||||
selectCardById(selectedId, elements);
|
||||
});
|
||||
|
||||
if (elements.tarotSearchInputEl) {
|
||||
elements.tarotSearchInputEl.addEventListener("input", () => {
|
||||
state.searchQuery = elements.tarotSearchInputEl.value || "";
|
||||
applySearchFilter(elements);
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.tarotSearchClearEl && elements.tarotSearchInputEl) {
|
||||
elements.tarotSearchClearEl.addEventListener("click", () => {
|
||||
elements.tarotSearchInputEl.value = "";
|
||||
state.searchQuery = "";
|
||||
applySearchFilter(elements);
|
||||
elements.tarotSearchInputEl.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.tarotHouseFocusToggleEl) {
|
||||
elements.tarotHouseFocusToggleEl.addEventListener("click", () => {
|
||||
state.houseFocusMode = !state.houseFocusMode;
|
||||
syncHouseControls(elements);
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.tarotHouseTopCardsVisibleEl) {
|
||||
elements.tarotHouseTopCardsVisibleEl.addEventListener("change", () => {
|
||||
state.houseTopCardsVisible = Boolean(elements.tarotHouseTopCardsVisibleEl.checked);
|
||||
renderHouseOfCards(elements);
|
||||
syncHouseControls(elements);
|
||||
});
|
||||
}
|
||||
|
||||
[
|
||||
[elements.tarotHouseTopInfoHebrewEl, "hebrew"],
|
||||
[elements.tarotHouseTopInfoPlanetEl, "planet"],
|
||||
[elements.tarotHouseTopInfoZodiacEl, "zodiac"],
|
||||
[elements.tarotHouseTopInfoTrumpEl, "trump"],
|
||||
[elements.tarotHouseTopInfoPathEl, "path"]
|
||||
].forEach(([checkbox, key]) => {
|
||||
if (!checkbox) {
|
||||
return;
|
||||
}
|
||||
checkbox.addEventListener("change", () => {
|
||||
state.houseTopInfoModes[key] = Boolean(checkbox.checked);
|
||||
renderHouseOfCards(elements);
|
||||
syncHouseControls(elements);
|
||||
});
|
||||
});
|
||||
|
||||
if (elements.tarotHouseBottomCardsVisibleEl) {
|
||||
elements.tarotHouseBottomCardsVisibleEl.addEventListener("change", () => {
|
||||
state.houseBottomCardsVisible = Boolean(elements.tarotHouseBottomCardsVisibleEl.checked);
|
||||
renderHouseOfCards(elements);
|
||||
syncHouseControls(elements);
|
||||
});
|
||||
}
|
||||
|
||||
[
|
||||
[elements.tarotHouseBottomInfoZodiacEl, "zodiac"],
|
||||
[elements.tarotHouseBottomInfoDecanEl, "decan"],
|
||||
[elements.tarotHouseBottomInfoMonthEl, "month"],
|
||||
[elements.tarotHouseBottomInfoRulerEl, "ruler"],
|
||||
[elements.tarotHouseBottomInfoDateEl, "date"]
|
||||
].forEach(([checkbox, key]) => {
|
||||
if (!checkbox) {
|
||||
return;
|
||||
}
|
||||
checkbox.addEventListener("change", () => {
|
||||
state.houseBottomInfoModes[key] = Boolean(checkbox.checked);
|
||||
renderHouseOfCards(elements);
|
||||
syncHouseControls(elements);
|
||||
});
|
||||
});
|
||||
|
||||
if (elements.tarotHouseExportEl) {
|
||||
elements.tarotHouseExportEl.addEventListener("click", () => {
|
||||
exportHouseOfCards(elements, "png");
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.tarotHouseExportWebpEl) {
|
||||
elements.tarotHouseExportWebpEl.addEventListener("click", () => {
|
||||
exportHouseOfCards(elements, "webp");
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.tarotDetailImageEl) {
|
||||
elements.tarotDetailImageEl.addEventListener("click", () => {
|
||||
if (elements.tarotDetailImageEl.style.display === "none" || !state.selectedCardId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = buildLightboxCardRequestById(state.selectedCardId);
|
||||
if (!request?.src) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.TarotUiLightbox?.open?.(request);
|
||||
});
|
||||
}
|
||||
|
||||
state.initialized = true;
|
||||
})();
|
||||
|
||||
try {
|
||||
await state.loadingPromise;
|
||||
} finally {
|
||||
state.loadingPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
function selectCardByTrump(trumpNumber) {
|
||||
|
||||
Reference in New Issue
Block a user