/** * Blog Listing Page - Client-Side Logic * Handles fetching, filtering, searching, sorting, and pagination of blog posts */ // State management let allPosts = []; let filteredPosts = []; let currentPage = 1; const postsPerPage = 9; // Filter state const activeFilters = { search: '', category: '', sort: 'date-desc' }; /** * Initialize the blog page */ async function init() { try { await loadPosts(); attachEventListeners(); } catch (error) { console.error('Error initializing blog:', error); showError('Failed to load blog posts. Please refresh the page.'); } } /** * Load all published blog posts from API */ async function loadPosts() { try { const response = await fetch('/api/blog'); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to load posts'); } allPosts = data.posts || []; filteredPosts = [...allPosts]; // Apply initial sorting sortPosts(); // Render initial view renderPosts(); updateResultsCount(); } catch (error) { console.error('Error loading posts:', error); showError('Failed to load blog posts'); } } /** * Render blog posts grid */ function renderPosts() { const gridEl = document.getElementById('blog-grid'); const emptyStateEl = document.getElementById('empty-state'); if (filteredPosts.length === 0) { gridEl.innerHTML = ''; emptyStateEl.classList.remove('hidden'); document.getElementById('pagination').classList.add('hidden'); return; } emptyStateEl.classList.add('hidden'); // Calculate pagination const startIndex = (currentPage - 1) * postsPerPage; const endIndex = startIndex + postsPerPage; const postsToShow = filteredPosts.slice(startIndex, endIndex); // Render posts const postsHTML = postsToShow.map(post => renderPostCard(post)).join(''); gridEl.innerHTML = postsHTML; // Render pagination renderPagination(); // Scroll to top when changing pages (except initial load) if (currentPage > 1) { window.scrollTo({ top: 0, behavior: 'smooth' }); } } /** * Render a single post card */ function renderPostCard(post) { const publishedDate = new Date(post.published_at); const formattedDate = publishedDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); // Calculate read time (rough estimate: 200 words per minute) const wordCount = post.content ? post.content.split(/\s+/).length : 0; const readTime = Math.max(1, Math.ceil(wordCount / 200)); // Truncate excerpt to 150 characters const excerpt = post.excerpt ? (post.excerpt.length > 150 ? `${post.excerpt.substring(0, 150) }...` : post.excerpt) : 'Read more...'; // Get category color const categoryColor = getCategoryColor(post.category); return `
${post.featured_image ? `
${escapeHtml(post.title)}
` : `
`}
${post.category ? ` ${escapeHtml(post.category)} ` : ''}

${escapeHtml(post.title)}

${escapeHtml(excerpt)}

${readTime} min read
${post.tags && post.tags.length > 0 ? `
${post.tags.slice(0, 3).map(tag => ` ${escapeHtml(tag)} `).join('')} ${post.tags.length > 3 ? `+${post.tags.length - 3} more` : ''}
` : ''}
`; } /** * Get category color gradient */ function getCategoryColor(category) { const colorMap = { 'Framework Updates': 'from-blue-400 to-blue-600', 'Case Studies': 'from-purple-400 to-purple-600', 'Research': 'from-green-400 to-green-600', 'Implementation': 'from-yellow-400 to-yellow-600', 'Community': 'from-pink-400 to-pink-600' }; return colorMap[category] || 'from-gray-400 to-gray-600'; } /** * Render pagination controls */ function renderPagination() { const paginationEl = document.getElementById('pagination'); const totalPages = Math.ceil(filteredPosts.length / postsPerPage); if (totalPages <= 1) { paginationEl.classList.add('hidden'); return; } paginationEl.classList.remove('hidden'); const prevBtn = document.getElementById('prev-page'); const nextBtn = document.getElementById('next-page'); const pageNumbersEl = document.getElementById('page-numbers'); // Update prev/next buttons prevBtn.disabled = currentPage === 1; nextBtn.disabled = currentPage === totalPages; // Render page numbers let pageNumbersHTML = ''; const maxVisiblePages = 5; let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); // Adjust start if we're near the end if (endPage - startPage < maxVisiblePages - 1) { startPage = Math.max(1, endPage - maxVisiblePages + 1); } // First page + ellipsis if (startPage > 1) { pageNumbersHTML += ` ${startPage > 2 ? '...' : ''} `; } // Visible page numbers for (let i = startPage; i <= endPage; i++) { const isActive = i === currentPage; pageNumbersHTML += ` `; } // Ellipsis + last page if (endPage < totalPages) { pageNumbersHTML += ` ${endPage < totalPages - 1 ? '...' : ''} `; } pageNumbersEl.innerHTML = pageNumbersHTML; } /** * Apply filters and search */ function applyFilters() { // Reset to first page when filters change currentPage = 1; // Start with all posts filteredPosts = [...allPosts]; // Apply search if (activeFilters.search) { const searchLower = activeFilters.search.toLowerCase(); filteredPosts = filteredPosts.filter(post => { return ( post.title.toLowerCase().includes(searchLower) || (post.content && post.content.toLowerCase().includes(searchLower)) || (post.excerpt && post.excerpt.toLowerCase().includes(searchLower)) || (post.tags && post.tags.some(tag => tag.toLowerCase().includes(searchLower))) ); }); } // Apply category filter if (activeFilters.category) { filteredPosts = filteredPosts.filter(post => post.category === activeFilters.category); } // Sort sortPosts(); // Update UI renderPosts(); updateResultsCount(); updateActiveFiltersDisplay(); } /** * Sort posts based on active sort option */ function sortPosts() { switch (activeFilters.sort) { case 'date-desc': filteredPosts.sort((a, b) => new Date(b.published_at) - new Date(a.published_at)); break; case 'date-asc': filteredPosts.sort((a, b) => new Date(a.published_at) - new Date(b.published_at)); break; case 'title-asc': filteredPosts.sort((a, b) => a.title.localeCompare(b.title)); break; case 'title-desc': filteredPosts.sort((a, b) => b.title.localeCompare(a.title)); break; } } /** * Update results count display */ function updateResultsCount() { const countEl = document.getElementById('post-count'); if (countEl) { countEl.textContent = filteredPosts.length; } } /** * Update active filters display */ function updateActiveFiltersDisplay() { const activeFiltersEl = document.getElementById('active-filters'); const filterTagsEl = document.getElementById('filter-tags'); const hasActiveFilters = activeFilters.search || activeFilters.category; if (!hasActiveFilters) { activeFiltersEl.classList.add('hidden'); return; } activeFiltersEl.classList.remove('hidden'); let tagsHTML = ''; if (activeFilters.search) { tagsHTML += ` Search: "${escapeHtml(activeFilters.search)}" `; } if (activeFilters.category) { tagsHTML += ` Category: ${escapeHtml(activeFilters.category)} `; } filterTagsEl.innerHTML = tagsHTML; } /** * Clear all filters */ function clearFilters() { activeFilters.search = ''; activeFilters.category = ''; // Reset UI elements document.getElementById('search-input').value = ''; document.getElementById('category-filter').value = ''; // Reapply filters applyFilters(); } /** * Remove specific filter */ function removeFilter(filterType) { if (filterType === 'search') { activeFilters.search = ''; document.getElementById('search-input').value = ''; } else if (filterType === 'category') { activeFilters.category = ''; document.getElementById('category-filter').value = ''; } applyFilters(); } /** * Attach event listeners */ function attachEventListeners() { // Search input (debounced) const searchInput = document.getElementById('search-input'); let searchTimeout; searchInput.addEventListener('input', e => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { activeFilters.search = e.target.value.trim(); applyFilters(); }, 300); }); // Category filter const categoryFilter = document.getElementById('category-filter'); categoryFilter.addEventListener('change', e => { activeFilters.category = e.target.value; applyFilters(); }); // Sort select const sortSelect = document.getElementById('sort-select'); sortSelect.addEventListener('change', e => { activeFilters.sort = e.target.value; applyFilters(); }); // Clear filters button const clearFiltersBtn = document.getElementById('clear-filters'); clearFiltersBtn.addEventListener('click', clearFilters); // Pagination - prev/next buttons const prevBtn = document.getElementById('prev-page'); const nextBtn = document.getElementById('next-page'); prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; renderPosts(); } }); nextBtn.addEventListener('click', () => { const totalPages = Math.ceil(filteredPosts.length / postsPerPage); if (currentPage < totalPages) { currentPage++; renderPosts(); } }); // Pagination - page numbers (event delegation) const pageNumbersEl = document.getElementById('page-numbers'); pageNumbersEl.addEventListener('click', e => { const pageBtn = e.target.closest('.page-number'); if (pageBtn) { currentPage = parseInt(pageBtn.dataset.page, 10); renderPosts(); } }); // Remove filter tags (event delegation) const filterTagsEl = document.getElementById('filter-tags'); filterTagsEl.addEventListener('click', e => { const removeBtn = e.target.closest('[data-remove-filter]'); if (removeBtn) { const filterType = removeBtn.dataset.removeFilter; removeFilter(filterType); } }); } /** * Show error message */ function showError(message) { const gridEl = document.getElementById('blog-grid'); gridEl.innerHTML = `

Error

${escapeHtml(message)}

`; } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Initialize on page load document.addEventListener('DOMContentLoaded', () => { init(); });