diff --git a/app/styles.css b/app/styles.css index e19579d..b8b9058 100644 --- a/app/styles.css +++ b/app/styles.css @@ -525,7 +525,47 @@ margin-top: 18px; padding-top: 16px; border-top: 1px solid rgba(63, 63, 70, 0.7); - justify-content: flex-end; + justify-content: space-between; + align-items: center; + gap: 16px; + flex-wrap: wrap; + } + .settings-page-status { + min-width: min(100%, 360px); + padding: 10px 12px; + border-radius: 14px; + border: 1px solid rgba(63, 63, 70, 0.92); + background: rgba(9, 9, 11, 0.78); + display: flex; + flex-direction: column; + gap: 4px; + box-sizing: border-box; + } + .settings-page-status[data-tone="info"] { + border-color: rgba(59, 130, 246, 0.55); + background: rgba(30, 64, 175, 0.14); + } + .settings-page-status[data-tone="success"] { + border-color: rgba(34, 197, 94, 0.5); + background: rgba(20, 83, 45, 0.18); + } + .settings-page-status[data-tone="warning"] { + border-color: rgba(245, 158, 11, 0.55); + background: rgba(120, 53, 15, 0.18); + } + .settings-page-status[data-tone="error"] { + border-color: rgba(239, 68, 68, 0.55); + background: rgba(127, 29, 29, 0.2); + } + .settings-page-status-text { + color: #f8fafc; + font-size: 13px; + line-height: 1.45; + } + .settings-page-status-time { + color: #94a3b8; + font-size: 12px; + line-height: 1.4; } .calendar-year-control { display: flex; diff --git a/app/ui-settings.js b/app/ui-settings.js index f076a15..b5692e9 100644 --- a/app/ui-settings.js +++ b/app/ui-settings.js @@ -2,6 +2,7 @@ "use strict"; const SETTINGS_STORAGE_KEY = "tarot-time-settings-v1"; + const SETTINGS_LAST_SAVED_AT_STORAGE_KEY = "tarot-time-settings-last-saved-at-v1"; let config = { defaultSettings: { @@ -39,11 +40,89 @@ stellariumBackgroundHintEl: document.getElementById("stellarium-background-hint"), apiBaseUrlEl: document.getElementById("api-base-url"), apiKeyEl: document.getElementById("api-key"), + settingsPageStatusEl: document.getElementById("settings-page-status"), + settingsPageStatusTextEl: document.getElementById("settings-page-status-text"), + settingsPageStatusTimeEl: document.getElementById("settings-page-status-time"), saveSettingsEl: document.getElementById("save-settings"), useLocationEl: document.getElementById("use-location") }; } + function loadLastSavedAt() { + try { + const raw = window.localStorage.getItem(SETTINGS_LAST_SAVED_AT_STORAGE_KEY); + const parsed = Number(raw); + return Number.isFinite(parsed) && parsed > 0 ? parsed : null; + } catch { + return null; + } + } + + function persistLastSavedAt(timestamp) { + try { + window.localStorage.setItem(SETTINGS_LAST_SAVED_AT_STORAGE_KEY, String(timestamp)); + return true; + } catch { + return false; + } + } + + function formatLastSavedAt(timestamp) { + if (!Number.isFinite(timestamp) || timestamp <= 0) { + return ""; + } + + try { + return new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "short" + }).format(new Date(timestamp)); + } catch { + return new Date(timestamp).toLocaleString(); + } + } + + function setSettingsPageStatus(message, tone = "neutral", options = {}) { + const { + settingsPageStatusEl, + settingsPageStatusTextEl, + settingsPageStatusTimeEl + } = getElements(); + + if (settingsPageStatusEl) { + settingsPageStatusEl.dataset.tone = String(tone || "neutral"); + } + + if (settingsPageStatusTextEl) { + settingsPageStatusTextEl.textContent = String(message || "Settings ready."); + } + + const savedAt = options.savedAt === undefined ? loadLastSavedAt() : options.savedAt; + const formattedSavedAt = formatLastSavedAt(savedAt); + if (settingsPageStatusTimeEl) { + settingsPageStatusTimeEl.hidden = !formattedSavedAt; + settingsPageStatusTimeEl.textContent = formattedSavedAt ? `Last saved ${formattedSavedAt}` : ""; + } + } + + function syncSavedSettingsStatus(message = "Settings ready.") { + setSettingsPageStatus(message, "neutral", { savedAt: loadLastSavedAt() }); + } + + function setSaveButtonBusy(isBusy) { + const { saveSettingsEl } = getElements(); + if (!saveSettingsEl) { + return; + } + + if (!saveSettingsEl.dataset.defaultLabel) { + saveSettingsEl.dataset.defaultLabel = String(saveSettingsEl.textContent || "Save Settings").trim() || "Save Settings"; + } + + saveSettingsEl.disabled = Boolean(isBusy); + saveSettingsEl.textContent = isBusy ? "Saving..." : saveSettingsEl.dataset.defaultLabel; + } + function setLocationEntryState(isExplicit) { const { latEl, lngEl } = getElements(); const normalizedValue = isExplicit ? "true" : "false"; @@ -466,6 +545,7 @@ lastNonSettingsSection = activeSection; } applySettingsToInputs(loadSavedSettings()); + syncSavedSettingsStatus(); config.setActiveSection?.("settings"); } @@ -474,6 +554,9 @@ } async function handleSaveSettings() { + setSaveButtonBusy(true); + setSettingsPageStatus("Saving settings...", "info"); + try { const settings = getSettingsFromInputs(); const previousConnectionSettings = getConnectionSettings(); @@ -501,22 +584,39 @@ } if (!didPersist || connectionResult.didPersist === false) { + setSettingsPageStatus("Settings applied for this session. Browser storage is unavailable.", "warning", { + savedAt: loadLastSavedAt() + }); setStatus("Settings applied for this session. Browser storage is unavailable."); } else { + const savedAt = Date.now(); + persistLastSavedAt(savedAt); + setSettingsPageStatus("Settings saved.", "success", { savedAt }); setStatus("Settings saved."); } } catch (error) { + setSettingsPageStatus(error?.message || "Unable to save settings.", "error", { + savedAt: loadLastSavedAt() + }); setStatus(error?.message || "Unable to save settings."); + } finally { + setSaveButtonBusy(false); } } function requestGeoLocation() { const { latEl, lngEl } = getElements(); if (!navigator.geolocation) { + setSettingsPageStatus("Geolocation not available in this browser.", "warning", { + savedAt: loadLastSavedAt() + }); setStatus("Geolocation not available in this browser."); return; } + setSettingsPageStatus("Getting your location...", "info", { + savedAt: loadLastSavedAt() + }); setStatus("Getting your location..."); navigator.geolocation.getCurrentPosition( ({ coords }) => { @@ -531,10 +631,16 @@ hasExplicitLocation: true } ); + setSettingsPageStatus("Location captured. Save Settings to keep it in this browser.", "info", { + savedAt: loadLastSavedAt() + }); setStatus("Location set from browser. Save Settings to use it across the app."); }, (err) => { const detail = err?.message || `code ${err?.code ?? "unknown"}`; + setSettingsPageStatus(`Could not get location (${detail}).`, "error", { + savedAt: loadLastSavedAt() + }); setStatus(`Could not get location (${detail}).`); }, { enableHighAccuracy: true, timeout: 10000 } @@ -612,6 +718,7 @@ } }; + syncSavedSettingsStatus(); bindInteractions(); } diff --git a/index.html b/index.html index 01e31f9..02c9628 100644 --- a/index.html +++ b/index.html @@ -174,6 +174,10 @@