authoring service now gets localstorage
All checks were successful
Build Org Website / build (push) Successful in 47s

This commit is contained in:
2026-05-09 20:05:32 +01:00
parent 901ee02929
commit 063350b4bc
3 changed files with 137 additions and 17 deletions

View File

@@ -860,6 +860,7 @@ APP_HTML = r"""<!doctype html>
.tree-add:hover { opacity: 1; background: rgba(184, 145, 69, 0.18); }
.page-item { width: 100%; text-align: left; min-height: auto; padding: 10px 11px; background: transparent; border-color: transparent; }
.page-item.active { border-color: rgba(214, 181, 102, 0.65); background: rgba(243, 234, 215, 0.13); box-shadow: inset 3px 0 0 var(--accent), 0 12px 24px rgba(0, 0, 0, 0.18); }
.page-item.unsaved { border-color: rgba(203, 130, 75, 0.78); }
.page-item strong { display: block; font-size: 14px; margin-bottom: 3px; overflow-wrap: anywhere; font-weight: 780; color: #f3e7ce; }
.page-item span { display: block; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
.page-item .filename { color: #cab68d; }
@@ -939,6 +940,7 @@ APP_HTML = r"""<!doctype html>
.preview-panel img, .preview-panel video { max-width: 100%; height: auto; border-radius: 6px; border: 1px solid rgba(107, 91, 57, 0.24); }
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.hint { color: #b3a181; font-size: 13px; overflow-wrap: anywhere; }
.dirty-hint { color: #e0b06f; }
.full { grid-column: 1 / -1; }
.tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 5px; }
.tag { background: rgba(68, 106, 87, 0.25); color: #d5eadb; border: 1px solid rgba(104, 151, 121, 0.36); padding: 2px 7px; border-radius: 999px; font-size: 12px; }
@@ -1037,7 +1039,7 @@ APP_HTML = r"""<!doctype html>
<label><span><input name="comments" type="checkbox" checked style="width:auto" /> Comments enabled</span></label>
<div class="actions">
<button class="primary" id="saveBtn" type="submit">Save and build</button>
<button id="resetBtn" type="button">Reset</button>
<button id="discardBtn" type="button">Discard changes</button>
<span id="saveMessage" class="hint"></span>
</div>
</form>
@@ -1048,7 +1050,8 @@ APP_HTML = r"""<!doctype html>
</main>
</div>
<script>
const state = { pages: [], currentPath: "", build: null, pathManual: false, treeOpen: {} };
const draftStorageKey = "orgAuthoringDrafts:v1";
const state = { pages: [], currentPath: "", build: null, pathManual: false, treeOpen: {}, drafts: {}, loadingForm: false };
const $ = (selector) => document.querySelector(selector);
const editor = $("#editor");
const mdToolbar = $("#mdToolbar");
@@ -1075,6 +1078,88 @@ APP_HTML = r"""<!doctype html>
return `<${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${days[d.getDay()]} ${pad(d.getHours())}:${pad(d.getMinutes())}>`;
}
function loadDrafts() {
try {
return JSON.parse(localStorage.getItem(draftStorageKey) || "{}");
} catch (_err) {
return {};
}
}
function saveDrafts() {
try {
localStorage.setItem(draftStorageKey, JSON.stringify(state.drafts));
} catch (_err) {
saveMessage.textContent = "Browser storage is full; draft could not be saved.";
}
}
function draftKey(path = undefined) {
if (path !== undefined) return path || "__new__";
return state.currentPath || editor.targetPath.value || "__new__";
}
function hasDraft(path = undefined) {
return Object.prototype.hasOwnProperty.call(state.drafts, draftKey(path));
}
function editorSnapshot() {
return {
pageType: editor.pageType.value,
section: editor.section.value,
targetPath: editor.targetPath.value,
title: editor.title.value,
slug: editor.slug.value,
tags: editor.tags.value,
date: editor.date.value,
content: editor.content.value,
comments: editor.comments.checked,
pathManual: state.pathManual,
};
}
function applyDraft(page, draft) {
return {
...page,
pageType: draft.pageType,
section: draft.section,
path: page.path || draft.targetPath || "",
title: draft.title,
slug: draft.slug,
tags: typeof draft.tags === "string" ? draft.tags.split(",").map((tag) => tag.trim()).filter(Boolean) : page.tags,
date: draft.date,
content: draft.content,
comments: draft.comments,
targetPath: draft.targetPath,
pathManual: draft.pathManual,
};
}
function rememberDraft() {
if (state.loadingForm) return;
const key = draftKey();
state.drafts[key] = { ...editorSnapshot(), updatedAt: Date.now() };
saveDrafts();
renderDraftState("Unsaved changes.");
if (state.pages.length) renderPages();
}
function clearDraft(path = undefined) {
const key = draftKey(path);
if (!hasDraft(key)) return;
delete state.drafts[key];
saveDrafts();
renderDraftState("");
if (state.pages.length) renderPages();
}
function renderDraftState(message = null) {
const dirty = hasDraft();
saveMessage.classList.toggle("dirty-hint", dirty);
if (message !== null) saveMessage.textContent = message;
if (!message && dirty) saveMessage.textContent = "Unsaved changes.";
}
async function api(path, options = {}) {
const res = await fetch(path, { headers: { "Content-Type": "application/json" }, ...options });
let data = null;
@@ -1195,8 +1280,9 @@ APP_HTML = r"""<!doctype html>
function renderPageButton(page) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = `page-item${page.path === state.currentPath ? " active" : ""}`;
btn.innerHTML = `<strong>${html(page.title)}</strong><span class="filename">${html(page.filename || page.path)}</span><span>${html(page.path)}</span>${page.tags.length ? `<span class="tags">${page.tags.map((tag) => `<span class="tag">${html(tag)}</span>`).join("")}</span>` : ""}`;
btn.className = `page-item${page.path === state.currentPath ? " active" : ""}${hasDraft(page.path) ? " unsaved" : ""}`;
const draftLabel = hasDraft(page.path) ? `<span class="dirty-hint">Unsaved changes</span>` : "";
btn.innerHTML = `<strong>${html(page.title)}</strong><span class="filename">${html(page.filename || page.path)}</span><span>${html(page.path)}</span>${draftLabel}${page.tags.length ? `<span class="tags">${page.tags.map((tag) => `<span class="tag">${html(tag)}</span>`).join("")}</span>` : ""}`;
btn.onclick = () => loadPage(page.path);
return btn;
}
@@ -1323,12 +1409,16 @@ APP_HTML = r"""<!doctype html>
markdownPreview.innerHTML = markdownToHtml(editor.content.value);
}
function setForm(page) {
function setForm(page, options = {}) {
const draft = options.useDraft === false ? null : state.drafts[draftKey(page.path || page.targetPath || "")];
const restoredDraft = Boolean(draft);
if (draft) page = applyDraft(page, draft);
state.loadingForm = true;
state.currentPath = page.path || "";
state.pathManual = Boolean(page.path);
state.pathManual = Object.prototype.hasOwnProperty.call(page, "pathManual") ? Boolean(page.pathManual) : Boolean(page.path);
editor.pageType.value = page.pageType || "blog";
editor.section.value = "";
editor.targetPath.value = page.path || "";
editor.section.value = page.section || "";
editor.targetPath.value = page.targetPath || page.path || "";
editor.title.value = page.title || "";
editor.slug.value = page.slug || slugify(page.title || "");
editor.tags.value = (page.tags || []).join(", ");
@@ -1341,13 +1431,33 @@ APP_HTML = r"""<!doctype html>
updateEditorMode();
updateSuggestedPath();
updateMarkdownPreview();
state.loadingForm = false;
if (state.pages.length) renderPages();
saveMessage.textContent = "";
renderDraftState(restoredDraft ? "Unsaved changes restored." : "");
}
async function loadPage(path) {
async function loadPage(path, options = {}) {
const page = await api(`/api/page?path=${encodeURIComponent(path)}`);
setForm(page);
setForm(page, options);
}
async function discardChanges() {
const path = state.currentPath;
const key = draftKey();
if (!state.drafts[key]) {
saveMessage.textContent = "No unsaved changes to discard.";
return;
}
if (!confirm("Discard unsaved changes for this file?")) return;
delete state.drafts[key];
saveDrafts();
if (path) {
await loadPage(path, { useDraft: false });
} else {
setForm({ pageType: "blog", date: currentOrgDate(), comments: true }, { useDraft: false });
}
saveMessage.textContent = "Unsaved changes discarded.";
renderPages();
}
async function refreshPages() {
@@ -1404,19 +1514,25 @@ APP_HTML = r"""<!doctype html>
$("#cancelPageLinkBtn").addEventListener("click", () => { pageLinkPicker.hidden = true; });
attachInput.addEventListener("change", uploadAttachment);
editor.content.addEventListener("input", updateMarkdownPreview);
editor.addEventListener("input", rememberDraft);
editor.addEventListener("change", rememberDraft);
$("#search").addEventListener("input", renderPages);
$("#typeFilter").addEventListener("change", renderPages);
$("#newBtn").addEventListener("click", () => setForm({ pageType: "blog", date: currentOrgDate(), comments: true }));
$("#resetBtn").addEventListener("click", () => state.currentPath ? loadPage(state.currentPath) : setForm({ pageType: "blog", date: currentOrgDate(), comments: true }));
$("#discardBtn").addEventListener("click", discardChanges);
editor.addEventListener("submit", async (event) => {
event.preventDefault();
saveMessage.textContent = "Saving and queueing build.";
const payload = Object.fromEntries(new FormData(editor).entries());
payload.path = state.currentPath;
payload.comments = editor.comments.checked;
const previousDraftKey = draftKey();
try {
const saved = await api("/api/page", { method: "POST", body: JSON.stringify(payload) });
setForm(saved);
delete state.drafts[previousDraftKey];
delete state.drafts[draftKey(saved.path)];
saveDrafts();
setForm(saved, { useDraft: false });
saveMessage.textContent = "Saved. Build queued.";
await refreshPages();
await refreshBuild();
@@ -1486,6 +1602,7 @@ APP_HTML = r"""<!doctype html>
area.setSelectionRange(start + selectStart, start + selectEnd);
}
updateMarkdownPreview();
rememberDraft();
}
function toggleLinePrefix(prefix, fallback, ordered = false) {
@@ -1508,6 +1625,7 @@ APP_HTML = r"""<!doctype html>
area.setSelectionRange(start, start + replacement.length);
area.focus();
updateMarkdownPreview();
rememberDraft();
}
function toggleWrap(prefix, suffix, fallback) {
@@ -1530,6 +1648,7 @@ APP_HTML = r"""<!doctype html>
}
area.focus();
updateMarkdownPreview();
rememberDraft();
}
function applyMarkdown(action) {
@@ -1649,6 +1768,7 @@ APP_HTML = r"""<!doctype html>
}
}
state.drafts = loadDrafts();
setForm({ pageType: "blog", date: currentOrgDate(), comments: true });
refreshPages();
refreshBuild();