PageSourceSearch

https://colonelpanic.tech/wp-content/themes/colonelpanic-theme/js/blog-manager.js?ver=1.4.0

js colonelpanic.tech collected 2026-10-07 23:06:00 UTC 17,118 bytes, 417 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.init();
15    }
16
17    async init() {
18        try {
19            await this.loadBlogData();
20            this.setupEventListeners();
21            this.renderBlogPosts();
22            this.renderFilters();
23        } catch (error) {
24            console.error('Failed to initialize blog:', error);
25            this.renderError();
26        }
27    }
28
29    async loadBlogData() {
30        try {
31            const response = await fetch('./blog-data.json?v=' + Date.now());
32            if (!response.ok) throw new Error('Failed to load blog data');
33            
34            const data = await response.json();
35            this.posts = data.posts.filter(post => post.published && post.category !== 'Reviews');
36            this.categories = data.categories;
37            this.tags = data.tags;
38            
39            // Sort posts by date (newest first)
40            this.posts.sort((a, b) => new Date(b.date) - new Date(a.date));
41            
42
43        } catch (error) {
44            console.error('Error loading blog data:', error);
45            // Fallback to hardcoded data with correct authors
46            this.posts = [
47                {
48                    id: 1,
49                    title: "Mesh-Mapper Project on Hackster.io",
50                    url: "https://www.hackster.io/colonelpanic/mesh-mapper-drone-remote-id-mapping-and-mesh-alerts-8e7c61",
51                    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.",
52                    date: "2025-05-31",
53                    author: "colonelpanic",
54                    category: "Projects",
55                    tags: ["ESP32", "Drone", "RemoteID"],
56                    published: true
57                },
58                {
59                    id: 2,
60                    title: "mesh-detect Featured in CNX Software",
61                    url: "https://www.cnx-software.com/2025/06/05/map-remote-id-enabled-drones-with-esp32-c3-s3-and-meshtastic-lora-modules/",
62                    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.",
63                    date: "2024-06-05",
64                    author: "CNX Software",
65                    category: "Press",
66                    tags: ["ESP32", "Coverage"],
67                    published: true
68                },
69                {
70                    id: 3,
71                    title: "Drone Mesh Mapper Update",
72                    url: "https://wrewtopia.com/read/2025-07-13",
73                    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.",
74                    date: "2025-07-13",
75                    author: "wrewtopia",
76                    category: "Reviews",
77                    tags: ["Update", "Deployment"],
78                    published: true
79                },
80                {
81                    id: 4,
82                    title: "Mesh Detect Setup (Part II)",
83                    url: "https://wrewtopia.com/read/2025-06-15",
84                    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.",
85                    date: "2025-06-15",
86                    author: "wrewtopia",
87                    category: "Tutorials",
88                    tags: ["Setup", "Guide"],
89                    published: true
90                },
91                {
92                    id: 5,
93                    title: "Mesh Detect Setup (Part I)",
94                    url: "https://wrewtopia.com/read/2025-06-14",
95                    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.",
96                    date: "2025-06-14",
97                    author: "wrewtopia",
98                    category: "Tutorials",
99                    tags: ["Setup", "Guide"],
100                    published: true
101                },
102                {
103                    id: 6,
104                    title: "Mesh Detect",
105                    url: "https://wrewtopia.com/read/2025-06-12-2",
106                    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.",
107                    date: "2025-06-12",
108                    author: "wrewtopia",
109                    category: "Reviews",
110                    tags: ["Introduction", "Hardware"],
111                    published: true
112                }
113            ];
114            this.categories = ['Projects', 'Tutorials', 'Updates', 'Press'];
115            this.tags = [];
116            
117            // Sort posts by date (newest first)
118            this.posts.sort((a, b) => new Date(b.date) - new Date(a.date));
119            
120
121        }
122    }
123
124    loadStaticContent() {
125        // Fallback to extract existing static content
126        const existingPosts = document.querySelectorAll('.blog-post');
127        this.posts = Array.from(existingPosts).map((post, index) => {
128            const title = post.querySelector('h3 a')?.textContent || 'Untitled';
129            const url = post.querySelector('h3 a')?.href || '#';
130            const excerpt = post.querySelector('p')?.textContent || '';
131            const date = post.querySelector('.blog-meta')?.textContent || '';
132            
133            return {
134                id: `post-${index}`,
135                title,
136                url,
137                excerpt,
138                date,
139                category: 'General',
140                tags: [],
141                author: 'colonelpanic', // Default to colonelpanic for existing posts
142                published: true
143            };
144        });
145    }
146
147    setupEventListeners() {
148        // Search functionality
149        const searchInput = document.getElementById('blog-search');
150        if (searchInput) {
151            searchInput.addEventListener('input', (e) => {
152                this.searchTerm = e.target.value.toLowerCase();
153                this.renderBlogPosts();
154            });
155        }
156
157        // Category filter
158        const categoryFilter = document.getElementById('category-filter');
159        if (categoryFilter) {
160            categoryFilter.addEventListener('change', (e) => {
161                this.currentCategory = e.target.value;
162                this.renderBlogPosts();
163            });
164        }
165
166        // Tag filter buttons (will be created dynamically)
167        document.addEventListener('click', (e) => {
168            if (e.target.classList.contains('tag-filter')) {
169                const tag = e.target.dataset.tag;
170                this.toggleTagFilter(tag);
171                this.renderBlogPosts();
172            }
173        });
174    }
175
176    toggleTagFilter(tag) {
177        if (this.currentFilter === tag) {
178            this.currentFilter = 'all';
179        } else {
180            this.currentFilter = tag;
181        }
182        this.updateTagButtons();
183    }
184
185    updateTagButtons() {
186        document.querySelectorAll('.tag-filter').forEach(btn => {
187            btn.classList.toggle('active', btn.dataset.tag === this.currentFilter);
188        });
189    }
190
191    filterPosts() {
192        return this.posts.filter(post => {
193            // Search filter
194            if (this.searchTerm) {
195                const searchMatch = 
196                    post.title.toLowerCase().includes(this.searchTerm) ||
197                    post.excerpt.toLowerCase().includes(this.searchTerm) ||
198                    post.tags.some(tag => tag.toLowerCase().includes(this.searchTerm));
199                if (!searchMatch) return false;
200            }
201
202            // Category filter
203            if (this.currentCategory !== 'all' && post.category !== this.currentCategory) {
204                return false;
205            }
206
207            // Tag filter
208            if (this.currentFilter !== 'all' && !post.tags.includes(this.currentFilter)) {
209                return false;
210            }
211
212            return true;
213        });
214    }
215
216    renderBlogPosts() {
217        this.renderFeaturedPosts();
218        this.renderRegularPosts();
219    }
220
221    renderFeaturedPosts() {
222        const featuredContainer = document.querySelector('#blog .featured-posts');
223        if (!featuredContainer) {
224            // Create featured posts container if it doesn't exist
225            const blogContainer = document.querySelector('#blog .container');
226            const regularGrid = blogContainer.querySelector('.grid-2');
227            if (blogContainer && regularGrid) {
228                const featuredSection = document.createElement('div');
229                featuredSection.className = 'featured-posts';
230                blogContainer.insertBefore(featuredSection, regularGrid);
231            }
232        }
233
234        // Featured posts should always show regardless of filters
235        const featuredPosts = this.posts.filter(post => post.featured === true);
236        const container = document.querySelector('#blog .featured-posts');
237        
238        if (!container) return;
239
240        if (featuredPosts.length === 0) {
241            container.innerHTML = '';
242            return;
243        }
244
245        container.innerHTML = `
246            <div class="featured-posts-grid">
247                ${featuredPosts.map(post => `
248                    <article class="featured-post" data-id="${post.id}">
249                        ${post.image ? `
250                            <div class="featured-image">
251                                <img src="${post.image}" alt="${post.title}" class="featured-img" />
252                                <div class="featured-image-overlay"></div>
253                            </div>
254                        ` : ''}
255                        <div class="featured-post-content">
256                            <div class="featured-meta">
257                                <span class="featured-category">${post.category}</span>
258                                <span class="featured-date">${this.formatDate(post.date)}</span>
259                                <span class="featured-author ${post.author === 'colonelpanic' ? 'author-colonelpanic' : ''}">${this.formatAuthor(post.author)}</span>
260                            </div>
261                            <h2 class="featured-post-title">
262                                <a href="${post.url || '#'}" ${post.url ? 'target="_blank" rel="noopener"' : ''}>${post.title}</a>
263                            </h2>
264                            <p class="featured-excerpt">${post.excerpt}</p>
265                            <div class="featured-tags">
266                                ${post.tags.map(tag => `<span class="featured-tag">${tag}</span>`).join('')}
267                            </div>
268                            <div class="featured-cta">
269                                <a href="${post.url || '#'}" ${post.url ? 'target="_blank" rel="noopener"' : ''} class="featured-link">Read Full Article</a>
270                            </div>
271                        </div>
272                    </article>
273                `).join('')}
274            </div>
275        `;
276
277        // No image removal for featured posts - we want to keep the images!
278    }
279
280    renderRegularPosts() {
281        const gridContainer = document.querySelector('#blog .grid-2');
282        if (!gridContainer) return;
283
284        const filteredPosts = this.filterPosts();
285        const regularPosts = filteredPosts.filter(post => post.featured !== true);
286        
287        if (regularPosts.length === 0 && filteredPosts.filter(post => post.featured === true).length === 0) {
288            gridContainer.innerHTML = `
289                <div class="no-posts">
290                    <h3>No articles found</h3>
291                    <p>Try adjusting your filters or search terms.</p>
292                </div>
293            `;
294            return;
295        }
296
297        if (regularPosts.length === 0) {
298            // Just add the header, no posts
299            const blogContainer = document.querySelector('#blog .container');
300            let headerElement = blogContainer.querySelector('.more-articles-header');
301            if (headerElement) {
302                headerElement.remove();
303            }
304            gridContainer.innerHTML = '';
305            return;
306        }
307
308        // Add "More Articles" header before the grid
309        const blogContainer = document.querySelector('#blog .container');
310        let headerElement = blogContainer.querySelector('.more-articles-header');
311        
312        if (!headerElement) {
313            headerElement = document.createElement('div');
314            headerElement.className = 'more-articles-header';
315            headerElement.innerHTML = '<h3 class="more-articles-title">More Articles</h3>';
316            blogContainer.insertBefore(headerElement, gridContainer);
317        }
318
319        gridContainer.innerHTML = regularPosts.map(post => `
320            <article class="blog-post" data-id="${post.id}">
321                <div class="blog-meta">
322                    <span class="blog-date">${this.formatDate(post.date)}</span>
323                    <span class="blog-author ${post.author === 'colonelpanic' ? 'author-colonelpanic' : ''}">${this.formatAuthor(post.author)}</span>
324                </div>
325                <div class="blog-category">${post.category}</div>
326                <h3><a href="${post.url || '#'}" ${post.url ? 'target="_blank" rel="noopener"' : ''}>${post.title}</a></h3>
327                <p>${post.excerpt}</p>
328                <div class="blog-tags">
329                    ${post.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
330                </div>
331            </article>
332        `).join('');
333        
334        // Remove any stray images from regular posts only
335        setTimeout(() => {
336            const blogImages = gridContainer.querySelectorAll('img:not(.featured-img)');
337            blogImages.forEach(img => {
338                if (!img.closest('.featured-posts')) {
339                    img.remove();
340                }
341            });
342        }, 100);
343    }
344
345    renderFilters() {
346        // Add search and filter controls
347        const blogSection = document.querySelector('#blog .container');
348        if (!blogSection) return;
349
350        const filtersHTML = `
351            <div class="blog-controls">
352                <div class="search-container">
353                    <input type="text" id="blog-search" placeholder="Search articles..." />
354                </div>
355                <div class="filter-container">
356                    <select id="category-filter">
357                        <option value="all">All Categories</option>
358                        ${this.categories.map(cat => `<option value="${cat}">${cat}</option>`).join('')}
359                    </select>
360                </div>
361                <div class="tag-filters">
362                    <button class="tag-filter active" data-tag="all">All</button>
363                    ${this.tags.slice(0, 8).map(tag => 
364                        `<button class="tag-filter" data-tag="${tag}">${tag}</button>`
365                    ).join('')}
366                </div>
367            </div>
368        `;
369
370        // Insert after the section title
371        const title = blogSection.querySelector('.section-title');
372        if (title) {
373            title.insertAdjacentHTML('afterend', filtersHTML);
374        }
375    }
376
377
378
379    formatDate(dateString) {
380        const date = new Date(dateString);
381        return date.toLocaleDateString('en-US', {
382            year: 'numeric',
383            month: 'long',
384            day: 'numeric'
385        });
386    }
387
388    formatAuthor(author) {
389        if (!author) return 'Unknown';
390        if (author === 'colonelpanic') {
391            return 'Colonel Panic';
392        }
393        return `by ${author}`;
394    }
395
396    renderError() {
397        const container = document.querySelector('#blog .grid-2');
398        if (container) {
399            container.innerHTML = `
400                <div class="blog-error">
401                    <h3>Unable to load articles</h3>
402                    <p>Please check your connection and try again.</p>
403                </div>
404            `;
405        }
406    }
407}
408
409// Initialize blog manager when DOM is loaded
410document.addEventListener('DOMContentLoaded', () => {
411    window.blogManager = new BlogManager();
412});
413
414// Export for use in other scripts
415if (typeof module !== 'undefined' && module.exports) {
416    module.exports = BlogManager;
417} 

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.