77 lines
2.3 KiB
JavaScript
77 lines
2.3 KiB
JavaScript
const API = "";
|
|
|
|
const THEME_KEY = "pts_theme";
|
|
|
|
function resolveTheme(pref) {
|
|
if (pref === "light" || pref === "dark") return pref;
|
|
// system
|
|
return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
|
}
|
|
|
|
function getThemePref() {
|
|
return localStorage.getItem(THEME_KEY) || "system";
|
|
}
|
|
|
|
function applyTheme(pref) {
|
|
const resolved = resolveTheme(pref);
|
|
document.documentElement.setAttribute("data-theme", resolved);
|
|
document.documentElement.dataset.themePref = pref;
|
|
document.documentElement.classList.toggle("dark", resolved === "dark");
|
|
document.querySelectorAll("[data-theme-option]").forEach((btn) => {
|
|
btn.classList.toggle("active", btn.getAttribute("data-theme-option") === pref);
|
|
});
|
|
}
|
|
|
|
function setThemePref(pref) {
|
|
localStorage.setItem(THEME_KEY, pref);
|
|
applyTheme(pref);
|
|
}
|
|
|
|
function initTheme() {
|
|
applyTheme(getThemePref());
|
|
// react to OS changes when using system
|
|
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => {
|
|
if (getThemePref() === "system") applyTheme("system");
|
|
});
|
|
document.querySelectorAll("[data-theme-option]").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
setThemePref(btn.getAttribute("data-theme-option"));
|
|
});
|
|
});
|
|
}
|
|
|
|
async function api(path, options = {}) {
|
|
const res = await fetch(`${API}${path}`, {
|
|
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
|
|
...options,
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
const detail = data.detail;
|
|
const message =
|
|
typeof detail === "string"
|
|
? detail
|
|
: Array.isArray(detail)
|
|
? detail.map((d) => d.msg || JSON.stringify(d)).join("; ")
|
|
: res.statusText;
|
|
throw new Error(message || res.statusText);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function todayStr() {
|
|
return new Date().toISOString().slice(0, 10);
|
|
}
|
|
|
|
function pill(label, value, ok) {
|
|
const cls = ok === true ? "ok" : ok === false ? "warn" : "";
|
|
return `<span class="pill ${cls}"><span class="pill-label">${label}</span><span class="pill-value">${value}</span></span>`;
|
|
}
|
|
|
|
// apply ASAP if script loads after head bootstrap
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initTheme);
|
|
} else {
|
|
initTheme();
|
|
}
|