https://colonelpanic.tech/wp-content/themes/colonelpanic-theme/blog-manager.js?v=20260421-eff-forbes
1/** 2 * Blog Management System 3 * Handles dynamic loading, filtering, and display of blog posts 4 */ 5 6class BlogManager { 7 constructor() { 8 this.posts = []; 9 this.categories = []; 10 this.tags = []; 11 this.currentFilter = 'all'; 12 this.currentCategory = 'all'; 13 this.searchTerm = ''; 14 this.basePath = '.'; 15 this.init(); 16 } 17 18 async init() { 19 try { 20 await this.loadBlogData(); 21 this.setupEventListeners(); 22 this.renderBlogPosts(); 23 this.renderFilters(); 24 } catch (error) { 25 console.error('Failed to initialize blog:', error); 26 this.renderError(); 27 } 28 } 29 30 31 32 async loadBlogData() { 33 try { 34 // Use WordPress theme path if available, otherwise use relative path 35 const basePath = window.themePath || '.'; 36 const dataUrl = `${basePath}/blog-data-2026-04-21.json?v=${Date.now()}`; 37 console.log('Fetching blog data from:', dataUrl); 38 const response = await fetch(dataUrl); 39 console.log('Blog data response status:', response.status); 40 if (!response.ok) throw new Error('Failed to load blog data'); 41 42 const data = await response.json(); 43 this.posts = data.posts.filter(post => post.published && post.category !== 'Reviews'); 44 this.categories = data.categories; 45 this.tags = data.tags; 46 47 // Store the base path for image URL resolution 48 this.basePath = basePath; 49 50 // Convert relative image paths to absolute URLs if loading from remote 51 if (this.basePath && this.basePath !== '.') { 52 this.posts = this.posts.map(post => { 53 if (post.image && !post.image.startsWith('http')) { 54 // Convert relative path to absolute URL 55 const imageUrl = post.image.startsWith('/') 56 ? `${this.basePath}${post.image}` 57 : `${this.basePath}/${post.image}`; 58 console.log(`Converting image path: ${post.image} -> ${imageUrl}`); 59 return { ...post, image: imageUrl }; 60 } 61 return post; 62 }); 63 } 64 65 // Sort posts by date (newest first) 66 this.posts.sort((a, b) => new Date(b.date) - new Date(a.date)); 67 68 69 } catch (error) { 70 console.error('Error loading blog data:', error); 71 // Fallback to hardcoded data with correct authors 72 this.posts = [ 73 { 74 id: 1, 75 title: "Mesh-Mapper Project on Hackster.io", 76 url: "https://www.hackster.io/colonelpanic/mesh-mapper-drone-remote-id-mapping-and-mesh-alerts-8e7c61", 77 excerpt: "Comprehensive project documentation featuring dual-core ESP32 drone Remote ID detection, real-time web mapping, Meshtastic mesh alerts, FAA database integration, and custom PCB design with 2,595 views and full build instructions.", 78 date: "2025-05-31", 79 author: "colonelpanic", 80 category: "Projects", 81 tags: ["ESP32", "Drone", "RemoteID"], 82 published: true 83 }, 84 { 85 id: 2, 86 title: "mesh-detect Featured in CNX Software", 87 url: "https://www.cnx-software.com/2025/06/05/map-remote-id-enabled-drones-with-esp32-c3-s3-and-meshtastic-lora-modules/", 88 excerpt: "CNX Software covers our mesh-detect v2 project, highlighting its capabilities for mapping Remote ID-enabled drones using ESP32-C3/S3 and Meshtastic LoRa modules for distributed detection networks with 5-15km range.", 89 date: "2025-06-05", 90 author: "CNX Software", 91 category: "Press", 92 tags: ["ESP32", "Coverage"], 93 published: true 94 }, 95 { 96 id: 3, 97 title: "Drone Mesh Mapper Update", 98 url: "https://wrewtopia.com/read/2025-07-13", 99 excerpt: "Real-world deployment update on the mesh-detect system running 24/7/365 on a Raspberry Pi 4, successfully detecting and tracking drone activity with permanent placement in an office environment.", 100 date: "2025-07-13", 101 author: "wrewtopia", 102 category: "Reviews", 103 tags: ["Update", "Deployment"],
104 published: true 105 }, 106 { 107 id: 4, 108 title: "Mesh Detect Setup (Part II)", 109 url: "https://wrewtopia.com/read/2025-06-15", 110 excerpt: "Comprehensive setup guide for the drone mesh mapper software, including Python environment management with Uv, serial connection troubleshooting, and step-by-step instructions for getting the system operational.", 111 date: "2025-06-15", 112 author: "wrewtopia", 113 category: "Tutorials", 114 tags: ["Setup", "Guide"], 115 published: true 116 }, 117 { 118 id: 5, 119 title: "Mesh Detect Setup (Part I)", 120 url: "https://wrewtopia.com/read/2025-06-14", 121 excerpt: "Initial setup experience with the mesh-detect hardware and software, covering Python environment issues, web interface functionality, and first impressions of the drone detection system's capabilities.", 122 date: "2025-06-14", 123 author: "wrewtopia", 124 category: "Tutorials", 125 tags: ["Setup", "Guide"], 126 published: true 127 }, 128 { 129 id: 6, 130 title: "Mesh Detect", 131 url: "https://wrewtopia.com/read/2025-06-12-2", 132 excerpt: "Introduction to the Mesh Detect project - an inexpensive hardware solution for detecting WiFi and Bluetooth devices, with specialized firmware for drone identification and tracking capabilities.", 133 date: "2025-06-12", 134 author: "wrewtopia", 135 category: "Reviews", 136 tags: ["Introduction", "Hardware"], 137 published: true 138 } 139 ]; 140 this.categories = ['Projects', 'Tutorials', 'Updates', 'Press']; 141 this.tags = []; 142 143 144 145 // Sort posts by date (newest first) 146 this.posts.sort((a, b) => new Date(b.date) - new Date(a.date)); 147 148 149 } 150 } 151 152 loadStaticContent() { 153 // Fallback to extract existing static content 154 const existingPosts = document.querySelectorAll('.blog-post');
155 this.posts = Array.from(existingPosts).map((post, index) => { 156 const title = post.querySelector('h3 a')?.textContent || 'Untitled'; 157 const url = post.querySelector('h3 a')?.href || '#'; 158 const excerpt = post.querySelector('p')?.textContent || ''; 159 const date = post.querySelector('.blog-meta')?.textContent || ''; 160 161 return { 162 id: `post-${index}`, 163 title, 164 url, 165 excerpt, 166 date, 167 category: 'General', 168 tags: [], 169 author: 'colonelpanic', // Default to colonelpanic for existing posts 170 published: true 171 }; 172 }); 173 } 174 175 setupEventListeners() { 176 // Search functionality 177 const searchInput = document.getElementById('blog-search'); 178 if (searchInput) { 179 searchInput.addEventListener('input', (e) => { 180 this.searchTerm = e.target.value.toLowerCase(); 181 this.renderBlogPosts(); 182 }); 183 } 184 185 // Category filter 186 const categoryFilter = document.getElementById('category-filter'); 187 if (categoryFilter) { 188 categoryFilter.addEventListener('change', (e) => { 189 this.currentCategory = e.target.value; 190 this.renderBlogPosts(); 191 }); 192 } 193 194 // Tag filter buttons (will be created dynamically) 195 document.addEventListener('click', (e) => { 196 if (e.target.classList.contains('tag-filter')) { 197 const tag = e.target.dataset.tag; 198 this.toggleTagFilter(tag); 199 this.renderBlogPosts(); 200 } 201 }); 202 } 203 204 toggleTagFilter(tag) { 205 if (this.currentFilter === tag) { 206 this.currentFilter = 'all'; 207 } else { 208 this.currentFilter = tag; 209 } 210 this.updateTagButtons(); 211 } 212 213 updateTagButtons() { 214 document.querySelectorAll('.tag-filter').forEach(btn => { 215 btn.classList.toggle('active', btn.dataset.tag === this.currentFilter); 216 }); 217 } 218 219 filterPosts() { 220 return this.posts.filter(post => { 221 // Search filter 222 if (this.searchTerm) { 223 const searchMatch = 224 post.title.toLowerCase().includes(this.searchTerm) || 225 post.excerpt.toLowerCase().includes(this.searchTerm) || 226 post.tags.some(tag => tag.toLowerCase().includes(this.searchTerm)); 227 if (!searchMatch) return false; 228 } 229 230 // Category filter 231 if (this.currentCategory !== 'all' && post.category !== this.currentCategory) { 232 return false; 233 } 234 235 // Tag filter 236 if (this.currentFilter !== 'all' && !post.tags.includes(this.currentFilter)) { 237 return false; 238 } 239 240 return true; 241 }); 242 } 243 244 renderBlogPosts() { 245 this.renderFeaturedPosts(); 246 this.renderRegularPosts(); 247 } 248 249 renderFeaturedPosts() { 250 const featuredContainer = document.querySelector('#blog .featured-posts'); 251 if (!featuredContainer) { 252 // Create featured posts container if it doesn't exist 253 const blogContainer = document.querySelector('#blog .container'); 254 const regularGrid = blogContainer.querySelector('.grid-2'); 255 if (blogContainer && regularGrid) { 256 const featuredSection = document.createElement('div'); 257 featuredSection.className = 'featured-posts'; 258 blogContainer.insertBefore(featuredSection, regularGrid); 259 } 260 } 261 262 // Featured posts should always show regardless of filters 263 const featuredPosts = this.posts.filter(post => post.featured === true); 264 console.log('ð° Featured posts found:', featuredPosts.length, featuredPosts); 265 const container = document.querySelector('#blog .featured-posts'); 266 console.log('ð¦ Featured container found:', !!container); 267 268 if (!container) { 269 console.log('â No featured posts container found!'); 270 return; 271 } 272 273 if (featuredPosts.length === 0) { 274 console.log('ð« No featured posts to display'); 275 container.innerHTML = ''; 276 return; 277 } 278 279 console.log('ð¨ Rendering featured posts...'); 280 281 container.innerHTML = ` 282 <div class="featured-posts-grid"> 283 ${featuredPosts.map(post => { 284 // Use the image from blog data (converted to absolute URL if needed) 285 const imageUrl = post.image; 286 console.log(`ð¼ï¸ Featured post "${post.title}" using image: ${imageUrl || 'none'}`); 287 return ` 288 <article class="featured-post" data-id="${post.id}"> 289 ${imageUrl ? ` 290 <div class="featured-image"> 291 <img src="${imageUrl}" alt="${post.title}" class="featured-img" /> 292 <div class="featured-image-overlay"></div> 293 </div> 294 ` : ''} 295 <div class="featured-post-content"> 296 <div class="featured-meta">
297 <span class="featured-category">${post.category}</span> 298 <span class="featured-date">${this.formatDate(post.date)}</span> 299 <span class="featured-author ${post.author === 'colonelpanic' ? 'author-colonelpanic' : ''}">${this.formatAuthor(post.author)}</span> 300 </div> 301 <h2 class="featured-post-title"> 302 <a href="${post.url || '#'}" ${post.url ? 'target="_blank" rel="noopener"' : ''}>${post.title}</a> 303 </h2> 304 <p class="featured-excerpt">${post.excerpt}</p> 305 <div class="featured-tags"> 306 ${post.tags.map(tag => `<span class="featured-tag">${tag}</span>`).join('')} 307 </div> 308 <div class="featured-cta"> 309 <a href="${post.url || '#'}" ${post.url ? 'target="_blank" rel="noopener"' : ''} class="featured-link">Read Full Article</a> 310 </div> 311 </div> 312 </article> 313 `; 314 }).join('')} 315 </div> 316 `; 317 } 318 319 renderRegularPosts() { 320 const gridContainer = document.querySelector('#blog .grid-2'); 321 if (!gridContainer) return; 322 323 const filteredPosts = this.filterPosts(); 324 const regularPosts = filteredPosts.filter(post => post.featured !== true); 325 326 if (regularPosts.length === 0 && filteredPosts.filter(post => post.featured === true).length === 0) { 327 gridContainer.innerHTML = ` 328 <div class="no-posts"> 329 <h3>No articles found</h3> 330 <p>Try adjusting your filters or search terms.</p> 331 </div> 332 `; 333 return; 334 } 335 336 if (regularPosts.length === 0) { 337 // Just add the header, no posts 338 const blogContainer = document.querySelector('#blog .container'); 339 let headerElement = blogContainer.querySelector('.more-articles-header'); 340 if (headerElement) { 341 headerElement.remove(); 342 } 343 gridContainer.innerHTML = ''; 344 return; 345 } 346 347 // Add "More Articles" header before the grid 348 const blogContainer = document.querySelector('#blog .container'); 349 let headerElement = blogContainer.querySelector('.more-articles-header'); 350 351 if (!headerElement) { 352 headerElement = document.createElement('div'); 353 headerElement.className = 'more-articles-header'; 354 headerElement.innerHTML = '<h3 class="more-articles-title">More Articles</h3>'; 355 blogContainer.insertBefore(headerElement, gridContainer); 356 } 357 358 gridContainer.innerHTML = regularPosts.map(post => ` 359 <article class="blog-post" data-id="${post.id}"> 360 <div class="blog-meta"> 361 <span class="blog-date">${this.formatDate(post.date)}</span> 362 <span class="blog-author ${post.author === 'colonelpanic' ? 'author-colonelpanic' : ''}">${this.formatAuthor(post.author)}</span> 363 </div> 364 <div class="blog-category">${post.category}</div> 365 <h3><a href="${post.url || '#'}" ${post.url ? 'target="_blank" rel="noopener"' : ''}>${post.title}</a></h3> 366 <p>${post.excerpt}</p> 367 <div class="blog-tags"> 368 ${post.tags.map(tag => `<span class="tag">${tag}</span>`).join('')} 369 </div> 370 </article> 371 `).join(''); 372 373 // Remove any stray images from regular posts only 374 setTimeout(() => { 375 const blogImages = gridContainer.querySelectorAll('img:not(.featured-img)'); 376 blogImages.forEach(img => { 377 if (!img.closest('.featured-posts')) { 378 img.remove(); 379 } 380 }); 381 }, 100); 382 } 383 384 renderFilters() { 385 // Add search and filter controls 386 const blogSection = document.querySelector('#blog .container'); 387 if (!blogSection) return; 388 389 const filtersHTML = `
390 <div class="blog-controls"> 391 <div class="search-container"> 392 <input type="text" id="blog-search" placeholder="Search articles..." /> 393 </div> 394 <div class="filter-container"> 395 <select id="category-filter"> 396 <option value="all">All Categories</option> 397 ${this.categories.map(cat => `<option value="${cat}">${cat}</option>`).join('')} 398 </select> 399 </div> 400 <div class="tag-filters"> 401 <button class="tag-filter active" data-tag="all">All</button> 402 ${this.tags.slice(0, 8).map(tag => 403 `<button class="tag-filter" data-tag="${tag}">${tag}</button>` 404 ).join('')} 405 </div> 406 </div> 407 `; 408 409 // Insert after the section title 410 const title = blogSection.querySelector('.section-title'); 411 if (title) { 412 title.insertAdjacentHTML('afterend', filtersHTML); 413 } 414 } 415 416 417 418 formatDate(dateString) { 419 const date = new Date(dateString); 420 return date.toLocaleDateString('en-US', { 421 year: 'numeric', 422 month: 'long', 423 day: 'numeric' 424 }); 425 } 426 427 formatAuthor(author) { 428 if (!author) return 'Unknown'; 429 if (author === 'colonelpanic') { 430 return 'Colonel Panic'; 431 } 432 return `by ${author}`; 433 } 434 435 renderError() { 436 const container = document.querySelector('#blog .grid-2'); 437 if (container) { 438 container.innerHTML = ` 439 <div class="blog-error"> 440 <h3>Unable to load articles</h3> 441 <p>Please check your connection and try again.</p> 442 </div> 443 `; 444 } 445 } 446} 447 448// Initialize blog manager when DOM is loaded 449document.addEventListener('DOMContentLoaded', () => { 450 window.blogManager = new BlogManager(); 451}); 452 453// Export for use in other scripts 454if (typeof module !== 'undefined' && module.exports) { 455 module.exports = BlogManager; 456}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.