Files
Website-2026/js/blog.js
T
2026-07-11 23:20:43 +02:00

193 lines
6.4 KiB
JavaScript
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.
// blog.js with collapsible posts
const BLOG_DIR = 'blog/';
const MANIFEST = BLOG_DIR + 'manifest.json';
// ---------- helpers ----------
function parseIndexHtmlList(text) {
const hrefs = [];
const re = /href="([^"]+)"/g;
let m;
while ((m = re.exec(text)) !== null) {
const name = m[1];
if (name.match(/\.(txt)$/i)) hrefs.push(name);
}
return hrefs;
}
// fetch a list of post filenames
async function loadPostList() {
try {
const r = await fetch(MANIFEST, { cache: 'no-cache' });
if (r.ok) return await r.json();
} catch (_) { /* fall through */ }
try {
const r = await fetch(BLOG_DIR, { cache: 'no-cache' });
if (r.ok) {
const text = await r.text();
return parseIndexHtmlList(text);
}
} catch (e) {
console.warn('Could not read blog directory listing', e);
}
return [];
}
// parse a single post file: first line = date/time, second = title, rest = content
async function fetchPost(filename) {
const resp = await fetch(BLOG_DIR + filename);
if (!resp.ok) throw new Error('Failed to fetch ' + filename);
const text = await resp.text();
const lines = text.split(/\r?\n/);
const dateTime = lines[0]?.trim() || '';
const title = lines[1]?.trim() || 'Untitled';
const content = lines.slice(2).join('\n').trim();
return { dateTime, title, content, filename };
}
// ---------- rendering with collapse ----------
function createPostElement(post) {
const article = document.createElement('article');
article.className = 'blog-post';
// header: date & title
const header = document.createElement('header');
const dateP = document.createElement('p');
dateP.className = 'post-date';
dateP.textContent = post.dateTime || 'No date';
const titleH = document.createElement('h3');
titleH.textContent = post.title;
header.appendChild(dateP);
header.appendChild(titleH);
article.appendChild(header);
// content container
const contentDiv = document.createElement('div');
contentDiv.className = 'post-content';
// full content (preserve line breaks)
const fullContent = document.createElement('div');
fullContent.className = 'post-full';
fullContent.style.whiteSpace = 'pre-wrap';
fullContent.textContent = post.content;
// preview content (only if needed)
const previewContent = document.createElement('div');
previewContent.className = 'post-preview';
previewContent.style.whiteSpace = 'pre-wrap';
const fullText = post.content;
const needsCollapse = fullText.length > 300;
if (needsCollapse) {
// Build preview: take up to 3 lines, but keep total length < 300
const lines = fullText.split(/\r?\n/);
let previewLines = [];
let previewLength = 0;
// take at least 3 lines if available, but stop if adding next line exceeds 300
for (let i = 0; i < Math.min(lines.length, 3); i++) {
const line = lines[i];
if (previewLength + line.length + (i > 0 ? 1 : 0) < 300) {
previewLines.push(line);
previewLength += line.length + (i > 0 ? 1 : 0);
} else {
// if we haven't taken any line yet, take part of this line
if (i === 0) {
previewLines.push(line.slice(0, 300));
previewLength = 300;
}
break;
}
}
// if we still have less than 3 lines but we have more lines, force include them up to 300 chars
while (previewLines.length < 3 && previewLines.length < lines.length) {
const nextLine = lines[previewLines.length];
if (previewLength + nextLine.length + (previewLines.length > 0 ? 1 : 0) < 300) {
previewLines.push(nextLine);
previewLength += nextLine.length + (previewLines.length > 0 ? 1 : 0);
} else {
// truncate the next line to fit within 300
const remaining = 300 - previewLength - (previewLines.length > 0 ? 1 : 0);
if (remaining > 0) {
previewLines.push(nextLine.slice(0, remaining));
}
break;
}
}
previewContent.textContent = previewLines.join('\n');
// if preview is empty, fallback to first 300 chars
if (!previewContent.textContent) {
previewContent.textContent = fullText.slice(0, 300);
}
// toggle button
const toggleBtn = document.createElement('button');
toggleBtn.className = 'read-more-btn';
toggleBtn.textContent = 'Read more';
toggleBtn.setAttribute('aria-expanded', 'false');
// Initially, full content is hidden, preview is visible
fullContent.style.display = 'none';
previewContent.style.display = 'block';
toggleBtn.addEventListener('click', () => {
const expanded = toggleBtn.getAttribute('aria-expanded') === 'true';
if (expanded) {
fullContent.style.display = 'none';
previewContent.style.display = 'block';
toggleBtn.textContent = 'Read more';
toggleBtn.setAttribute('aria-expanded', 'false');
} else {
fullContent.style.display = 'block';
previewContent.style.display = 'none';
toggleBtn.textContent = 'Show less';
toggleBtn.setAttribute('aria-expanded', 'true');
}
});
contentDiv.appendChild(previewContent);
contentDiv.appendChild(fullContent);
contentDiv.appendChild(toggleBtn);
} else {
// short post show full content only
fullContent.style.display = 'block';
contentDiv.appendChild(fullContent);
}
article.appendChild(contentDiv);
return article;
}
// ---------- main init ----------
async function initBlog() {
const list = await loadPostList();
const feed = document.querySelector('.blog-feed');
feed.innerHTML = '';
if (!list.length) {
feed.textContent = 'No blog posts found. Create blog/manifest.json or enable directory listing.';
return;
}
// fetch and sort posts (newest first based on filename or date? we'll sort by filename descending)
const posts = await Promise.all(list.map(f => fetchPost(f).catch(err => {
console.warn('Skipping', f, err);
return null;
})));
const validPosts = posts.filter(p => p !== null);
// sort by dateTime (if possible) or filename we'll sort by dateTime descending
validPosts.sort((a, b) => {
// try to parse as ISO date, else compare as strings
const dA = new Date(a.dateTime);
const dB = new Date(b.dateTime);
if (!isNaN(dA) && !isNaN(dB)) return dB - dA;
return b.dateTime.localeCompare(a.dateTime);
});
validPosts.forEach(post => {
const el = createPostElement(post);
feed.appendChild(el);
});
}
document.addEventListener('DOMContentLoaded', initBlog);