Skip to content

Commit 9324292

Browse files
committed
Implement multi-select filtering for blog posts and improved mobile filter behavior.
1 parent 703aadf commit 9324292

8 files changed

Lines changed: 123 additions & 31 deletions

File tree

blog.html

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -495,15 +495,18 @@ <h2 class="section-heading">Filter by Technology</h2>
495495

496496
let currentMax = 5;
497497
const POSTS_PER_PAGE = 5;
498-
let currentFilter = 'all';
498+
// Multi-select: Set of active filter tags (empty = show all)
499+
let activeFilters = new Set();
499500

500501
function updatePostsDisplay() {
501502
let visibleCount = 0;
502503

503-
// Filter posts
504+
// Filter posts: if no filters active, show all; otherwise OR logic
504505
const matchingPosts = blogPosts.filter(post => {
505-
const postTags = post.getAttribute('data-tags');
506-
return currentFilter === 'all' || postTags.includes(currentFilter);
506+
if (activeFilters.size === 0) return true;
507+
const postTags = post.getAttribute('data-tags').split(',');
508+
// OR: show post if ANY of its tags match ANY active filter
509+
return postTags.some(tag => activeFilters.has(tag.trim()));
507510
});
508511

509512
// Hide all posts first
@@ -525,30 +528,48 @@ <h2 class="section-heading">Filter by Technology</h2>
525528
}
526529
}
527530

528-
// Sync active state across both filter containers
529-
function syncFilterState(clickedTag, sourceContainer) {
531+
// Sync the visual active state across both filter containers
532+
function syncFilterUI() {
530533
const allContainers = [tagContainer, mobileTagContainer].filter(Boolean);
531-
const selectedTag = clickedTag.getAttribute('data-tag').toLowerCase();
532534

533535
allContainers.forEach(container => {
534536
container.querySelectorAll('.filter-tag').forEach(btn => {
535-
btn.classList.remove('active');
536-
if (btn.getAttribute('data-tag').toLowerCase() === selectedTag) {
537-
btn.classList.add('active');
537+
const tag = btn.getAttribute('data-tag').toLowerCase();
538+
if (tag === 'all') {
539+
// "Show All" is active when no filters are selected
540+
btn.classList.toggle('active', activeFilters.size === 0);
541+
} else {
542+
btn.classList.toggle('active', activeFilters.has(tag));
538543
}
539544
});
540545
});
546+
}
547+
548+
// Handle a filter tag click
549+
function handleFilterClick(clickedBtn) {
550+
const tag = clickedBtn.getAttribute('data-tag').toLowerCase();
551+
552+
if (tag === 'all') {
553+
// "Show All" clicked — clear all filters
554+
activeFilters.clear();
555+
} else if (activeFilters.has(tag)) {
556+
// Already active — deselect it (toggle off)
557+
activeFilters.delete(tag);
558+
} else {
559+
// Not active — add it (multi-select)
560+
activeFilters.add(tag);
561+
}
541562

542-
currentFilter = selectedTag;
543-
currentMax = POSTS_PER_PAGE;
563+
currentMax = POSTS_PER_PAGE; // Reset pagination on filter change
564+
syncFilterUI();
544565
updatePostsDisplay();
545566
}
546567

547568
// Desktop sidebar filter
548569
if (tagContainer) {
549570
tagContainer.addEventListener('click', function (e) {
550571
if (e.target.matches('.filter-tag')) {
551-
syncFilterState(e.target, tagContainer);
572+
handleFilterClick(e.target);
552573
}
553574
});
554575
}
@@ -557,23 +578,45 @@ <h2 class="section-heading">Filter by Technology</h2>
557578
if (mobileTagContainer) {
558579
mobileTagContainer.addEventListener('click', function (e) {
559580
if (e.target.matches('.filter-tag')) {
560-
syncFilterState(e.target, mobileTagContainer);
581+
handleFilterClick(e.target);
561582
}
562583
});
563584
}
564585

565586
// Mobile filter toggle button
566587
if (mobileFilterToggle && mobileFilterPanel) {
567-
mobileFilterToggle.addEventListener('click', function () {
588+
mobileFilterToggle.addEventListener('click', function (e) {
589+
e.stopPropagation();
568590
const isOpen = mobileFilterPanel.classList.toggle('open');
569591
mobileFilterToggle.classList.toggle('active', isOpen);
570592
});
593+
594+
// Close mobile filter when clicking outside
595+
document.addEventListener('click', function (e) {
596+
if (!mobileFilterPanel.contains(e.target) && !mobileFilterToggle.contains(e.target)) {
597+
mobileFilterPanel.classList.remove('open');
598+
mobileFilterToggle.classList.remove('active');
599+
}
600+
});
571601
}
572602

603+
// Load More Posts — scroll to the first newly visible post
573604
if (loadMoreBtn) {
574605
loadMoreBtn.addEventListener('click', function () {
606+
const previousMax = currentMax;
575607
currentMax += POSTS_PER_PAGE;
576608
updatePostsDisplay();
609+
610+
// Find the first newly revealed post and scroll to it
611+
const visiblePosts = blogPosts.filter(p => p.style.display !== 'none');
612+
if (visiblePosts.length > previousMax) {
613+
const firstNewPost = visiblePosts[previousMax];
614+
if (firstNewPost) {
615+
setTimeout(() => {
616+
firstNewPost.scrollIntoView({ behavior: 'smooth', block: 'start' });
617+
}, 50);
618+
}
619+
}
577620
});
578621
}
579622

blog_template.html

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ <h1>{{ name }}'s Blog</h1>
9393
<h2 class="section-heading">
9494
<span style="display: inline-flex; align-items: baseline; gap: 0.5rem;">
9595
Recent Posts
96-
<span
96+
<span id="total-posts-count"
9797
style="font-size: 0.65em; font-weight: 500; opacity: 0.6; text-transform: uppercase; font-style: italic; letter-spacing: 0.5px;">Total
9898
Posts: {{ blogs | length }}</span>
9999
</span>
@@ -183,15 +183,18 @@ <h2 class="section-heading">Filter by Technology</h2>
183183

184184
let currentMax = 5;
185185
const POSTS_PER_PAGE = 5;
186-
let currentFilter = 'all';
186+
// Multi-select: Set of active filter tags (empty = show all)
187+
let activeFilters = new Set();
187188

188189
function updatePostsDisplay() {
189190
let visibleCount = 0;
190191

191-
// Filter posts
192+
// Filter posts: if no filters active, show all; otherwise OR logic
192193
const matchingPosts = blogPosts.filter(post => {
193-
const postTags = post.getAttribute('data-tags');
194-
return currentFilter === 'all' || postTags.includes(currentFilter);
194+
if (activeFilters.size === 0) return true;
195+
const postTags = post.getAttribute('data-tags').split(',');
196+
// OR: show post if ANY of its tags match ANY active filter
197+
return postTags.some(tag => activeFilters.has(tag.trim()));
195198
});
196199

197200
// Hide all posts first
@@ -211,32 +214,56 @@ <h2 class="section-heading">Filter by Technology</h2>
211214
} else {
212215
loadMoreBtn.style.display = 'inline-block';
213216
}
217+
218+
// Update total posts counter
219+
const totalPostsEl = document.getElementById('total-posts-count');
220+
if (totalPostsEl) {
221+
totalPostsEl.textContent = 'Total Posts: ' + matchingPosts.length;
222+
}
214223
}
215224

216-
// Sync active state across both filter containers
217-
function syncFilterState(clickedTag, sourceContainer) {
225+
// Sync the visual active state across both filter containers
226+
function syncFilterUI() {
218227
const allContainers = [tagContainer, mobileTagContainer].filter(Boolean);
219-
const selectedTag = clickedTag.getAttribute('data-tag').toLowerCase();
220228

221229
allContainers.forEach(container => {
222230
container.querySelectorAll('.filter-tag').forEach(btn => {
223-
btn.classList.remove('active');
224-
if (btn.getAttribute('data-tag').toLowerCase() === selectedTag) {
225-
btn.classList.add('active');
231+
const tag = btn.getAttribute('data-tag').toLowerCase();
232+
if (tag === 'all') {
233+
// "Show All" is active when no filters are selected
234+
btn.classList.toggle('active', activeFilters.size === 0);
235+
} else {
236+
btn.classList.toggle('active', activeFilters.has(tag));
226237
}
227238
});
228239
});
240+
}
241+
242+
// Handle a filter tag click
243+
function handleFilterClick(clickedBtn) {
244+
const tag = clickedBtn.getAttribute('data-tag').toLowerCase();
245+
246+
if (tag === 'all') {
247+
// "Show All" clicked — clear all filters
248+
activeFilters.clear();
249+
} else if (activeFilters.has(tag)) {
250+
// Already active — deselect it (toggle off)
251+
activeFilters.delete(tag);
252+
} else {
253+
// Not active — add it (multi-select)
254+
activeFilters.add(tag);
255+
}
229256

230-
currentFilter = selectedTag;
231-
currentMax = POSTS_PER_PAGE;
257+
currentMax = POSTS_PER_PAGE; // Reset pagination on filter change
258+
syncFilterUI();
232259
updatePostsDisplay();
233260
}
234261

235262
// Desktop sidebar filter
236263
if (tagContainer) {
237264
tagContainer.addEventListener('click', function (e) {
238265
if (e.target.matches('.filter-tag')) {
239-
syncFilterState(e.target, tagContainer);
266+
handleFilterClick(e.target);
240267
}
241268
});
242269
}
@@ -245,23 +272,45 @@ <h2 class="section-heading">Filter by Technology</h2>
245272
if (mobileTagContainer) {
246273
mobileTagContainer.addEventListener('click', function (e) {
247274
if (e.target.matches('.filter-tag')) {
248-
syncFilterState(e.target, mobileTagContainer);
275+
handleFilterClick(e.target);
249276
}
250277
});
251278
}
252279

253280
// Mobile filter toggle button
254281
if (mobileFilterToggle && mobileFilterPanel) {
255-
mobileFilterToggle.addEventListener('click', function () {
282+
mobileFilterToggle.addEventListener('click', function (e) {
283+
e.stopPropagation();
256284
const isOpen = mobileFilterPanel.classList.toggle('open');
257285
mobileFilterToggle.classList.toggle('active', isOpen);
258286
});
287+
288+
// Close mobile filter when clicking outside
289+
document.addEventListener('click', function (e) {
290+
if (!mobileFilterPanel.contains(e.target) && !mobileFilterToggle.contains(e.target)) {
291+
mobileFilterPanel.classList.remove('open');
292+
mobileFilterToggle.classList.remove('active');
293+
}
294+
});
259295
}
260296

297+
// Load More Posts — scroll to the first newly visible post
261298
if (loadMoreBtn) {
262299
loadMoreBtn.addEventListener('click', function () {
300+
const previousMax = currentMax;
263301
currentMax += POSTS_PER_PAGE;
264302
updatePostsDisplay();
303+
304+
// Find the first newly revealed post and scroll to it
305+
const visiblePosts = blogPosts.filter(p => p.style.display !== 'none');
306+
if (visiblePosts.length > previousMax) {
307+
const firstNewPost = visiblePosts[previousMax];
308+
if (firstNewPost) {
309+
setTimeout(() => {
310+
firstNewPost.scrollIntoView({ behavior: 'smooth', block: 'start' });
311+
}, 50);
312+
}
313+
}
265314
});
266315
}
267316

182 KB
Loading
223 KB
Loading
220 KB
Loading
206 KB
Loading
209 KB
Loading
181 KB
Loading

0 commit comments

Comments
 (0)