refactoring
This commit is contained in:
453
app/ui-settings.js
Normal file
453
app/ui-settings.js
Normal file
@@ -0,0 +1,453 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const SETTINGS_STORAGE_KEY = "tarot-time-settings-v1";
|
||||
|
||||
let config = {
|
||||
defaultSettings: {
|
||||
latitude: 51.5074,
|
||||
longitude: -0.1278,
|
||||
timeFormat: "minutes",
|
||||
birthDate: "",
|
||||
tarotDeck: "ceremonial-magick"
|
||||
},
|
||||
onSettingsApplied: null,
|
||||
onSyncSkyBackground: null,
|
||||
onStatus: null,
|
||||
onReopenActiveSection: null,
|
||||
getActiveSection: null,
|
||||
onRenderWeek: null
|
||||
};
|
||||
|
||||
function getElements() {
|
||||
return {
|
||||
openSettingsEl: document.getElementById("open-settings"),
|
||||
closeSettingsEl: document.getElementById("close-settings"),
|
||||
settingsPopupEl: document.getElementById("settings-popup"),
|
||||
settingsPopupCardEl: document.getElementById("settings-popup-card"),
|
||||
latEl: document.getElementById("lat"),
|
||||
lngEl: document.getElementById("lng"),
|
||||
timeFormatEl: document.getElementById("time-format"),
|
||||
birthDateEl: document.getElementById("birth-date"),
|
||||
tarotDeckEl: document.getElementById("tarot-deck"),
|
||||
saveSettingsEl: document.getElementById("save-settings"),
|
||||
useLocationEl: document.getElementById("use-location")
|
||||
};
|
||||
}
|
||||
|
||||
function setStatus(text) {
|
||||
if (typeof config.onStatus === "function") {
|
||||
config.onStatus(text);
|
||||
}
|
||||
}
|
||||
|
||||
function applyExternalSettings(settings) {
|
||||
if (typeof config.onSettingsApplied === "function") {
|
||||
config.onSettingsApplied(settings);
|
||||
}
|
||||
}
|
||||
|
||||
function syncSky(geo, force) {
|
||||
if (typeof config.onSyncSkyBackground === "function") {
|
||||
config.onSyncSkyBackground(geo, force);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTimeFormat(value) {
|
||||
if (value === "hours") {
|
||||
return "hours";
|
||||
}
|
||||
|
||||
if (value === "seconds") {
|
||||
return "seconds";
|
||||
}
|
||||
|
||||
return "minutes";
|
||||
}
|
||||
|
||||
function normalizeBirthDate(value) {
|
||||
const normalized = String(value || "").trim();
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : "";
|
||||
}
|
||||
|
||||
function getKnownTarotDeckIds() {
|
||||
const knownDeckIds = new Set();
|
||||
const deckOptions = window.TarotCardImages?.getDeckOptions?.();
|
||||
|
||||
if (Array.isArray(deckOptions)) {
|
||||
deckOptions.forEach((option) => {
|
||||
const id = String(option?.id || "").trim().toLowerCase();
|
||||
if (id) {
|
||||
knownDeckIds.add(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!knownDeckIds.size) {
|
||||
knownDeckIds.add(String(config.defaultSettings?.tarotDeck || "ceremonial-magick").trim().toLowerCase());
|
||||
}
|
||||
|
||||
return knownDeckIds;
|
||||
}
|
||||
|
||||
function getFallbackTarotDeckId() {
|
||||
const deckOptions = window.TarotCardImages?.getDeckOptions?.();
|
||||
if (Array.isArray(deckOptions)) {
|
||||
for (let i = 0; i < deckOptions.length; i += 1) {
|
||||
const id = String(deckOptions[i]?.id || "").trim().toLowerCase();
|
||||
if (id) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return String(config.defaultSettings?.tarotDeck || "ceremonial-magick").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeTarotDeck(value) {
|
||||
const normalized = String(value || "").trim().toLowerCase();
|
||||
const knownDeckIds = getKnownTarotDeckIds();
|
||||
|
||||
if (knownDeckIds.has(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return getFallbackTarotDeckId();
|
||||
}
|
||||
|
||||
function parseStoredNumber(value, fallback) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
function normalizeSettings(settings) {
|
||||
return {
|
||||
latitude: parseStoredNumber(settings?.latitude, config.defaultSettings.latitude),
|
||||
longitude: parseStoredNumber(settings?.longitude, config.defaultSettings.longitude),
|
||||
timeFormat: normalizeTimeFormat(settings?.timeFormat),
|
||||
birthDate: normalizeBirthDate(settings?.birthDate),
|
||||
tarotDeck: normalizeTarotDeck(settings?.tarotDeck)
|
||||
};
|
||||
}
|
||||
|
||||
function getResolvedTimeZone() {
|
||||
try {
|
||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
return String(timeZone || "");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function buildBirthDateParts(birthDate) {
|
||||
const normalized = normalizeBirthDate(birthDate);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [year, month, day] = normalized.split("-").map((value) => Number(value));
|
||||
if (!year || !month || !day) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const localNoon = new Date(year, month - 1, day, 12, 0, 0, 0);
|
||||
const utcNoon = new Date(Date.UTC(year, month - 1, day, 12, 0, 0, 0));
|
||||
|
||||
return {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
isoDate: normalized,
|
||||
localNoonIso: localNoon.toISOString(),
|
||||
utcNoonIso: utcNoon.toISOString(),
|
||||
timezoneOffsetMinutesAtNoon: localNoon.getTimezoneOffset()
|
||||
};
|
||||
}
|
||||
|
||||
function buildNatalContext(settings) {
|
||||
const normalized = normalizeSettings(settings);
|
||||
const birthDateParts = buildBirthDateParts(normalized.birthDate);
|
||||
const timeZone = getResolvedTimeZone();
|
||||
|
||||
return {
|
||||
latitude: normalized.latitude,
|
||||
longitude: normalized.longitude,
|
||||
birthDate: normalized.birthDate || null,
|
||||
birthDateParts,
|
||||
timeZone: timeZone || "UTC",
|
||||
timezoneOffsetMinutesNow: new Date().getTimezoneOffset(),
|
||||
timezoneOffsetMinutesAtBirthDateNoon: birthDateParts?.timezoneOffsetMinutesAtNoon ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function emitSettingsUpdated(settings) {
|
||||
const normalized = normalizeSettings(settings);
|
||||
const natalContext = buildNatalContext(normalized);
|
||||
document.dispatchEvent(new CustomEvent("settings:updated", {
|
||||
detail: {
|
||||
settings: normalized,
|
||||
natalContext
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function loadSavedSettings() {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(SETTINGS_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return { ...config.defaultSettings };
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
return normalizeSettings(parsed);
|
||||
} catch {
|
||||
return { ...config.defaultSettings };
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings(settings) {
|
||||
try {
|
||||
const normalized = normalizeSettings(settings);
|
||||
window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(normalized));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function syncTarotDeckInputOptions() {
|
||||
const { tarotDeckEl } = getElements();
|
||||
if (!tarotDeckEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deckOptions = window.TarotCardImages?.getDeckOptions?.();
|
||||
const previousValue = String(tarotDeckEl.value || "").trim().toLowerCase();
|
||||
tarotDeckEl.innerHTML = "";
|
||||
|
||||
if (!Array.isArray(deckOptions) || !deckOptions.length) {
|
||||
const emptyOption = document.createElement("option");
|
||||
emptyOption.value = String(config.defaultSettings?.tarotDeck || "ceremonial-magick").trim().toLowerCase();
|
||||
emptyOption.textContent = "No deck manifests found";
|
||||
tarotDeckEl.appendChild(emptyOption);
|
||||
tarotDeckEl.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
tarotDeckEl.disabled = false;
|
||||
|
||||
deckOptions.forEach((option) => {
|
||||
const id = String(option?.id || "").trim().toLowerCase();
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = String(option?.label || id);
|
||||
const optionEl = document.createElement("option");
|
||||
optionEl.value = id;
|
||||
optionEl.textContent = label;
|
||||
tarotDeckEl.appendChild(optionEl);
|
||||
});
|
||||
|
||||
tarotDeckEl.value = normalizeTarotDeck(previousValue);
|
||||
}
|
||||
|
||||
function applySettingsToInputs(settings) {
|
||||
const { latEl, lngEl, timeFormatEl, birthDateEl, tarotDeckEl } = getElements();
|
||||
syncTarotDeckInputOptions();
|
||||
const normalized = normalizeSettings(settings);
|
||||
latEl.value = String(normalized.latitude);
|
||||
lngEl.value = String(normalized.longitude);
|
||||
timeFormatEl.value = normalized.timeFormat;
|
||||
birthDateEl.value = normalized.birthDate;
|
||||
if (tarotDeckEl) {
|
||||
tarotDeckEl.value = normalized.tarotDeck;
|
||||
}
|
||||
if (window.TarotCardImages?.setActiveDeck) {
|
||||
window.TarotCardImages.setActiveDeck(normalized.tarotDeck);
|
||||
}
|
||||
applyExternalSettings(normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getSettingsFromInputs() {
|
||||
const { latEl, lngEl, timeFormatEl, birthDateEl, tarotDeckEl } = getElements();
|
||||
const latitude = Number(latEl.value);
|
||||
const longitude = Number(lngEl.value);
|
||||
|
||||
if (Number.isNaN(latitude) || Number.isNaN(longitude)) {
|
||||
throw new Error("Latitude/Longitude must be valid numbers.");
|
||||
}
|
||||
|
||||
return normalizeSettings({
|
||||
latitude,
|
||||
longitude,
|
||||
timeFormat: normalizeTimeFormat(timeFormatEl.value),
|
||||
birthDate: normalizeBirthDate(birthDateEl.value),
|
||||
tarotDeck: normalizeTarotDeck(tarotDeckEl?.value)
|
||||
});
|
||||
}
|
||||
|
||||
function openSettingsPopup() {
|
||||
const { settingsPopupEl, openSettingsEl } = getElements();
|
||||
if (!settingsPopupEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
settingsPopupEl.hidden = false;
|
||||
if (openSettingsEl) {
|
||||
openSettingsEl.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
}
|
||||
|
||||
function closeSettingsPopup() {
|
||||
const { settingsPopupEl, openSettingsEl } = getElements();
|
||||
if (!settingsPopupEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
settingsPopupEl.hidden = true;
|
||||
if (openSettingsEl) {
|
||||
openSettingsEl.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveSettings() {
|
||||
try {
|
||||
const settings = getSettingsFromInputs();
|
||||
const normalized = applySettingsToInputs(settings);
|
||||
syncSky({ latitude: normalized.latitude, longitude: normalized.longitude }, true);
|
||||
const didPersist = saveSettings(normalized);
|
||||
emitSettingsUpdated(normalized);
|
||||
if (typeof config.getActiveSection === "function" && config.getActiveSection() !== "home") {
|
||||
config.onReopenActiveSection?.(config.getActiveSection());
|
||||
}
|
||||
closeSettingsPopup();
|
||||
if (typeof config.onRenderWeek === "function") {
|
||||
await config.onRenderWeek();
|
||||
}
|
||||
|
||||
if (!didPersist) {
|
||||
setStatus("Settings applied for this session. Browser storage is unavailable.");
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(error?.message || "Unable to save settings.");
|
||||
}
|
||||
}
|
||||
|
||||
function requestGeoLocation() {
|
||||
const { latEl, lngEl } = getElements();
|
||||
if (!navigator.geolocation) {
|
||||
setStatus("Geolocation not available in this browser.");
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("Getting your location...");
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
({ coords }) => {
|
||||
latEl.value = coords.latitude.toFixed(4);
|
||||
lngEl.value = coords.longitude.toFixed(4);
|
||||
syncSky({ latitude: coords.latitude, longitude: coords.longitude }, true);
|
||||
setStatus("Location set from browser. Click Save Settings to refresh.");
|
||||
},
|
||||
(err) => {
|
||||
const detail = err?.message || `code ${err?.code ?? "unknown"}`;
|
||||
setStatus(`Could not get location (${detail}).`);
|
||||
},
|
||||
{ enableHighAccuracy: true, timeout: 10000 }
|
||||
);
|
||||
}
|
||||
|
||||
function bindInteractions() {
|
||||
const {
|
||||
saveSettingsEl,
|
||||
useLocationEl,
|
||||
openSettingsEl,
|
||||
closeSettingsEl,
|
||||
settingsPopupEl,
|
||||
settingsPopupCardEl
|
||||
} = getElements();
|
||||
|
||||
if (saveSettingsEl) {
|
||||
saveSettingsEl.addEventListener("click", () => {
|
||||
void handleSaveSettings();
|
||||
});
|
||||
}
|
||||
|
||||
if (useLocationEl) {
|
||||
useLocationEl.addEventListener("click", requestGeoLocation);
|
||||
}
|
||||
|
||||
if (openSettingsEl) {
|
||||
openSettingsEl.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
if (settingsPopupEl?.hidden) {
|
||||
openSettingsPopup();
|
||||
} else {
|
||||
closeSettingsPopup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (closeSettingsEl) {
|
||||
closeSettingsEl.addEventListener("click", closeSettingsPopup);
|
||||
}
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
const clickTarget = event.target;
|
||||
if (!settingsPopupEl || settingsPopupEl.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(clickTarget instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settingsPopupCardEl?.contains(clickTarget) || openSettingsEl?.contains(clickTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeSettingsPopup();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
closeSettingsPopup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init(nextConfig = {}) {
|
||||
config = {
|
||||
...config,
|
||||
...nextConfig,
|
||||
defaultSettings: {
|
||||
...config.defaultSettings,
|
||||
...(nextConfig.defaultSettings || {})
|
||||
}
|
||||
};
|
||||
|
||||
bindInteractions();
|
||||
}
|
||||
|
||||
function loadInitialSettingsAndApply() {
|
||||
const initialSettings = loadSavedSettings();
|
||||
const normalized = applySettingsToInputs(initialSettings);
|
||||
emitSettingsUpdated(normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
window.TarotSettingsUi = {
|
||||
...(window.TarotSettingsUi || {}),
|
||||
init,
|
||||
openSettingsPopup,
|
||||
closeSettingsPopup,
|
||||
loadInitialSettingsAndApply,
|
||||
buildNatalContext,
|
||||
normalizeSettings
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user