authoring service now gets localstorage
All checks were successful
Build Org Website / build (push) Successful in 47s
All checks were successful
Build Org Website / build (push) Successful in 47s
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
||||
|
||||
* Posts:
|
||||
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">09-05-2026 19:59</span>@@
|
||||
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">09-05-2026 20:04</span>@@
|
||||
- [[file:posts-list.sync-conflict-20260509-195434-NE5VEIB.org][Posts List]] @@html:<span class="post-date">09-05-2026 19:54</span>@@
|
||||
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
- [[file:tags/notes.org][Tag: notes]]
|
||||
- [[file:tags/review.org][Tag: review]]
|
||||
- [[file:tags/website.org][Tag: website]]
|
||||
- [[file:tags/update.org][Tag: update]]
|
||||
- [[file:tags/life.org][Tag: life]]
|
||||
- [[file:tags/emacs.org][Tag: emacs]]
|
||||
- [[file:tags/education.org][Tag: education]]
|
||||
- [[file:tags/update.org][Tag: update]]
|
||||
- [[file:tags/insights.org][Tag: insights]]
|
||||
- [[file:tags/reading.org][Tag: reading]]
|
||||
- [[file:tags/emacs.org][Tag: emacs]]
|
||||
- [[file:tags/maths.org][Tag: maths]]
|
||||
- [[file:tags/reading.org][Tag: reading]]
|
||||
- posts
|
||||
- [[file:posts/posts-intro.org][Posts Introduction]]
|
||||
- [[file:posts/posts-list.sync-conflict-20260509-195434-NE5VEIB.org][Posts List]]
|
||||
|
||||
Reference in New Issue
Block a user