672 lines
19 KiB
JavaScript
672 lines
19 KiB
JavaScript
(function () {
|
|
let magickManifestCache = null;
|
|
let magickDataCache = null;
|
|
let deckOptionsCache = null;
|
|
const deckManifestCache = new Map();
|
|
let quizCategoriesCache = null;
|
|
const quizTemplatesCache = new Map();
|
|
let textLibraryCache = null;
|
|
const textSourceCache = new Map();
|
|
const textSectionCache = new Map();
|
|
const textLexiconCache = new Map();
|
|
const textLexiconOccurrencesCache = new Map();
|
|
const textSearchCache = new Map();
|
|
|
|
const DATA_ROOT = "data";
|
|
const MAGICK_ROOT = DATA_ROOT;
|
|
|
|
const TAROT_TRUMP_NUMBER_BY_NAME = {
|
|
"the fool": 0,
|
|
fool: 0,
|
|
"the magus": 1,
|
|
magus: 1,
|
|
magician: 1,
|
|
"the high priestess": 2,
|
|
"high priestess": 2,
|
|
"the empress": 3,
|
|
empress: 3,
|
|
"the emperor": 4,
|
|
emperor: 4,
|
|
"the hierophant": 5,
|
|
hierophant: 5,
|
|
"the lovers": 6,
|
|
lovers: 6,
|
|
"the chariot": 7,
|
|
chariot: 7,
|
|
strength: 8,
|
|
lust: 8,
|
|
"the hermit": 9,
|
|
hermit: 9,
|
|
fortune: 10,
|
|
"wheel of fortune": 10,
|
|
justice: 11,
|
|
"the hanged man": 12,
|
|
"hanged man": 12,
|
|
death: 13,
|
|
temperance: 14,
|
|
art: 14,
|
|
"the devil": 15,
|
|
devil: 15,
|
|
"the tower": 16,
|
|
tower: 16,
|
|
"the star": 17,
|
|
star: 17,
|
|
"the moon": 18,
|
|
moon: 18,
|
|
"the sun": 19,
|
|
sun: 19,
|
|
aeon: 20,
|
|
judgement: 20,
|
|
judgment: 20,
|
|
universe: 21,
|
|
world: 21,
|
|
"the world": 21
|
|
};
|
|
|
|
const HEBREW_BY_TRUMP_NUMBER = {
|
|
0: { hebrewLetterId: "alef", kabbalahPathNumber: 11 },
|
|
1: { hebrewLetterId: "bet", kabbalahPathNumber: 12 },
|
|
2: { hebrewLetterId: "gimel", kabbalahPathNumber: 13 },
|
|
3: { hebrewLetterId: "dalet", kabbalahPathNumber: 14 },
|
|
4: { hebrewLetterId: "he", kabbalahPathNumber: 15 },
|
|
5: { hebrewLetterId: "vav", kabbalahPathNumber: 16 },
|
|
6: { hebrewLetterId: "zayin", kabbalahPathNumber: 17 },
|
|
7: { hebrewLetterId: "het", kabbalahPathNumber: 18 },
|
|
8: { hebrewLetterId: "tet", kabbalahPathNumber: 19 },
|
|
9: { hebrewLetterId: "yod", kabbalahPathNumber: 20 },
|
|
10: { hebrewLetterId: "kaf", kabbalahPathNumber: 21 },
|
|
11: { hebrewLetterId: "lamed", kabbalahPathNumber: 22 },
|
|
12: { hebrewLetterId: "mem", kabbalahPathNumber: 23 },
|
|
13: { hebrewLetterId: "nun", kabbalahPathNumber: 24 },
|
|
14: { hebrewLetterId: "samekh", kabbalahPathNumber: 25 },
|
|
15: { hebrewLetterId: "ayin", kabbalahPathNumber: 26 },
|
|
16: { hebrewLetterId: "pe", kabbalahPathNumber: 27 },
|
|
17: { hebrewLetterId: "tsadi", kabbalahPathNumber: 28 },
|
|
18: { hebrewLetterId: "qof", kabbalahPathNumber: 29 },
|
|
19: { hebrewLetterId: "resh", kabbalahPathNumber: 30 },
|
|
20: { hebrewLetterId: "shin", kabbalahPathNumber: 31 },
|
|
21: { hebrewLetterId: "tav", kabbalahPathNumber: 32 }
|
|
};
|
|
|
|
const ICHING_PLANET_BY_PLANET_ID = {
|
|
sol: "Sun",
|
|
luna: "Moon",
|
|
mercury: "Mercury",
|
|
venus: "Venus",
|
|
mars: "Mars",
|
|
jupiter: "Jupiter",
|
|
saturn: "Saturn",
|
|
earth: "Earth",
|
|
uranus: "Uranus",
|
|
neptune: "Neptune",
|
|
pluto: "Pluto"
|
|
};
|
|
|
|
function buildRequestHeaders() {
|
|
const apiKey = getApiKey();
|
|
return apiKey
|
|
? {
|
|
"x-api-key": apiKey
|
|
}
|
|
: undefined;
|
|
}
|
|
|
|
async function fetchJson(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})`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
function buildObjectPath(target, pathParts, value) {
|
|
let cursor = target;
|
|
for (let index = 0; index < pathParts.length - 1; index += 1) {
|
|
const part = pathParts[index];
|
|
if (!cursor[part] || typeof cursor[part] !== "object") {
|
|
cursor[part] = {};
|
|
}
|
|
cursor = cursor[part];
|
|
}
|
|
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;
|
|
textLibraryCache = null;
|
|
deckManifestCache.clear();
|
|
quizTemplatesCache.clear();
|
|
textSourceCache.clear();
|
|
textSectionCache.clear();
|
|
textLexiconCache.clear();
|
|
textLexiconOccurrencesCache.clear();
|
|
textSearchCache.clear();
|
|
}
|
|
|
|
function normalizeTarotName(value) {
|
|
return String(value || "")
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/\s+/g, " ");
|
|
}
|
|
|
|
function resolveTarotTrumpNumber(cardName) {
|
|
const key = normalizeTarotName(cardName);
|
|
if (!key) {
|
|
return null;
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, key)) {
|
|
return TAROT_TRUMP_NUMBER_BY_NAME[key];
|
|
}
|
|
|
|
const withoutLeadingThe = key.replace(/^the\s+/, "");
|
|
if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, withoutLeadingThe)) {
|
|
return TAROT_TRUMP_NUMBER_BY_NAME[withoutLeadingThe];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function enrichAssociation(associations) {
|
|
if (!associations || typeof associations !== "object") {
|
|
return associations;
|
|
}
|
|
|
|
const next = { ...associations };
|
|
|
|
if (next.tarotCard) {
|
|
const trumpNumber = resolveTarotTrumpNumber(next.tarotCard);
|
|
if (trumpNumber != null) {
|
|
if (!Number.isFinite(Number(next.tarotTrumpNumber))) {
|
|
next.tarotTrumpNumber = trumpNumber;
|
|
}
|
|
|
|
const hebrew = HEBREW_BY_TRUMP_NUMBER[trumpNumber];
|
|
if (hebrew) {
|
|
if (!next.hebrewLetterId) {
|
|
next.hebrewLetterId = hebrew.hebrewLetterId;
|
|
}
|
|
if (!Number.isFinite(Number(next.kabbalahPathNumber))) {
|
|
next.kabbalahPathNumber = hebrew.kabbalahPathNumber;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const planetId = String(next.planetId || "").trim().toLowerCase();
|
|
if (!next.iChingPlanetaryInfluence && planetId) {
|
|
const influence = ICHING_PLANET_BY_PLANET_ID[planetId];
|
|
if (influence) {
|
|
next.iChingPlanetaryInfluence = influence;
|
|
}
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
function enrichCalendarMonth(month) {
|
|
const events = Array.isArray(month?.events)
|
|
? month.events.map((event) => ({
|
|
...event,
|
|
associations: enrichAssociation(event?.associations)
|
|
}))
|
|
: [];
|
|
|
|
return {
|
|
...month,
|
|
associations: enrichAssociation(month?.associations),
|
|
events
|
|
};
|
|
}
|
|
|
|
function enrichCelestialHoliday(holiday) {
|
|
return {
|
|
...holiday,
|
|
associations: enrichAssociation(holiday?.associations)
|
|
};
|
|
}
|
|
|
|
function enrichCalendarHoliday(holiday) {
|
|
return {
|
|
...holiday,
|
|
associations: enrichAssociation(holiday?.associations)
|
|
};
|
|
}
|
|
|
|
async function loadMagickManifest() {
|
|
if (magickManifestCache) {
|
|
return magickManifestCache;
|
|
}
|
|
|
|
magickManifestCache = await fetchJson(buildApiUrl("/api/v1/bootstrap/magick-manifest"));
|
|
return magickManifestCache;
|
|
}
|
|
|
|
async function loadMagickDataset() {
|
|
if (magickDataCache) {
|
|
return magickDataCache;
|
|
}
|
|
|
|
magickDataCache = await fetchJson(buildApiUrl("/api/v1/bootstrap/magick-dataset"));
|
|
return magickDataCache;
|
|
}
|
|
|
|
async function loadReferenceData() {
|
|
return fetchJson(buildApiUrl("/api/v1/bootstrap/reference-data"));
|
|
}
|
|
|
|
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
|
|
}));
|
|
}
|
|
|
|
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
|
|
}));
|
|
}
|
|
|
|
async function loadTarotCards(filters = {}) {
|
|
return fetchJson(buildApiUrl("/api/v1/tarot/cards", {
|
|
q: filters?.query,
|
|
arcana: filters?.arcana,
|
|
suit: filters?.suit
|
|
}));
|
|
}
|
|
|
|
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
|
|
}));
|
|
}
|
|
|
|
async function loadGematriaWordsByValue(value) {
|
|
return fetchJson(buildApiUrl("/api/v1/gematria/words", {
|
|
value
|
|
}));
|
|
}
|
|
|
|
async function loadWordAnagrams(text) {
|
|
return fetchJson(buildApiUrl("/api/v1/words/anagrams", {
|
|
text
|
|
}));
|
|
}
|
|
|
|
async function loadWordsByPrefix(prefix) {
|
|
return fetchJson(buildApiUrl("/api/v1/words/prefix", {
|
|
prefix
|
|
}));
|
|
}
|
|
|
|
async function loadTextLibrary(forceRefresh = false) {
|
|
if (!forceRefresh && textLibraryCache) {
|
|
return textLibraryCache;
|
|
}
|
|
|
|
textLibraryCache = await fetchJson(buildApiUrl("/api/v1/texts"));
|
|
return textLibraryCache;
|
|
}
|
|
|
|
async function loadTextSource(sourceId, forceRefresh = false) {
|
|
const normalizedSourceId = String(sourceId || "").trim().toLowerCase();
|
|
if (!normalizedSourceId) {
|
|
return null;
|
|
}
|
|
|
|
if (!forceRefresh && textSourceCache.has(normalizedSourceId)) {
|
|
return textSourceCache.get(normalizedSourceId);
|
|
}
|
|
|
|
const payload = await fetchJson(buildApiUrl(`/api/v1/texts/${encodeURIComponent(normalizedSourceId)}`));
|
|
textSourceCache.set(normalizedSourceId, payload);
|
|
return payload;
|
|
}
|
|
|
|
async function loadTextSection(sourceId, workId, sectionId, forceRefresh = false) {
|
|
const normalizedSourceId = String(sourceId || "").trim().toLowerCase();
|
|
const normalizedWorkId = String(workId || "").trim().toLowerCase();
|
|
const normalizedSectionId = String(sectionId || "").trim().toLowerCase();
|
|
if (!normalizedSourceId || !normalizedWorkId || !normalizedSectionId) {
|
|
return null;
|
|
}
|
|
|
|
const cacheKey = `${normalizedSourceId}::${normalizedWorkId}::${normalizedSectionId}`;
|
|
if (!forceRefresh && textSectionCache.has(cacheKey)) {
|
|
return textSectionCache.get(cacheKey);
|
|
}
|
|
|
|
const payload = await fetchJson(buildApiUrl(
|
|
`/api/v1/texts/${encodeURIComponent(normalizedSourceId)}/works/${encodeURIComponent(normalizedWorkId)}/sections/${encodeURIComponent(normalizedSectionId)}`
|
|
));
|
|
textSectionCache.set(cacheKey, payload);
|
|
return payload;
|
|
}
|
|
|
|
async function loadTextLexiconEntry(lexiconId, entryId, forceRefresh = false) {
|
|
const normalizedLexiconId = String(lexiconId || "").trim().toLowerCase();
|
|
const normalizedEntryId = String(entryId || "").trim().toUpperCase();
|
|
if (!normalizedLexiconId || !normalizedEntryId) {
|
|
return null;
|
|
}
|
|
|
|
const cacheKey = `${normalizedLexiconId}::${normalizedEntryId}`;
|
|
if (!forceRefresh && textLexiconCache.has(cacheKey)) {
|
|
return textLexiconCache.get(cacheKey);
|
|
}
|
|
|
|
const payload = await fetchJson(buildApiUrl(
|
|
`/api/v1/texts/lexicons/${encodeURIComponent(normalizedLexiconId)}/entries/${encodeURIComponent(normalizedEntryId)}`
|
|
));
|
|
textLexiconCache.set(cacheKey, payload);
|
|
return payload;
|
|
}
|
|
|
|
async function loadTextLexiconOccurrences(lexiconId, entryId, options = {}, forceRefresh = false) {
|
|
const normalizedLexiconId = String(lexiconId || "").trim().toLowerCase();
|
|
const normalizedEntryId = String(entryId || "").trim().toUpperCase();
|
|
const normalizedLimit = Number.parseInt(options?.limit, 10);
|
|
const limit = Number.isFinite(normalizedLimit) ? normalizedLimit : 100;
|
|
if (!normalizedLexiconId || !normalizedEntryId) {
|
|
return null;
|
|
}
|
|
|
|
const cacheKey = `${normalizedLexiconId}::${normalizedEntryId}::${limit}`;
|
|
if (!forceRefresh && textLexiconOccurrencesCache.has(cacheKey)) {
|
|
return textLexiconOccurrencesCache.get(cacheKey);
|
|
}
|
|
|
|
const payload = await fetchJson(buildApiUrl(
|
|
`/api/v1/texts/lexicons/${encodeURIComponent(normalizedLexiconId)}/entries/${encodeURIComponent(normalizedEntryId)}/occurrences`,
|
|
{ limit }
|
|
));
|
|
textLexiconOccurrencesCache.set(cacheKey, payload);
|
|
return payload;
|
|
}
|
|
|
|
async function searchTextLibrary(query, options = {}, forceRefresh = false) {
|
|
const normalizedQuery = String(query || "").trim();
|
|
const normalizedSourceId = String(options?.sourceId || "").trim().toLowerCase();
|
|
const normalizedLimit = Number.parseInt(options?.limit, 10);
|
|
const limit = Number.isFinite(normalizedLimit) ? normalizedLimit : 50;
|
|
|
|
if (!normalizedQuery) {
|
|
return {
|
|
query: "",
|
|
normalizedQuery: "",
|
|
scope: normalizedSourceId ? { type: "source", sourceId: normalizedSourceId } : { type: "global" },
|
|
limit,
|
|
totalMatches: 0,
|
|
resultCount: 0,
|
|
truncated: false,
|
|
results: []
|
|
};
|
|
}
|
|
|
|
const cacheKey = `${normalizedSourceId || "global"}::${limit}::${normalizedQuery.toLowerCase()}`;
|
|
if (!forceRefresh && textSearchCache.has(cacheKey)) {
|
|
return textSearchCache.get(cacheKey);
|
|
}
|
|
|
|
const path = normalizedSourceId
|
|
? `/api/v1/texts/${encodeURIComponent(normalizedSourceId)}/search`
|
|
: "/api/v1/texts/search";
|
|
|
|
const payload = await fetchJson(buildApiUrl(path, {
|
|
q: normalizedQuery,
|
|
limit
|
|
}));
|
|
textSearchCache.set(cacheKey, payload);
|
|
return payload;
|
|
}
|
|
|
|
async function loadDeckOptions(forceRefresh = false) {
|
|
if (!forceRefresh && deckOptionsCache) {
|
|
return deckOptionsCache;
|
|
}
|
|
|
|
deckOptionsCache = await fetchJson(buildApiUrl("/api/v1/decks/options"));
|
|
return deckOptionsCache;
|
|
}
|
|
|
|
async function loadDeckManifest(deckId, forceRefresh = false) {
|
|
const normalizedDeckId = String(deckId || "").trim().toLowerCase();
|
|
if (!normalizedDeckId) {
|
|
return null;
|
|
}
|
|
|
|
if (!forceRefresh && deckManifestCache.has(normalizedDeckId)) {
|
|
return deckManifestCache.get(normalizedDeckId);
|
|
}
|
|
|
|
const manifest = await fetchJson(buildApiUrl(`/api/v1/decks/${encodeURIComponent(normalizedDeckId)}/manifest`));
|
|
deckManifestCache.set(normalizedDeckId, manifest);
|
|
return manifest;
|
|
}
|
|
|
|
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,
|
|
loadGematriaWordsByValue,
|
|
loadWordAnagrams,
|
|
loadWordsByPrefix,
|
|
loadQuizCategories,
|
|
loadQuizTemplates,
|
|
loadTarotCards,
|
|
loadReferenceData,
|
|
loadMagickManifest,
|
|
loadMagickDataset,
|
|
loadTextLibrary,
|
|
loadTextSource,
|
|
searchTextLibrary,
|
|
loadTextSection,
|
|
loadTextLexiconEntry,
|
|
loadTextLexiconOccurrences,
|
|
probeConnection,
|
|
pullQuizQuestion,
|
|
pullTarotSpread,
|
|
toApiAssetUrl
|
|
};
|
|
|
|
document.addEventListener("connection:updated", resetCaches);
|
|
})();
|