initial commit
This commit is contained in:
@@ -0,0 +1,641 @@
|
||||
(() => {
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
|
||||
const TABS = [
|
||||
{ id: "trips", label: "Trips" },
|
||||
{ id: "shopping", label: "Shopping" },
|
||||
{ id: "todo", label: "Pre-trip Do" },
|
||||
{ id: "check", label: "Pre-trip Check" },
|
||||
{ id: "upgrades", label: "Upgrades" },
|
||||
{ id: "fixes", label: "Fixes" },
|
||||
];
|
||||
|
||||
const state = {
|
||||
activeTab: "trips",
|
||||
data: null,
|
||||
dirty: false,
|
||||
saveTimer: null,
|
||||
};
|
||||
|
||||
const uid = () => Math.random().toString(16).slice(2) + Date.now().toString(16);
|
||||
|
||||
const mapsLink = (address) =>
|
||||
`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address || "")}`;
|
||||
|
||||
function setStatus(msg) {
|
||||
const el = $("#status");
|
||||
if (el) el.textContent = msg;
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function safeUrl(u) {
|
||||
const s = String(u ?? "").trim();
|
||||
if (!s) return "";
|
||||
if (/^https?:\/\//i.test(s)) return s;
|
||||
return "";
|
||||
}
|
||||
|
||||
function panel(title, subtitle, innerHTML) {
|
||||
return `
|
||||
<div class="panel">
|
||||
<h2>${escapeHtml(title)}</h2>
|
||||
<div class="sub">${escapeHtml(subtitle || "")}</div>
|
||||
<hr />
|
||||
${innerHTML}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function apiGet() {
|
||||
const res = await fetch("/api/data_get.php", { credentials: "include" });
|
||||
if (!res.ok) throw new Error(`GET failed: ${res.status}`);
|
||||
const j = await res.json();
|
||||
if (!j.ok) throw new Error(j.error || "GET error");
|
||||
return j.data;
|
||||
}
|
||||
|
||||
async function apiPut(payload) {
|
||||
const res = await fetch("/api/data_put.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error(`PUT failed: ${res.status}`);
|
||||
const j = await res.json();
|
||||
if (!j.ok) throw new Error(j.error || "PUT error");
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalize(d) {
|
||||
return {
|
||||
meta: { schema: 1, updatedAt: new Date().toISOString() },
|
||||
trips: Array.isArray(d?.trips) ? d.trips : [],
|
||||
shopping: Array.isArray(d?.shopping) ? d.shopping : [],
|
||||
preTripTodos: Array.isArray(d?.preTripTodos) ? d.preTripTodos : [],
|
||||
preTripChecklist: Array.isArray(d?.preTripChecklist) ? d.preTripChecklist : [],
|
||||
upgrades: Array.isArray(d?.upgrades) ? d.upgrades : [],
|
||||
fixes: Array.isArray(d?.fixes) ? d.fixes : [],
|
||||
};
|
||||
}
|
||||
|
||||
function markDirty() {
|
||||
state.dirty = true;
|
||||
setStatus("Unsaved changes...");
|
||||
if (state.saveTimer) clearTimeout(state.saveTimer);
|
||||
state.saveTimer = setTimeout(saveNow, 650);
|
||||
}
|
||||
|
||||
async function saveNow() {
|
||||
if (!state.dirty) return;
|
||||
state.dirty = false;
|
||||
|
||||
try {
|
||||
const payload = (typeof structuredClone === "function")
|
||||
? structuredClone(state.data)
|
||||
: JSON.parse(JSON.stringify(state.data));
|
||||
|
||||
payload.meta = payload.meta || {};
|
||||
payload.meta.schema = 1;
|
||||
payload.meta.updatedAt = new Date().toISOString();
|
||||
|
||||
setStatus("Saving...");
|
||||
await apiPut(payload);
|
||||
setStatus("Saved.");
|
||||
} catch (e) {
|
||||
state.dirty = true;
|
||||
setStatus(`Save failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
const tabs = $("#tabs");
|
||||
if (!tabs) return;
|
||||
tabs.innerHTML = "";
|
||||
|
||||
for (const t of TABS) {
|
||||
const b = document.createElement("button");
|
||||
b.className = "tab" + (state.activeTab === t.id ? " active" : "");
|
||||
b.textContent = t.label;
|
||||
b.onclick = () => {
|
||||
state.activeTab = t.id;
|
||||
render();
|
||||
};
|
||||
tabs.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
function moneyLabel(v) {
|
||||
if (v === "" || v == null) return "";
|
||||
const n = Number(v);
|
||||
if (Number.isNaN(n)) return String(v);
|
||||
return n.toLocaleString(undefined, { style: "currency", currency: "USD" });
|
||||
}
|
||||
|
||||
// ---------- Trips ----------
|
||||
function renderTrips() {
|
||||
const trips = state.data.trips || [];
|
||||
const sorted = [...trips].sort((a, b) => (b.startDate || "").localeCompare(a.startDate || ""));
|
||||
|
||||
const form = `
|
||||
<div class="panel">
|
||||
<h2>Upcoming Trips</h2>
|
||||
<div class="sub">Name, contact, dates, cost, paid status, venue link, and a one-click Maps link.</div>
|
||||
<hr />
|
||||
|
||||
<div class="grid2">
|
||||
<label><span>Name</span><input id="trip_name" placeholder="Campground / Park name"></label>
|
||||
<label><span>Phone</span><input id="trip_phone" placeholder="(###) ###-####"></label>
|
||||
<label><span>Address</span><input id="trip_address" placeholder="Street, City, State"></label>
|
||||
<label><span>Venue website</span><input id="trip_website" placeholder="https://..."></label>
|
||||
<label><span>Start date</span><input id="trip_start" type="date"></label>
|
||||
<label><span>End date</span><input id="trip_end" type="date"></label>
|
||||
<label><span>Cost</span><input id="trip_cost" placeholder="e.g. 245.00"></label>
|
||||
<label style="display:flex;gap:10px;align-items:center;margin-top:26px">
|
||||
<input id="trip_paid" type="checkbox" style="width:auto">
|
||||
<span class="muted">Paid?</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="actions" style="margin-top:12px">
|
||||
<button class="btn primary" id="trip_add">Add Trip</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const list = sorted.length
|
||||
? `<div class="items">${
|
||||
sorted
|
||||
.map((t) => {
|
||||
const badge = t.paid
|
||||
? `<span class="badge good">Paid</span>`
|
||||
: `<span class="badge warn">Unpaid</span>`;
|
||||
|
||||
const pills = [
|
||||
t.cost !== "" ? `<span class="pill">${escapeHtml(moneyLabel(t.cost))}</span>` : "",
|
||||
t.phone ? `<span class="pill">${escapeHtml(t.phone)}</span>` : "",
|
||||
(t.startDate || t.endDate)
|
||||
? `<span class="pill">${escapeHtml((t.startDate || "") + (t.endDate ? " \u2192 " + t.endDate : ""))}</span>`
|
||||
: "",
|
||||
].filter(Boolean).join("");
|
||||
|
||||
const addr = t.address ? `<div class="muted small" style="margin-top:6px">${escapeHtml(t.address)}</div>` : "";
|
||||
|
||||
const mapsBtn = t.address
|
||||
? `<a class="btn" href="${mapsLink(t.address)}" target="_blank" rel="noopener noreferrer">Maps</a>`
|
||||
: `<button class="btn" disabled>Maps</button>`;
|
||||
|
||||
const venue = safeUrl(t.venueWebsite);
|
||||
const venueBtn = venue
|
||||
? `<a class="btn" href="${venue}" target="_blank" rel="noopener noreferrer">Venue</a>`
|
||||
: `<button class="btn" disabled>Venue</button>`;
|
||||
|
||||
return `
|
||||
<div class="item">
|
||||
<div class="item-left">
|
||||
<div>
|
||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||||
<div class="item-title">${escapeHtml(t.name)}</div>
|
||||
${badge}
|
||||
</div>
|
||||
${pills ? `<div class="pills">${pills}</div>` : ""}
|
||||
${addr}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
${mapsBtn}
|
||||
${venueBtn}
|
||||
<button class="btn" data-action="trip_toggle_paid" data-id="${t.id}">Toggle Paid</button>
|
||||
<button class="btn danger" data-action="trip_remove" data-id="${t.id}">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("")
|
||||
}</div>`
|
||||
: `<div class="panel"><div class="muted">No trips yet.</div></div>`;
|
||||
|
||||
return form + list;
|
||||
}
|
||||
|
||||
function bindTrips() {
|
||||
$("#trip_add")?.addEventListener("click", () => {
|
||||
const name = $("#trip_name").value.trim();
|
||||
if (!name) return;
|
||||
|
||||
const trip = {
|
||||
id: uid(),
|
||||
createdAt: new Date().toISOString(),
|
||||
name,
|
||||
address: $("#trip_address").value.trim(),
|
||||
phone: $("#trip_phone").value.trim(),
|
||||
startDate: $("#trip_start").value,
|
||||
endDate: $("#trip_end").value,
|
||||
cost: $("#trip_cost").value.trim(),
|
||||
paid: $("#trip_paid").checked,
|
||||
venueWebsite: $("#trip_website").value.trim(),
|
||||
};
|
||||
|
||||
state.data.trips = [trip, ...(state.data.trips || [])];
|
||||
markDirty();
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Generic Checklist ----------
|
||||
function renderChecklist(opts) {
|
||||
const items = opts.items || [];
|
||||
|
||||
const addForm = `
|
||||
<div class="grid2">
|
||||
<label><span>Item</span><input id="${opts.id}_label" placeholder="${escapeHtml(opts.placeholder || "")}"></label>
|
||||
${opts.allowStore ? `<label><span>Store</span><input id="${opts.id}_store" placeholder="Walmart / HEB / Amazon"></label>` : `<div></div>`}
|
||||
<label style="grid-column:1 / -1"><span>Notes (optional)</span><textarea id="${opts.id}_notes"></textarea></label>
|
||||
${opts.extraForm ? opts.extraForm() : ""}
|
||||
</div>
|
||||
<div class="actions" style="margin-top:12px">
|
||||
<button class="btn primary" id="${opts.id}_add">Add</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const list = items.length
|
||||
? `<div class="items">${
|
||||
items
|
||||
.map((x) => {
|
||||
// NOTE: no "Notes" pill; notes render as plain text below.
|
||||
const pills = [
|
||||
x.store ? `<span class="pill">Store: ${escapeHtml(x.store)}</span>` : "",
|
||||
x.partsNeeded ? `<span class="pill">Parts: ${escapeHtml(x.partsNeeded)}</span>` : "",
|
||||
typeof x.purchased === "boolean"
|
||||
? `<span class="pill">${x.purchased ? "Parts purchased" : "Parts NOT purchased"}</span>`
|
||||
: "",
|
||||
].filter(Boolean).join("");
|
||||
|
||||
return `
|
||||
<div class="item">
|
||||
<div class="item-left">
|
||||
<input type="checkbox" data-action="${opts.id}_toggle" data-id="${x.id}" ${x.done ? "checked" : ""} style="margin-top:3px">
|
||||
<div>
|
||||
<div class="item-title ${x.done ? "done" : ""}">${escapeHtml(x.label || "")}</div>
|
||||
${pills ? `<div class="pills">${pills}</div>` : ""}
|
||||
${x.notes ? `<div class="muted small" style="margin-top:6px">${escapeHtml(x.notes)}</div>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" data-action="${opts.id}_edit" data-id="${x.id}">Edit</button>
|
||||
<button class="btn danger" data-action="${opts.id}_remove" data-id="${x.id}">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("")
|
||||
}</div>`
|
||||
: `<div class="muted">No items yet.</div>`;
|
||||
|
||||
return panel(opts.title, opts.subtitle, addForm + "<hr />" + list);
|
||||
}
|
||||
|
||||
function bindChecklist(opts) {
|
||||
$(`#${opts.id}_add`)?.addEventListener("click", () => {
|
||||
const label = $(`#${opts.id}_label`).value.trim();
|
||||
if (!label) return;
|
||||
|
||||
const item = { id: uid(), label, done: false };
|
||||
if (opts.allowStore) item.store = $(`#${opts.id}_store`).value.trim();
|
||||
const notes = $(`#${opts.id}_notes`).value.trim();
|
||||
if (notes) item.notes = notes;
|
||||
|
||||
if (opts.extraRead) Object.assign(item, opts.extraRead());
|
||||
|
||||
const arr = state.data[opts.key] || [];
|
||||
state.data[opts.key] = [item, ...arr];
|
||||
|
||||
markDirty();
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
function renderFixes() {
|
||||
return renderChecklist({
|
||||
id: "fixes",
|
||||
key: "fixes",
|
||||
title: "Things to Fix",
|
||||
subtitle: "Track issues, parts required, and whether parts are already purchased.",
|
||||
placeholder: "E.g., Replace anode rod",
|
||||
items: state.data.fixes || [],
|
||||
extraForm: () => `
|
||||
<label><span>Parts needed</span><input id="fixes_parts" placeholder="Part name / SKU / link"></label>
|
||||
<label style="display:flex;gap:10px;align-items:center;margin-top:26px">
|
||||
<input id="fixes_purchased" type="checkbox" style="width:auto">
|
||||
<span class="muted">Parts purchased?</span>
|
||||
</label>
|
||||
`,
|
||||
extraRead: () => ({
|
||||
partsNeeded: $("#fixes_parts").value.trim(),
|
||||
purchased: $("#fixes_purchased").checked,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function renderShopping() {
|
||||
return renderChecklist({
|
||||
id: "shopping",
|
||||
key: "shopping",
|
||||
title: "Shopping List",
|
||||
subtitle: "Check items off while you're grabbing them off the shelf or buying.",
|
||||
placeholder: "E.g., 30A to 15A adapter",
|
||||
allowStore: true,
|
||||
items: state.data.shopping || [],
|
||||
});
|
||||
}
|
||||
|
||||
function renderTodo() {
|
||||
return renderChecklist({
|
||||
id: "todo",
|
||||
key: "preTripTodos",
|
||||
title: "Pre-Trip Do List",
|
||||
subtitle: "Tasks that can change depending on the trip.",
|
||||
placeholder: "E.g., Fill fresh water tank",
|
||||
items: state.data.preTripTodos || [],
|
||||
});
|
||||
}
|
||||
|
||||
function renderCheck() {
|
||||
return renderChecklist({
|
||||
id: "check",
|
||||
key: "preTripChecklist",
|
||||
title: "Pre-Trip Checklist",
|
||||
subtitle: "Repeatable checkboxes for go-time.",
|
||||
placeholder: "E.g., Hitch locked & pin installed",
|
||||
items: state.data.preTripChecklist || [],
|
||||
});
|
||||
}
|
||||
|
||||
function renderUpgrades() {
|
||||
return renderChecklist({
|
||||
id: "upgrades",
|
||||
key: "upgrades",
|
||||
title: "Upgrades",
|
||||
subtitle: "Wishlist and planned improvements for the trailer.",
|
||||
placeholder: "E.g., Add A/C soft start",
|
||||
items: state.data.upgrades || [],
|
||||
});
|
||||
}
|
||||
|
||||
function bindDelegates() {
|
||||
const content = $("#content");
|
||||
if (!content) return;
|
||||
|
||||
content.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-action]");
|
||||
if (!btn) return;
|
||||
|
||||
const action = btn.dataset.action;
|
||||
const id = btn.dataset.id;
|
||||
|
||||
// Trips
|
||||
if (action === "trip_remove") {
|
||||
state.data.trips = (state.data.trips || []).filter((t) => t.id !== id);
|
||||
markDirty();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if (action === "trip_toggle_paid") {
|
||||
state.data.trips = (state.data.trips || []).map((t) => (t.id === id ? { ...t, paid: !t.paid } : t));
|
||||
markDirty();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
// Removes for lists
|
||||
const removeMap = {
|
||||
shopping_remove: "shopping",
|
||||
todo_remove: "preTripTodos",
|
||||
check_remove: "preTripChecklist",
|
||||
upgrades_remove: "upgrades",
|
||||
fixes_remove: "fixes",
|
||||
};
|
||||
|
||||
if (removeMap[action]) {
|
||||
const key = removeMap[action];
|
||||
state.data[key] = (state.data[key] || []).filter((x) => x.id !== id);
|
||||
markDirty();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
// Edits for lists (prompt-based)
|
||||
const editMap = {
|
||||
shopping_edit: "shopping",
|
||||
todo_edit: "preTripTodos",
|
||||
check_edit: "preTripChecklist",
|
||||
upgrades_edit: "upgrades",
|
||||
fixes_edit: "fixes",
|
||||
};
|
||||
|
||||
if (editMap[action]) {
|
||||
const key = editMap[action];
|
||||
const list = state.data[key] || [];
|
||||
const item = list.find((z) => z.id === id);
|
||||
if (!item) return;
|
||||
|
||||
const newLabel = prompt("Edit item", item.label ?? "");
|
||||
if (newLabel === null) return;
|
||||
|
||||
const newNotes = prompt("Edit notes (optional)", item.notes ?? "");
|
||||
if (newNotes === null) return;
|
||||
|
||||
let newStore = item.store ?? "";
|
||||
if (key === "shopping") {
|
||||
const v = prompt("Edit store (optional)", item.store ?? "");
|
||||
if (v === null) return;
|
||||
newStore = v;
|
||||
}
|
||||
|
||||
let newParts = item.partsNeeded ?? "";
|
||||
let newPurchased = !!item.purchased;
|
||||
if (key === "fixes") {
|
||||
const p = prompt("Edit parts needed (optional)", item.partsNeeded ?? "");
|
||||
if (p === null) return;
|
||||
newParts = p;
|
||||
|
||||
const bought = prompt("Parts purchased? (yes/no)", item.purchased ? "yes" : "no");
|
||||
if (bought === null) return;
|
||||
newPurchased = /^y(es)?$/i.test((bought || "").trim());
|
||||
}
|
||||
|
||||
state.data[key] = list.map((z) => {
|
||||
if (z.id !== id) return z;
|
||||
const updated = { ...z, label: String(newLabel).trim() };
|
||||
|
||||
const nn = String(newNotes ?? "").trim();
|
||||
if (nn) updated.notes = nn;
|
||||
else delete updated.notes;
|
||||
|
||||
if (key === "shopping") {
|
||||
const st = String(newStore ?? "").trim();
|
||||
if (st) updated.store = st;
|
||||
else delete updated.store;
|
||||
}
|
||||
|
||||
if (key === "fixes") {
|
||||
const pn = String(newParts ?? "").trim();
|
||||
if (pn) updated.partsNeeded = pn;
|
||||
else delete updated.partsNeeded;
|
||||
updated.purchased = !!newPurchased;
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
markDirty();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Checkbox toggles (lists)
|
||||
content.addEventListener("change", (e) => {
|
||||
const el = e.target;
|
||||
if (!el.matches("[data-action]")) return;
|
||||
|
||||
const action = el.dataset.action;
|
||||
const id = el.dataset.id;
|
||||
|
||||
const toggleMap = {
|
||||
shopping_toggle: "shopping",
|
||||
todo_toggle: "preTripTodos",
|
||||
check_toggle: "preTripChecklist",
|
||||
upgrades_toggle: "upgrades",
|
||||
fixes_toggle: "fixes",
|
||||
};
|
||||
|
||||
if (toggleMap[action]) {
|
||||
const key = toggleMap[action];
|
||||
state.data[key] = (state.data[key] || []).map((x) => (x.id === id ? { ...x, done: !!el.checked } : x));
|
||||
markDirty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderTabs();
|
||||
|
||||
const c = $("#content");
|
||||
if (!c) return;
|
||||
|
||||
if (!state.data) {
|
||||
c.innerHTML = panel("Loading...", "", `<div class="muted">Please wait.</div>`);
|
||||
return;
|
||||
}
|
||||
|
||||
let htmlOut = "";
|
||||
switch (state.activeTab) {
|
||||
case "trips":
|
||||
htmlOut = renderTrips();
|
||||
break;
|
||||
case "shopping":
|
||||
htmlOut = renderShopping();
|
||||
break;
|
||||
case "todo":
|
||||
htmlOut = renderTodo();
|
||||
break;
|
||||
case "check":
|
||||
htmlOut = renderCheck();
|
||||
break;
|
||||
case "upgrades":
|
||||
htmlOut = renderUpgrades();
|
||||
break;
|
||||
case "fixes":
|
||||
htmlOut = renderFixes();
|
||||
break;
|
||||
}
|
||||
|
||||
c.innerHTML = htmlOut;
|
||||
|
||||
if (state.activeTab === "trips") bindTrips();
|
||||
if (state.activeTab === "shopping") bindChecklist({ id: "shopping", key: "shopping", allowStore: true });
|
||||
if (state.activeTab === "todo") bindChecklist({ id: "todo", key: "preTripTodos" });
|
||||
if (state.activeTab === "check") bindChecklist({ id: "check", key: "preTripChecklist" });
|
||||
if (state.activeTab === "upgrades") bindChecklist({ id: "upgrades", key: "upgrades" });
|
||||
if (state.activeTab === "fixes") {
|
||||
bindChecklist({
|
||||
id: "fixes",
|
||||
key: "fixes",
|
||||
extraRead: () => ({
|
||||
partsNeeded: $("#fixes_parts").value.trim(),
|
||||
purchased: $("#fixes_purchased").checked,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bindTopbar() {
|
||||
$("#btnLogout")?.addEventListener("click", () => {
|
||||
window.location.href = "/api/logout.php";
|
||||
});
|
||||
|
||||
$("#btnExport")?.addEventListener("click", () => {
|
||||
const blob = new Blob([JSON.stringify(state.data, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `trailer-trekker-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
$("#fileImport")?.addEventListener("change", async (e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (!f) return;
|
||||
try {
|
||||
const text = await f.text();
|
||||
const parsed = JSON.parse(text);
|
||||
state.data = normalize(parsed);
|
||||
markDirty();
|
||||
render();
|
||||
setStatus("Imported. Saving...");
|
||||
await saveNow();
|
||||
} catch (err) {
|
||||
setStatus("Import failed: " + err.message);
|
||||
} finally {
|
||||
e.target.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
if (state.dirty) {
|
||||
e.preventDefault();
|
||||
e.returnValue = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
setStatus("Loading from server...");
|
||||
const data = await apiGet();
|
||||
state.data = normalize(data);
|
||||
setStatus("Ready.");
|
||||
} catch (e) {
|
||||
state.data = normalize({});
|
||||
setStatus(`Server load failed: ${e.message} (using empty data)`);
|
||||
}
|
||||
|
||||
render();
|
||||
bindTopbar();
|
||||
bindDelegates();
|
||||
}
|
||||
|
||||
init();
|
||||
})();
|
||||
Reference in New Issue
Block a user