// blog.js – Loads blog posts and uses a gradient fade for long entries const BLOG_DIR = 'blog/'; const MANIFEST = BLOG_DIR + 'manifest.json'; // ---------- helpers ---------- function parseIndexHtmlList(html) { const hrefs = []; const re = /href="([^"]+)"/g; let m; while ((m = re.exec(html)) !== null) { const name = m[1]; if (name.match(/\.(txt|md|text)$/i) || !name.includes('.')) { if (!name.endsWith('/')) hrefs.push(name); } } return hrefs; } async function loadPostList() { try { const r = await fetch(MANIFEST, { cache: 'no-cache' }); if (r.ok) return await r.json(); } catch (_) { /* ignore */ } try { const r = await fetch(BLOG_DIR, { cache: 'no-cache' }); if (r.ok) { const html = await r.text(); return parseIndexHtmlList(html); } } catch (e) { console.warn('Could not read blog directory listing', e); } return []; } async function fetchPost(filename) { const res = await fetch(BLOG_DIR + filename, { cache: 'no-cache' }); if (!res.ok) throw new Error(`Failed to fetch ${filename}`); return await res.text(); } // ---------- rendering ---------- function renderPost(filename, content) { const lines = content.split('\n'); if (lines.length < 3) return null; const dateTime = lines[0].trim(); const title = lines[1].trim(); const bodyLines = lines.slice(2); const body = bodyLines.join('\n').trim(); const postDiv = document.createElement('div'); postDiv.className = 'blog-post'; // Title const titleEl = document.createElement('h3'); titleEl.className = 'blog-title'; titleEl.textContent = title; postDiv.appendChild(titleEl); // Date const metaEl = document.createElement('p'); metaEl.className = 'blog-meta'; metaEl.textContent = dateTime; postDiv.appendChild(metaEl); const isLong = body.length > 300; // Container for the body (with optional fade) const wrapper = document.createElement('div'); wrapper.className = 'blog-body-wrapper'; // The actual text const bodyDiv = document.createElement('div'); bodyDiv.className = 'blog-body'; bodyDiv.style.whiteSpace = 'pre-line'; bodyDiv.textContent = body; wrapper.appendChild(bodyDiv); // If long, add collapsed class and a toggle button if (isLong) { wrapper.classList.add('collapsed'); const toggleBtn = document.createElement('button'); toggleBtn.className = 'blog-toggle'; toggleBtn.textContent = 'Read more'; toggleBtn.addEventListener('click', function() { const isCollapsed = wrapper.classList.contains('collapsed'); if (isCollapsed) { wrapper.classList.remove('collapsed'); this.textContent = 'Show less'; } else { wrapper.classList.add('collapsed'); this.textContent = 'Read more'; } }); postDiv.appendChild(wrapper); postDiv.appendChild(toggleBtn); } else { // Short post – no collapse, no button postDiv.appendChild(wrapper); } return postDiv; } // ---------- main ---------- async function initBlog() { const feed = document.querySelector('.blog-feed'); if (!feed) return; feed.textContent = 'Loading blog posts…'; try { const files = await loadPostList(); if (!files.length) { feed.textContent = 'No blog posts found.'; return; } files.sort(); feed.innerHTML = ''; for (const file of files) { try { const content = await fetchPost(file); const el = renderPost(file, content); if (el) feed.appendChild(el); } catch (err) { console.warn(`Failed to load ${file}:`, err); const errDiv = document.createElement('div'); errDiv.className = 'blog-post'; errDiv.style.color = '#b91c1c'; errDiv.textContent = `⚠️ Could not load post: ${file}`; feed.appendChild(errDiv); } } if (feed.children.length === 0) { feed.textContent = 'No valid blog posts found.'; } } catch (err) { console.error('Blog init error:', err); feed.textContent = 'Error loading blog posts.'; } } document.addEventListener('DOMContentLoaded', initBlog);