PageSourceSearch

https://colonelpanic.tech/wp-content/themes/colonelpanic-theme/blog-manager.js?v=20260421-eff-forbes

js colonelpanic.tech collected 2026-10-07 23:05:58 UTC 18,979 bytes, 456 lines download raw bytes

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.