PageSourceSearch

https://ucl.edu.pk/js/modules/blog-list.js

js ucl.edu.pk collected 2026-09-24 10:15:25 UTC 13,277 bytes, 250 lines download raw bytes

1/**
2 * UCL Blogs Grid Listing Module
3 * Handles public blogs directory card rendering, category badges, tag filters, text search, and state transitions
4 */
5
6(function () {
7    'use strict';
8
9    class UCLBlogList {
10        get api() {
11            return window.UCLBlogAPI;
12        }
13
14        constructor() {
15            this.allBlogs = [];
16            this.categories = [];
17            this.tags = [];
18            this.activeCategoryId = null; // null = all
19            this.activeTagId = null;      // null = all
20            this.searchQuery = "";
21        }
22
23        // Initialize directory page
24        async init() {
25            const gridContainer = document.getElementById('blogs-grid-container');
26            if (!gridContainer) return;
27
28            gridContainer.innerHTML = `
29                <div style="text-align: center; padding: 60px; width: 100%;">
30                    <i class="fa fa-spinner fa-spin" style="font-size: 32px; color: #192f59; margin-bottom: 15px;"></i>
31                    <p style="font-size: 16px; color: #666; font-weight: 500;">Connecting to campus database...</p>
32                </div>
33            `;
34
35            try {
36                // 1. Fetch categories and tags to build filters
37                this.categories = await this.api.getCategories();
38                this.tags = await this.api.getTags();
39
40                // 2. Fetch all published blogs
41                this.allBlogs = await this.api.getBlogs({ status: 'published' });
42
43                // 3. Render filter UI panels
44                this.renderFilterPanels();
45
46                // 4. Render blogs grid
47                this.renderBlogsGrid();
48
49                // 5. Wire up search listener
50                const searchInput = document.getElementById('blog-search');
51                if (searchInput) {
52                    searchInput.addEventListener('input', (e) => {
53                        this.searchQuery = e.target.value.toLowerCase().trim();
54                        this.renderBlogsGrid();
55                    });
56                }
57
58            } catch (error) {
59                console.error("Failed to initialize blogs directory:", error);
60                gridContainer.innerHTML = `
61                    <div style="text-align: center; padding: 40px; width: 100%;">
62                        <i class="fa fa-exclamation-triangle" style="font-size: 40px; color: #D22B2B; margin-bottom: 15px;"></i>
63                        <p style="color: #D22B2B; font-size: 18px; font-weight: 600;">Failed to load university blogs.</p>
64                        <p style="color: #777; font-size: 14px;">Error: ${error.message}</p>
65                    </div>
66                `;
67            }
68        }
69
70        // Render dynamic category filter buttons and tag filter pills
71        renderFilterPanels() {
72            // A. Category Filter Buttons
73            const categoryContainer = document.getElementById('category-filters-container');
74            if (categoryContainer) {
75                let html = `<button class="filter-btn active" id="btn-cat-all" onclick="UCLBlogListInstance.filterCategory(null)" style="padding: 8px 18px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s;">All</button>`;
76                
77                html += this.categories.map(cat => `
78                    <button class="filter-btn" id="btn-cat-${cat.id}" onclick="UCLBlogListInstance.filterCategory('${cat.id}')" style="padding: 8px 18px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; margin-left: 5px;">${cat.name}</button>
79                `).join('');
80                
81                categoryContainer.innerHTML = html;
82            }
83
84            // B. Tags Filter Pills (Rendered dynamically if a #tag-filters-container exists)
85            const tagContainer = document.getElementById('tag-filters-container');
86            if (tagContainer) {
87                if (this.tags.length === 0) {
88                    tagContainer.style.display = 'none';
89                    return;
90                }
91
92                let html = `<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-top: 15px;">
93                    <span style="font-size:13px; color:#666; display:flex; align-items:center; margin-right:5px; font-weight:500;">Filter by Tag:</span>
94                    <button class="tag-pill-btn active" id="btn-tag-all" onclick="UCLBlogListInstance.filterTag(null)">#Any</button>`;
95                
96                html += this.tags.map(tag => `
97                    <button class="tag-pill-btn" id="btn-tag-${tag.id}" onclick="UCLBlogListInstance.filterTag('${tag.id}')">#${tag.name}</button>
98                `).join('');
99                
100                html += `</div>`;
101                tagContainer.innerHTML = html;
102            }
103        }
104
105        // Handle category filter selections
106        async filterCategory(categoryId) {
107            this.activeCategoryId = categoryId;
108
109            // Update UI class active states
110            const buttons = document.querySelectorAll('#category-filters-container .filter-btn');
111            buttons.forEach(btn => btn.classList.remove('active'));
112
113            const targetBtnId = categoryId ? `btn-cat-${categoryId}` : 'btn-cat-all';
114            const targetBtn = document.getElementById(targetBtnId);
115            if (targetBtn) targetBtn.classList.add('active');
116
117            // Re-render
118            this.renderBlogsGrid();
119        }
120
121        // Handle tag filter selections
122        async filterTag(tagId) {
123            this.activeTagId = tagId;
124
125            // Update tag pills class active states
126            const tagBtns = document.querySelectorAll('#tag-filters-container .tag-pill-btn');
127            tagBtns.forEach(btn => btn.classList.remove('active'));
128
129            const targetTagId = tagId ? `btn-tag-${tagId}` : 'btn-tag-all';
130            const targetTag = document.getElementById(targetTagId);
131            if (targetTag) targetTag.classList.add('active');
132
133            // If a tag is selected, we fetch blogs from the database matching this tag, or do it in-memory
134            // To ensure 100% correct tag matching, we'll fetch from database if tagId is active, otherwise reset
135            const gridContainer = document.getElementById('blogs-grid-container');
136            gridContainer.innerHTML = `
137                <div style="text-align: center; padding: 40px; width: 100%;">
138                    <i class="fa fa-spinner fa-spin" style="font-size: 24px; color: #192f59; margin-bottom: 10px;"></i>
139                    <p style="font-size: 14px; color: #777;">Filtering by tag...</p>
140                </div>
141            `;
142
143            try {
144                const filters = { status: 'published' };
145                if (this.activeCategoryId) filters.category_id = this.activeCategoryId;
146                if (this.activeTagId) filters.tag_id = this.activeTagId;
147
148                this.allBlogs = await this.api.getBlogs(filters);
149                this.renderBlogsGrid();
150            } catch (err) {
151                console.error("Tag filter API failed:", err);
152                gridContainer.innerHTML = `<div style="text-align: center; padding: 40px; width:100%; color:red;">Failed to filter tags.</div>`;
153            }
154        }
155
156        // Render standard grid with matching styling and responsive dimensions
157        renderBlogsGrid() {
158            const container = document.getElementById('blogs-grid-container');
159            if (!container) return;
160
161            // Client-side filter: Search text and Category ID
162            const filteredBlogs = this.allBlogs.filter(blog => {
163                const matchesSearch = blog.title.toLowerCase().includes(this.searchQuery) || 
164                                      (blog.excerpt && blog.excerpt.toLowerCase().includes(this.searchQuery)) ||
165                                      (blog.content && blog.content.toLowerCase().includes(this.searchQuery));
166                
167                const matchesCategory = !this.activeCategoryId || blog.category_id === this.activeCategoryId;
168                
169                return matchesSearch && matchesCategory;
170            });
171
172            if (filteredBlogs.length === 0) {
173                container.innerHTML = `
174                    <div style="text-align: center; padding: 60px; width: 100%; background: #ffffff; border-radius: 6px; border: 1px dashed #ddd;">
175                        <i class="fa fa-info-circle" style="font-size: 32px; color: #888; margin-bottom: 15px;"></i>
176                        <p style="font-size: 18px; font-weight: 500; color: #666; margin: 0;">No articles match your selection.</p>
177                        <p style="font-size: 14px; color: #999; margin-top: 5px;">
177Try adjusting your search filters or clearing categories.</p>
178                    </div>
179                `;
180                return;
181            }
182
183            container.innerHTML = filteredBlogs.map(blog => {
184                const dateObj = blog.published_at ? new Date(blog.published_at) : new Date(blog.created_at);
185                const formattedDate = dateObj.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
186                
187                // Truncate clean teaser from content
188                let teaserText = blog.excerpt;
189                if (!teaserText) {
190                    const tempDiv = document.createElement('div');
191                    tempDiv.innerHTML = blog.content || "";
192                    teaserText = tempDiv.textContent || tempDiv.innerText || "";
193                }
194                if (teaserText.length > 130) {
195                    teaserText = teaserText.substring(0, 127) + "...";
196                }
197
198                const categoryName = blog.categories ? blog.categories.name : 'News';
199                const imageSrc = blog.featured_image_url || 'images/blog-default.jpg';
200                const imageAlt = blog.featured_image_alt || blog.title;
201
202                // Adaptive link generation (Query strings on local file://, pretty paths on live server)
203                const detailUrl = window.location.protocol === 'file:' 
204                    ? `blog-detail.html?slug=${blog.slug}` 
205                    : `/${blog.slug}`;
206
207                return `
208                    <div class="gdlr-core-item-list ucl-blog-card-col gdlr-core-item-pdlr gdlr-core-item-mgb gdlr-core-column-20">
209                        <div class="gdlr-core-blog-modern" style="height: 100%; display: flex; flex-direction: column;">
210                            <div class="gdlr-core-blog-thumbnail gdlr-core-media-image">
211                                <a href="${detailUrl}">
212                                    <img src="${imageSrc}" alt="${imageAlt.replace(/"/g, '&quot;')}" style="width: 100%; height: 240px; object-fit: cover;" />
213                                </a>
214                            </div>
215                            <div class="gdlr-core-blog-modern-content" style="padding: 25px; flex: 1; display: flex; flex-direction: column; justify-content: space-between;">
216                                <div>
217                                    <div style="display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 13px;">
218                                        <span style="color: #D22B2B; font-weight: 600; text-transform: uppercase;">${categoryName}</span>
219                                        <span style="color: #888;">${formattedDate}</span>
220                                    </div>
221                                    <h3 class="gdlr-core-blog-title gdlr-core-skin-title" style="font-size: 18px; font-weight: 700; line-height: 1.4; margin-bottom: 12px; max-height: 52px; overflow: hidden;">
222                                        <a href="${detailUrl}" style="color: #163269; transition: color 0.2s;">${blog.title}</a>
223                                    </h3>
224                                    <p style="color: #666; font-size: 14px; line-height: 1.6; max-height: 68px; overflow: hidden; margin-bottom: 20px;">${teaserText}</p>
225                                </div>
226                                <div style="border-top: 1px solid #f0f0f0; padding-top: 12px; display: flex; align-items: center; justify-content: space-between;">
227                                    <span style="color: #192f59; font-weight: 500; font-size: 13px;">By ${blog.author_name || 'UCL Admin'}</span>
228                                    <a href="${detailUrl}" style="color: #D22B2B; font-weight: 600; font-size: 13px; text-decoration: none; transition: color 0.2s; display: flex; align-items: center; gap: 5px;">
229                                        Read More <i class="fa fa-long-arrow-right"></i>
230                                    </a>
231                                </div>
232                            </div>
233                        </div>
234                    </div>
235                `;
236            }).join('');
237        }
238    }
239
240    // Initialize instance and export to window for event handler references
241    const listInstance = new UCLBlogList();
242    window.UCLBlogListInstance = listInstance;
243
244    // Run when DOM loads
245    if (document.readyState === 'loading') {
246        document.addEventListener('DOMContentLoaded', () => listInstance.init());
247    } else {
248        listInstance.init();
249    }
250})();

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.