From 549fc73296d5a87229cb66991e38b8f2c089d042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Randall=20Havens=20=E2=96=B3=20The=20Empathic=20Tec?= =?UTF-8?q?hnologist=20=E2=9F=81=20Doctor=20Who=2042?= Date: Sat, 8 Nov 2025 19:32:19 -0600 Subject: [PATCH] Update app.js --- public/app.js | 175 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 3 deletions(-) diff --git a/public/app.js b/public/app.js index d9854c4..5aaa285 100755 --- a/public/app.js +++ b/public/app.js @@ -1,3 +1,140 @@ +const els = { + menuBtn: document.getElementById("menuBtn"), + primaryNav: document.getElementById("primaryNav"), + sectionSelect: document.getElementById("sectionSelect"), + tagSelect: document.getElementById("tagSelect"), + sortSelect: document.getElementById("sortSelect"), + searchMode: document.getElementById("searchMode"), + searchBox: document.getElementById("searchBox"), + postList: document.getElementById("postList"), + viewer: document.getElementById("viewer"), + content: document.getElementById("content"), + toggleControls: document.getElementById("toggleControls"), + filterPanel: document.getElementById("filterPanel") +}; + +let indexData = null; +let sidebarOpen = false; + +async function init() { + try { + indexData = await (await fetch("index.json")).json(); + populateNav(); + populateSections(); + populateTags(); + wireUI(); + renderList(); + handleHash(); + window.addEventListener("hashchange", handleHash); + } catch (e) { + els.viewer.innerHTML = "

Error

Failed to load site data.

"; + } +} + +// TOP NAV: Only folder name if ANY index.* exists +function populateNav() { + els.primaryNav.innerHTML = 'Home'; + const navSections = [...new Set( + indexData.flat + .filter(f => f.isIndex) + .map(f => f.path.split("/")[0]) + )].sort(); + navSections.forEach(s => { + els.primaryNav.innerHTML += `${s.charAt(0).toUpperCase() + s.slice(1)}`; + }); +} + +// DROPDOWN: Only sections with NON-index files +function populateSections() { + els.sectionSelect.innerHTML = ''; + indexData.sections.forEach(s => { + const opt = document.createElement("option"); + opt.value = s; opt.textContent = s; + els.sectionSelect.appendChild(opt); + }); +} + +function populateTags() { + indexData.tags.forEach(t => { + const opt = document.createElement("option"); + opt.value = t; opt.textContent = t; + els.tagSelect.appendChild(opt); + }); +} + +function formatTimestamp(ms) { + const d = new Date(ms); + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; +} + +function wireUI() { + els.menuBtn.addEventListener("click", () => { + sidebarOpen = !sidebarOpen; + document.body.classList.toggle("sidebar-open", sidebarOpen); + }); + + els.toggleControls.addEventListener("click", () => { + const open = els.filterPanel.open; + els.filterPanel.open = !open; + els.toggleControls.textContent = open ? "Filters" : "Hide"; + }); + + els.sectionSelect.addEventListener("change", () => { + renderList(); + if (els.sectionSelect.value !== "all") loadDefaultForSection(els.sectionSelect.value); + }); + + [els.tagSelect, els.sortSelect, els.searchMode].forEach(el => el.addEventListener("change", renderList)); + els.searchBox.addEventListener("input", renderList); + + els.content.addEventListener("click", (e) => { + if (window.innerWidth < 1024 && document.body.classList.contains("sidebar-open")) { + if (!e.target.closest("#sidebar")) { + document.body.classList.remove("sidebar-open"); + sidebarOpen = false; + } + } + }); +} + +function renderList() { + const section = els.sectionSelect.value; + const tags = Array.from(els.tagSelect.selectedOptions).map(o => o.value.toLowerCase()); + const sort = els.sortSelect.value; + const mode = els.searchMode.value; + const query = els.searchBox.value.toLowerCase(); + + let posts = indexData.flat.filter(p => !p.isIndex); + if (section !== "all") posts = posts.filter(p => p.path.split('/')[0] === section); + if (tags.length) posts = posts.filter(p => tags.every(t => p.tags.includes(t))); + if (query) { + posts = posts.filter(p => { + const text = mode === "content" ? p.title + " " + p.excerpt : p.title; + return text.toLowerCase().includes(query); + }); + } + posts.sort((a, b) => sort === "newest" ? b.mtime - a.mtime : a.mtime - b.mtime); + + els.postList.innerHTML = posts.length ? "" : "
  • No posts found.
  • "; + posts.forEach(p => { + const li = document.createElement("li"); + const pin = p.isPinned ? "Star " : ""; + const time = formatTimestamp(p.ctime); + li.innerHTML = `${pin}${p.title}${time}`; + els.postList.appendChild(li); + }); +} + +function loadDefaultForSection(section) { + const posts = indexData.flat.filter(p => p.path.split('/')[0] === section && !p.isIndex); + if (!posts.length) { + els.viewer.innerHTML = `

    ${section}

    No content yet.

    `; + return; + } + const pinned = posts.find(p => p.isPinned) || posts.sort((a,b) => b.mtime - a.mtime)[0]; + location.hash = `#/${pinned.path}`; +} + async function handleHash() { els.viewer.innerHTML = ""; const rel = location.hash.replace(/^#\//, ""); @@ -20,11 +157,9 @@ async function handleHash() { renderIframe(indexFile.path); } } catch (e) { - // If fetch fails, show clean message els.viewer.innerHTML = `

    ${section}

    No content yet.

    `; } } else { - // No index file → show posts els.sectionSelect.value = section; renderList(); loadDefaultForSection(section); @@ -37,4 +172,38 @@ async function handleHash() { } file.ext === ".md" ? await renderMarkdown(file.path) : renderIframe(file.path); } -} \ No newline at end of file +} + +async function renderMarkdown(rel) { + const src = await fetch(rel).then(r => r.ok ? r.text() : ""); + els.viewer.innerHTML = `
    ${marked.parse(src || "# Untitled")}
    `; +} + +function renderIframe(rel) { + const iframe = document.createElement("iframe"); + iframe.src = "/" + rel; + iframe.loading = "eager"; + iframe.setAttribute("sandbox", "allow-same-origin allow-scripts allow-forms"); + els.viewer.appendChild(iframe); + + iframe.onload = () => { + try { + const doc = iframe.contentDocument; + const style = doc.createElement("style"); + style.textContent = ` + html,body{background:#0b0b0b;color:#e6e3d7;font-family:Inter,sans-serif;margin:0;padding:2rem;} + *{max-width:720px;margin:auto;} + img, video, iframe {max-width:100%;height:auto;} + `; + doc.head.appendChild(style); + } catch (e) {} + }; +} + +function renderDefault() { + const latest = indexData.flat.filter(f => !f.isIndex).sort((a,b) => b.mtime - a.mtime)[0]; + if (latest) location.hash = `#/${latest.path}`; + else els.viewer.innerHTML = "

    Welcome

    Add content to begin.

    "; +} + +init(); \ No newline at end of file