diff --git a/js/blog.js b/js/blog.js index 4a7c31b..97c501e 100644 --- a/js/blog.js +++ b/js/blog.js @@ -1,4 +1,4 @@ -// blog.js – Loads and renders blog posts with automatic collapse for long entries +// blog.js – Loads and renders blog posts with simple "Read more" toggle for long entries const BLOG_DIR = 'blog/'; const MANIFEST = BLOG_DIR + 'manifest.json'; @@ -14,7 +14,6 @@ function parseIndexHtmlList(html) { const name = m[1]; // accept typical text file extensions (and no extension) if (name.match(/\.(txt|md|text)$/i) || !name.includes('.')) { - // avoid directory links (ending with /) if (!name.endsWith('/')) hrefs.push(name); } } @@ -47,44 +46,12 @@ async function fetchPost(filename) { return await res.text(); } -// generate preview: up to 3 sentences, but not exceeding ~300 chars -function getPreview(text, maxLen = 300) { - if (text.length <= maxLen) return text; - - // try to split on sentence boundaries (. ! ?) followed by space or newline - const sentences = text.match(/[^.!?]+[.!?]+/g) || []; - let preview = ''; - let count = 0; - for (const s of sentences) { - if (preview.length + s.length <= maxLen) { - preview += s; - count++; - if (count >= 3) break; - } else { - // if we haven't reached 3 sentences, break and use what we have - break; - } - } - - // If we have at least one sentence, use that; otherwise fallback to word‑break - if (preview.trim().length > 0) { - return preview.trim(); - } - - // fallback: take first maxLen chars and cut at last space - let truncated = text.slice(0, maxLen); - const lastSpace = truncated.lastIndexOf(' '); - if (lastSpace > 0) truncated = truncated.slice(0, lastSpace); - return truncated + '…'; -} - // ---------- rendering ---------- function renderPost(filename, content) { const lines = content.split('\n'); if (lines.length < 3) { - // malformed: skip or treat as title‑only - return null; + return null; // malformed } const dateTime = lines[0].trim(); const title = lines[1].trim(); @@ -94,13 +61,13 @@ function renderPost(filename, content) { const postDiv = document.createElement('div'); postDiv.className = 'blog-post'; - // title + // Title const titleEl = document.createElement('h3'); titleEl.className = 'blog-title'; titleEl.textContent = title; postDiv.appendChild(titleEl); - // meta + // Date const metaEl = document.createElement('p'); metaEl.className = 'blog-meta'; metaEl.textContent = dateTime; @@ -108,66 +75,31 @@ function renderPost(filename, content) { const isLong = body.length > 300; + // Body container (always created, but hidden for long posts) + const bodyDiv = document.createElement('div'); + bodyDiv.className = 'blog-body'; + bodyDiv.style.whiteSpace = 'pre-line'; + bodyDiv.textContent = body; + if (!isLong) { - // short post – display full body directly - const bodyDiv = document.createElement('div'); - bodyDiv.className = 'blog-body'; - bodyDiv.style.whiteSpace = 'pre-line'; - bodyDiv.textContent = body; + // Short post – show body directly postDiv.appendChild(bodyDiv); } else { - // long post – show preview with toggle - const container = document.createElement('div'); - container.className = 'blog-collapsible'; + // Long post – hide body initially, add toggle button + bodyDiv.style.display = 'none'; + postDiv.appendChild(bodyDiv); - // preview - const previewDiv = document.createElement('div'); - previewDiv.className = 'blog-preview'; - previewDiv.style.whiteSpace = 'pre-line'; - let previewText = getPreview(body); - // ensure ellipsis if preview is truncated - if (previewText.length < body.length && !previewText.endsWith('…')) { - previewText += '…'; - } - previewDiv.textContent = previewText; - container.appendChild(previewDiv); + // Toggle button + const toggleBtn = document.createElement('button'); + toggleBtn.className = 'blog-toggle'; + toggleBtn.textContent = 'Read more'; + postDiv.appendChild(toggleBtn); - // Read more button - const readMoreBtn = document.createElement('button'); - readMoreBtn.className = 'blog-toggle read-more'; - readMoreBtn.textContent = 'Read more'; - container.appendChild(readMoreBtn); - - // Full content (hidden initially) - const fullDiv = document.createElement('div'); - fullDiv.className = 'blog-full'; - fullDiv.style.whiteSpace = 'pre-line'; - fullDiv.textContent = body; - fullDiv.style.display = 'none'; - container.appendChild(fullDiv); - - // Show less button (hidden initially) – appears at the bottom - const showLessBtn = document.createElement('button'); - showLessBtn.className = 'blog-toggle show-less'; - showLessBtn.textContent = '✕ Show less'; - showLessBtn.style.display = 'none'; - container.appendChild(showLessBtn); - - // Toggle logic - function expand() { - fullDiv.style.display = 'block'; - readMoreBtn.style.display = 'none'; - showLessBtn.style.display = 'inline-block'; - } - function collapse() { - fullDiv.style.display = 'none'; - readMoreBtn.style.display = 'inline-block'; - showLessBtn.style.display = 'none'; - } - readMoreBtn.addEventListener('click', expand); - showLessBtn.addEventListener('click', collapse); - - postDiv.appendChild(container); + toggleBtn.addEventListener('click', function() { + const isHidden = bodyDiv.style.display === 'none'; + bodyDiv.style.display = isHidden ? 'block' : 'none'; + this.textContent = isHidden ? 'Show less' : 'Read more'; + }); } return postDiv; @@ -188,11 +120,8 @@ async function initBlog() { return; } - // sort files by name (oldest first? we'll sort by date if embedded, but we'll just alphabetical) - // Typically filenames like 2025-01-01.txt, so alphabetical = chronological - files.sort(); + files.sort(); // alphabetical -> chronological - // clear feed feed.innerHTML = ''; for (const file of files) { @@ -202,7 +131,6 @@ async function initBlog() { if (el) feed.appendChild(el); } catch (err) { console.warn(`Failed to load ${file}:`, err); - // optionally show error placeholder const errDiv = document.createElement('div'); errDiv.className = 'blog-post'; errDiv.style.color = '#b91c1c'; @@ -211,7 +139,6 @@ async function initBlog() { } } - // if nothing was added, show message if (feed.children.length === 0) { feed.textContent = 'No valid blog posts found.'; } diff --git a/styles.css b/styles.css index f696305..e60d1a3 100644 --- a/styles.css +++ b/styles.css @@ -24,17 +24,40 @@ a:hover { color:#991b1b; text-decoration:underline; } #links li { margin:0; } #links a { display:block; padding:12px 16px; background:#fff; border:1px solid #e5e7eb; border-radius:6px; transition:all 0.3s ease; } #links a:hover { border-color:#dc2626; box-shadow:0 4px 12px rgba(220,38,38,0.15); } + .blog-feed { display:grid; gap:24px; margin-top:16px; } .blog-post { background:#fff; border:1px solid #e5e7eb; border-radius:12px; padding:24px; box-shadow:0 1px 6px rgba(139,18,18,0.08); } .blog-post + .blog-post { border-top:4px solid #dc2626; margin-top:32px; padding-top:32px; } .blog-title { margin:0 0 8px 0; font-size:1.5rem; } .blog-meta { margin:0 0 16px 0; color:#7f1d1d; font-size:0.95rem; } + +.blog-body { + white-space: pre-line; + margin: 0 0 1em 0; +} .blog-body p { margin:0 0 1em 0; } .blog-body p:last-child { margin-bottom:0; } + +/* Toggle button */ +.blog-toggle { + background: none; + border: none; + color: #dc2626; + font-weight: bold; + cursor: pointer; + padding: 0.25rem 0; + font-size: 0.95rem; + display: inline-block; +} +.blog-toggle:hover { + text-decoration: underline; +} + .blog-attachment-list { margin:16px 0 0 0; padding:0; list-style:none; } .blog-attachment-list li { margin:0.5em 0; } .blog-attachment-list a { color:#dc2626; text-decoration:none; } .blog-attachment-list a:hover { text-decoration:underline; } + @media (max-width:520px){ h2 { font-size:1.4rem; } .site-header h1 { font-size:1.6rem; } .container { padding:16px; } } .header-links { margin-top:12px; } @@ -54,44 +77,4 @@ a:hover { color:#991b1b; text-decoration:underline; } .modal-close:hover, .modal-prev:hover, .modal-next:hover { background:rgba(255,255,255,0.2); } .modal-close { right:20px; top:20px; } .modal-prev { left:20px; top:50%; transform:translateY(-50%); } -.modal-next { right:20px; top:50%; transform:translateY(-50%); } - -.blog-collapse summary { cursor: pointer; list-style: none; } -.blog-collapse summary::-webkit-details-marker { display: none; } -.blog-preview { white-space: pre-line; margin-bottom: 0.5rem;} -.read-more-text {display: inline-block; font-weight: bold; margin-top: 0.5rem; } -.read-more-text:hover { text-decoration: underline; } -.blog-collapse[open] .read-more-text { margin-bottom: 1rem; } - -/* Collapsible blog post styles */ -.blog-collapsible { - margin-top: 0.5rem; -} - -.blog-preview { - white-space: pre-line; - margin-bottom: 0.5rem; -} - -.blog-full { - white-space: pre-line; - margin-top: 0.5rem; -} - -.blog-toggle { - background: none; - border: none; - color: #dc2626; - font-weight: bold; - cursor: pointer; - padding: 0.25rem 0; - font-size: 0.95rem; -} - -.blog-toggle:hover { - text-decoration: underline; -} - -.show-less { - display: none; /* hidden by default, shown when expanded */ -} \ No newline at end of file +.modal-next { right:20px; top:50%; transform:translateY(-50%); } \ No newline at end of file