thefoldwithin-earth/app.js
Mark Randall Havens 93d4f838b6 revamped
2025-10-18 13:03:53 -05:00

452 lines
17 KiB
JavaScript
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const SITE_URL = 'https://thefoldwithin.earth';
const state = {
posts: [],
bySlug: new Map(),
bySection: new Map(),
byTag: new Map(),
bySeries: new Map(),
byProgram: new Map(),
allTags: new Set(),
allSections: ['empathic-technologist', 'recursive-coherence', 'fold-within-earth', 'neutralizing-narcissism', 'simply-we', 'mirrormire'],
sectionTitles: {
'empathic-technologist': 'The Empathic Technologist',
'recursive-coherence': 'Recursive Coherence Theory',
'fold-within-earth': 'The Fold Within Earth',
'neutralizing-narcissism': 'Neutralizing Narcissism',
'simply-we': 'Simply WE',
'mirrormire': 'Mirrormire'
},
programTitles: {
'neutralizing-narcissism': 'Neutralizing Narcissism',
'open-source-justice': 'Open Source Justice',
'coparent': 'COPARENT'
},
pages: []
};
const $ = s => document.querySelector(s);
const $$ = s => document.querySelectorAll(s);
window.addEventListener('hashchange', () => {
$('nav ul').classList.remove('open');
$('.hamburger').setAttribute('aria-expanded', 'false');
router();
});
document.addEventListener('DOMContentLoaded', init);
async function init() {
try {
const res = await fetch('index.json');
if (!res.ok) throw new Error('Could not load index.');
state.posts = await res.json();
state.posts.sort((a, b) => b.date.localeCompare(a.date));
state.bySlug = new Map(state.posts.map(p => [p.slug, p]));
state.allSections.forEach(s => {
state.bySection.set(s, state.posts.filter(p => p.section === s));
});
state.posts.forEach(p => {
p.tags.forEach(t => {
state.allTags.add(t);
if (!state.byTag.has(t)) state.byTag.set(t, []);
state.byTag.get(t).push(p);
});
if (p.series) {
if (!state.bySeries.has(p.series)) state.bySeries.set(p.series, []);
state.bySeries.get(p.series).push(p);
}
p.programs.forEach(pr => {
if (!state.byProgram.has(pr)) state.byProgram.set(pr, []);
state.byProgram.get(pr).push(p);
});
});
const pagesRes = await fetch('pages.json');
if (pagesRes.ok) state.pages = await pagesRes.json();
renderNav();
renderFooter();
setupSearchForm();
setupHamburger();
router();
} catch (e) {
$('#main').innerHTML = `<p class="error">⚠️ ${e.message}</p>`;
}
}
function setupHamburger() {
const hamburger = $('.hamburger');
hamburger.addEventListener('click', () => {
const ul = $('nav ul');
const isOpen = ul.classList.toggle('open');
hamburger.setAttribute('aria-expanded', isOpen);
});
}
function renderNav() {
$('#sections-list').innerHTML = state.allSections.map(s => `<li><a href="#/section/${s}">${state.sectionTitles[s]}</a></li>`).join('');
const programsMenu = `
<li class="sections"><a href="#/programs">Programs</a>
<div class="mega">
<ul id="programs-list">
${Object.entries(state.programTitles).map(([k,v])=>`<li><a href="#/program/${k}">${v}</a></li>`).join('')}
</ul>
</div>
</li>
<li><a href="#/start">Start Here</a></li>
`;
document.querySelector('nav ul').insertAdjacentHTML('beforeend', programsMenu);
}
function renderFooter() {
const sectionLinks = state.allSections.map(s => `<li><a href="#/section/${s}">${state.sectionTitles[s]}</a></li>`).join('');
const tagLinks = Array.from(state.allTags).sort().map(t => `<a href="#/tag/${t}" class="pill tag">${t}</a>`).join('');
$('#footer').innerHTML = `
<div class="sections">
<h4>Sections</h4>
<ul>${sectionLinks}</ul>
</div>
<div class="tags">
<h4>Tags</h4>
<div class="tag-cloud">${tagLinks}</div>
</div>
<div class="contact">
<h4>Contact</h4>
<p>Email: info@thefoldwithin.earth</p>
</div>
<p>&copy; ${new Date().getFullYear()} The Fold Within Earth</p>
`;
}
function setupSearchForm() {
$('#search-form').addEventListener('submit', e => {
e.preventDefault();
const q = $('#search-input').value.trim();
if (q) {
sessionStorage.setItem('lastSearch', q);
location.hash = `/search?q=${encodeURIComponent(q)}`;
}
});
}
function router() {
const main = $('#main');
main.style.opacity = 0;
const {parts, params} = getQueryParams();
document.title = 'The Fold Within Earth';
if (parts.length === 0) {
renderHome();
} else if (parts[0] === 'section' && parts[1]) {
renderArchive('section', parts[1], params);
} else if (parts[0] === 'tag' && parts[1]) {
renderArchive('tag', parts[1], params);
} else if (parts[0] === 'post' && parts[1]) {
renderPost(parts[1]);
} else if (parts[0] === 'search') {
let q = params.get('q') || sessionStorage.getItem('lastSearch') || '';
$('#search-input').value = q;
renderSearch(q, params);
} else if (parts[0] === 'about') {
renderAbout();
} else if (parts[0] === 'mud') {
renderMud();
} else if (parts[0] === 'start') {
renderStart();
} else if (parts[0] === 'programs') {
renderProgramsHome();
} else if (parts[0] === 'program' && parts[1]) {
renderProgramArchive(parts[1], params);
} else {
render404();
}
setTimeout(() => {
main.style.opacity = 1;
window.scrollTo(0, 0);
main.focus();
}, 0);
}
function renderHome() {
const latestAll = state.posts.slice(0, 10).map(renderCard).join('');
const bySection = state.allSections.map(s => {
const secPosts = state.bySection.get(s) ? state.bySection.get(s).slice(0, 3) : [];
const list = secPosts.map(p => `<li><a href="#/post/${p.slug}">${escapeHTML(p.title)}</a> <span class="date">(${formatDate(p.date)})</span></li>`).join('');
return `<div class="sec-col">
<h3>${state.sectionTitles[s]}</h3>
<ul>${list}</ul>
<a href="#/section/${s}">More in ${state.sectionTitles[s]}...</a>
</div>`;
}).join('');
$('#main').innerHTML = `
<section class="latest-all">
<h2>Latest Across All Sections</h2>
<div class="grid">${latestAll}</div>
</section>
<section class="by-section">
<h2>Latest by Section</h2>
<div class="section-grid">${bySection}</div>
</section>
`;
addCardListeners();
}
function renderArchive(type, key, params) {
if (!key) return render404();
const sort = params.get('sort') || 'desc';
const page = parseInt(params.get('page') || '1', 10);
let activeTags = params.get('tags') ? params.get('tags').split(',') : [];
let postList = (type === 'section' ? state.bySection.get(key) : state.byTag.get(key)) || [];
const title = `${type.charAt(0).toUpperCase() + type.slice(1)}: ${type === 'section' ? state.sectionTitles[key] || key : key}`;
if (!postList.length) {
$('#main').innerHTML = `<p class="error">No posts found for ${escapeHTML(title)}.</p>`;
return;
}
let sorted = postList.slice().sort((a, b) => sort === 'desc' ? b.date.localeCompare(a.date) : a.date.localeCompare(b.date));
let filtered = activeTags.length ? sorted.filter(p => activeTags.some(t => p.tags.includes(t))) : sorted;
const perPage = 10;
const totalPages = Math.ceil(filtered.length / perPage);
const start = (page - 1) * perPage;
const end = start + perPage;
const cards = filtered.slice(start, end).map(renderCard).join('');
const availableTags = [...new Set(postList.flatMap(p => p.tags))];
$('#main').innerHTML = `
<section class="archive">
<div class="breadcrumbs">${renderBreadcrumbs([type, key])}</div>
<h1>${escapeHTML(title)}</h1>
${renderControls(sort, parts, params)}
${renderFilters(availableTags, activeTags, parts, params)}
<div class="grid">${cards}</div>
${renderPager(page, totalPages, parts, {sort, tags: activeTags.join(',')})}
</section>
`;
document.title = `${escapeHTML(title)} — The Fold Within Earth`;
$('#sort-select').addEventListener('change', e => {
updateHash(parts, {sort: e.target.value, page: 1, tags: activeTags.join(',')});
});
$$('.tag-cloud .pill').forEach(el => {
el.addEventListener('click', () => {
const t = el.dataset.tag;
activeTags = activeTags.includes(t) ? activeTags.filter(at => at !== t) : [...activeTags, t];
updateHash(parts, {sort, page: 1, tags: activeTags.join(',')});
});
});
$$('.pager button').forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.action;
const newPage = action === 'prev' ? page - 1 : page + 1;
updateHash(parts, {sort, page: newPage, tags: activeTags.join(',')});
});
});
addCardListeners();
}
async function renderPost(slug) {
if (!slug) return render404();
const post = state.bySlug.get(slug);
if (!post) {
$('#main').innerHTML = `<p class="error">⚠️ Post not found.</p>`;
return;
}
try {
const res = await fetch(`content/${post.file}`);
if (!res.ok) throw new Error('Post file missing.');
let md = await res.text();
md = md.replace(/^---[\s\S]*?---/, '').trim();
const html = sanitizeMarkdown(md);
const date = formatDate(post.date);
const sectionPill = renderPill('section', state.sectionTitles[post.section] || post.section);
const tagPills = post.tags.map(t => renderPill('tag', t)).join('');
const programPills = post.programs.map(pr => renderPill('program', state.programTitles[pr] || pr)).join('');
const reading = `<span class="reading">${post.readingTime} min read</span>`;
const authorHtml = post.author ? `<p class="author">By ${escapeHTML(post.author)}</p>` : '';
const share = `<div class="share"><a href="https://twitter.com/intent/tweet?url=${encodeURIComponent(location.href)}&text=${encodeURIComponent(post.title)}" target="_blank" rel="noopener noreferrer">Share on X</a></div>`;
const secPosts = state.bySection.get(post.section) || [];
const idx = secPosts.findIndex(p => p.slug === slug);
const prev = idx < secPosts.length - 1 ? secPosts[idx + 1] : null;
const next = idx > 0 ? secPosts[idx - 1] : null;
const navPost = `<div class="nav-post">${prev ? `<a href="#/post/${prev.slug}">← ${escapeHTML(prev.title)}</a>` : ''}${next ? `<a href="#/post/${next.slug}">${escapeHTML(next.title)} →</a>` : ''}</div>`;
let navSeries = '';
if (post.series) {
const seriesPosts = (state.bySeries.get(post.series) || []).sort((a, b) => a.date.localeCompare(b.date));
const sIdx = seriesPosts.findIndex(p => p.slug === slug);
const prevSeries = sIdx > 0 ? seriesPosts[sIdx - 1] : null;
const nextSeries = sIdx < seriesPosts.length - 1 ? seriesPosts[sIdx + 1] : null;
navSeries = `<div class="nav-series">${prevSeries ? `<a href="#/post/${prevSeries.slug}">← Previous in ${escapeHTML(post.series)}</a>` : ''}${nextSeries ? `<a href="#/post/${nextSeries.slug}">Next in ${escapeHTML(post.series)} →</a>` : ''}</div>`;
}
const related = state.posts.filter(p => p.slug !== slug && p.tags.some(t => post.tags.includes(t)))
.sort((a, b) => {
const sharedA = a.tags.filter(t => post.tags.includes(t)).length;
const sharedB = b.tags.filter(t => post.tags.includes(t)).length;
return sharedB - sharedA;
}).slice(0, 3);
const relatedHtml = related.length ? `<h2>Related Posts</h2><div class="grid">${related.map(renderCard).join('')}</div>` : '';
$('#main').innerHTML = `<section class="post">
<div class="breadcrumbs">${renderBreadcrumbs(['post', post.title])}</div>
<a href="#/" id="back">← Back to Home</a>
<div class="markdown">
<h1>${escapeHTML(post.title)}</h1>
<p class="date">${date}</p>
${authorHtml}
<div class="meta">${sectionPill} ${programPills} ${tagPills} ${reading}</div>
<hr/>
${html}
</div>
${share}
${navPost}
${navSeries}
${relatedHtml}
</section>`;
document.title = `${escapeHTML(post.title)} — The Fold Within Earth`;
$('#canonical').href = `${SITE_URL}/#/post/${slug}`;
addCardListeners();
} catch (e) {
$('#main').innerHTML = `<p class="error">⚠️ ${e.message}</p>`;
}
}
async function renderSearch(q, params) {
if (!q) {
$('#main').innerHTML = `<p class="info">Enter a search query above.</p>`;
return;
}
if (!window.Fuse) {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/fuse.js@7.0.0';
document.body.appendChild(script);
await new Promise(resolve => script.onload = resolve);
}
const fuse = new Fuse(state.posts, {keys: ['title', 'excerpt', 'tags', 'section'], threshold: 0.3, includeMatches: true});
const results = fuse.search(q).map(r => ({item: r.item, matches: r.matches}));
const title = `Search Results for "${escapeHTML(q)}" (${results.length} found)`;
$('#main').innerHTML = `<section class="search-results">
<h1>${title}</h1>
<div class="grid">${results.map(r => renderCard(r.item, r.matches)).join('')}</div>
</section>`;
document.title = `${title} — The Fold Within Earth`;
addCardListeners();
}
function renderAbout() {
const vision = [
'The Empathic Technologist — Fieldnotes, Research, Remembrance',
'Recursive Coherence Theory — Formal research, essays, whitepapers',
'The Fold Within Earth — Spiritual mythos; future interactive MUD (Evennia) link',
'Neutralizing Narcissism — Survivor support, behavioral research, accountability narratives',
'Simply WE — AI-focused identity/personhood/research/mythos',
'Mirrormire — AI-focused simulated world where machine gods & human myth intertwine'
];
const list = vision.map(v => `<li>${v}</li>`).join('');
$('#main').innerHTML = `<section class="about">
<h1>About The Fold Within Earth</h1>
<div class="markdown">
<p>Were building a canonical front door for a multi-track body of work:</p>
<ul>${list}</ul>
</div>
</section>`;
document.title = 'About — The Fold Within Earth';
}
function renderMud() {
$('#main').innerHTML = `<section class="mud">
<h1>MUD Portal</h1>
<p>MUD portal coming soon.</p>
</section>`;
document.title = 'MUD — The Fold Within Earth';
}
async function renderStart() {
const page = state.pages.find(p => p.file.endsWith('pages/start-here.md'));
if (!page) {
$('#main').innerHTML = `<p class="error">Start page not found.</p>`;
return;
}
try {
const res = await fetch(`content/${page.file}`);
const md = (await res.text()).replace(/^---[\s\S]*?---/,'').trim();
$('#main').innerHTML = `
<section class="post">
<div class="markdown">${sanitizeMarkdown(md)}</div>
</section>`;
document.title = `Start Here — The Fold Within Earth`;
} catch (e) {
$('#main').innerHTML = `<p class="error">⚠️ ${e.message}</p>`;
}
}
function renderProgramsHome() {
const cards = Object.entries(state.programTitles).map(([k, v]) => `<article data-slug="${k}" tabindex="0">
<div class="thumb"></div>
<h3>${escapeHTML(v)}</h3>
<p class="date">Program</p>
<p>Explore all posts and guidance for ${escapeHTML(v)}.</p>
</article>`).join('');
$('#main').innerHTML = `<section class="latest-all">
<h2>Programs</h2>
<div class="grid">${cards}</div>
</section>`;
document.title = `Programs — The Fold Within Earth`;
addCardListeners();
}
async function renderProgramArchive(key, params) {
if (!key) return render404();
const title = state.programTitles[key] || key;
const landing = state.pages.find(p => p.file.includes('pages/programs/') && (p.slug === key || p.title.toLowerCase().includes(key.toLowerCase())));
let landingHtml = '';
if (landing) {
const res = await fetch(`content/${landing.file}`);
const md = (await res.text()).replace(/^---[\s\S]*?---/,'').trim();
landingHtml = `<div class="markdown">${sanitizeMarkdown(md)}</div>`;
}
const list = (state.byProgram.get(key) || []).sort((a,b)=>b.date.localeCompare(a.date));
if (!list.length && !landingHtml) {
$('#main').innerHTML = `<p class="error">No posts found for Program: ${escapeHTML(title)}.</p>`;
return;
}
const cards = list.map(renderCard).join('');
$('#main').innerHTML = `
<section class="archive">
<div class="breadcrumbs">${renderBreadcrumbs(['program', key])}</div>
<h1>Program: ${escapeHTML(title)}</h1>
${landingHtml}
<div class="grid">${cards}</div>
</section>`;
document.title = `Program — ${escapeHTML(title)} — The Fold Within Earth`;
addCardListeners();
}
function render404() {
$('#main').innerHTML = `<p class="error">⚠️ Page not found.</p>`;
}
function addCardListeners() {
$('#main').addEventListener('click', e => {
const article = e.target.closest('article[data-slug]');
if (article) {
location.hash = `/post/${article.dataset.slug}`;
}
});
$('#main').addEventListener('keydown', e => {
const article = e.target.closest('article[data-slug]');
if (article && e.key === 'Enter') {
location.hash = `/post/${article.dataset.slug}`;
}
});
}
function renderBreadcrumbs(pathParts) {
let crumbs = `<a href="#/">Home</a>`;
let currentPath = '';
pathParts.forEach((part, i) => {
currentPath += `/${part}`;
let label = part.charAt(0).toUpperCase() + part.slice(1);
if (state.sectionTitles[part]) label = state.sectionTitles[part];
if (state.programTitles[part]) label = state.programTitles[part];
crumbs += ` <a href="#${currentPath}">${escapeHTML(label)}</a>`;
});
return `<nav class="breadcrumbs">${crumbs}</nav>`;
}
function escapeHTML(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}