|
| 1 | +// Load and render blog posts from posts/posts.json |
| 2 | +async function loadPosts() { |
| 3 | + const container = document.getElementById('posts-container'); |
| 4 | + const emptyState = document.getElementById('empty-state'); |
| 5 | + |
| 6 | + try { |
| 7 | + const response = await fetch('posts/posts.json', { cache: 'no-cache' }); |
| 8 | + if (!response.ok) throw new Error('Could not load posts'); |
| 9 | + const posts = await response.json(); |
| 10 | + |
| 11 | + if (!posts || posts.length === 0) { |
| 12 | + emptyState.style.display = 'flex'; |
| 13 | + return; |
| 14 | + } |
| 15 | + |
| 16 | + // Sort newest first |
| 17 | + posts.sort((a, b) => new Date(b.date) - new Date(a.date)); |
| 18 | + container.innerHTML = posts.map(renderPost).join(''); |
| 19 | + } catch (err) { |
| 20 | + console.error(err); |
| 21 | + emptyState.style.display = 'flex'; |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +function renderPost(post) { |
| 26 | + const dateStr = formatDate(post.date); |
| 27 | + const meta = post.location ? `${dateStr} · ${post.location}` : dateStr; |
| 28 | + |
| 29 | + const imagesHtml = (post.images && post.images.length > 0) |
| 30 | + ? `<div class="blog-post-gallery ${galleryClass(post.images.length)}"> |
| 31 | + ${post.images.map(img => ` |
| 32 | + <div class="blog-post-image"> |
| 33 | + <img src="posts/${img}" alt="${escapeHtml(post.title)}" loading="lazy"> |
| 34 | + </div> |
| 35 | + `).join('')} |
| 36 | + </div>` |
| 37 | + : ''; |
| 38 | + |
| 39 | + return ` |
| 40 | + <article class="blog-post" id="post-${escapeHtml(post.id)}"> |
| 41 | + <header class="blog-post-header"> |
| 42 | + <h2 class="blog-post-title">${escapeHtml(post.title)}</h2> |
| 43 | + <p class="blog-post-meta">${escapeHtml(meta)}</p> |
| 44 | + </header> |
| 45 | + ${imagesHtml} |
| 46 | + <p class="blog-post-description">${escapeHtml(post.description)}</p> |
| 47 | + </article> |
| 48 | + `; |
| 49 | +} |
| 50 | + |
| 51 | +function galleryClass(count) { |
| 52 | + if (count === 1) return 'gallery-1'; |
| 53 | + if (count === 2) return 'gallery-2'; |
| 54 | + return 'gallery-multi'; |
| 55 | +} |
| 56 | + |
| 57 | +function formatDate(d) { |
| 58 | + const date = new Date(d); |
| 59 | + if (isNaN(date)) return d; |
| 60 | + return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); |
| 61 | +} |
| 62 | + |
| 63 | +function escapeHtml(str) { |
| 64 | + if (str == null) return ''; |
| 65 | + return String(str) |
| 66 | + .replace(/&/g, '&') |
| 67 | + .replace(/</g, '<') |
| 68 | + .replace(/>/g, '>') |
| 69 | + .replace(/"/g, '"') |
| 70 | + .replace(/'/g, '''); |
| 71 | +} |
| 72 | + |
| 73 | +// Navbar scroll shadow (shared behavior with index.html) |
| 74 | +const navbar = document.querySelector('.navbar'); |
| 75 | +window.addEventListener('scroll', () => { |
| 76 | + if (window.pageYOffset > 50) { |
| 77 | + navbar.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.3)'; |
| 78 | + } else { |
| 79 | + navbar.style.boxShadow = 'none'; |
| 80 | + } |
| 81 | +}); |
| 82 | + |
| 83 | +loadPosts(); |
0 commit comments