Skip to content

Commit b782ff5

Browse files
committed
Standardize tags and display top row dynamically
1 parent 557dbbd commit b782ff5

10 files changed

+266
-109
lines changed

blog-data/ai-guardrails.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@
1313
}
1414
],
1515
"tags": [
16-
"AI",
16+
"Artificial Intelligence",
1717
"Prompt Engineering",
1818
"Guardrails",
1919
"Ethics",
2020
"Machine Learning",
21-
"Artificial Intelligence",
2221
"Responsible AI",
2322
"Future Skills"
2423
],

blog-data/getting-started-with-ai.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"Artificial Intelligence",
1717
"Machine Learning",
1818
"Technology",
19-
"Beginners",
19+
"Beginner Friendly",
2020
"Enterprise AI",
2121
"Generative AI Enterprise",
2222
"GDG Twin Cities"

blog-data/go-to-the-store-and-buy-bread.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
],
1515
"tags": [
1616
"Learning",
17-
"Tech-Education",
17+
"Tech Education",
1818
"Documentation",
19-
"Beginner-Friendly",
20-
"AI-Companion",
19+
"Beginner Friendly",
20+
"AI Assistant",
2121
"Sparky",
22-
"Developer-Experience",
23-
"Empathy-In-Tech",
22+
"Developer Experience",
23+
"Empathy in Tech",
2424
"Tutorials",
25-
"Blog-Series"
25+
"Blog Series"
2626
],
2727
"author": "Emily Anderson",
2828
"date": "2025-04-14",

blog-data/humans-have-ideas.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
}
1414
],
1515
"tags": [
16-
"AI",
16+
"Artificial Intelligence",
1717
"Productivity",
1818
"Creativity",
1919
"Technology",

blog-data/sean-website-case-study.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"Web Development",
1717
"Case Study",
1818
"Wix",
19-
"Website Design"
19+
"Web Design"
2020
],
2121
"author": "Emily Anderson",
2222
"date": "2025-03-10",

index.html

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,6 @@ <h2>My Projects</h2>
126126
<h2>My Blog</h2>
127127
<p>Thoughts, insights, and tutorials on technology, AI, and software development</p>
128128

129-
<div class="blog-filter">
130-
<div class="blog-filter-tags">
131-
<!-- Tag filters will be populated by JavaScript -->
132-
</div>
133-
</div>
134-
135129
<div class="blog-grid">
136130
<!-- Blog posts will be loaded dynamically from blog-data.json -->
137131
</div>

js/blog-archive.js

Lines changed: 174 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -93,45 +93,205 @@ function displayBlogPosts(posts) {
9393
function populateTagFilters(posts) {
9494
const tagFilterContainer = document.querySelector('.blog-filter-tags');
9595
if (!tagFilterContainer) return;
96-
97-
// Extract all unique tags
98-
const allTags = new Set();
96+
97+
// Calculate tag frequencies
98+
const tagCounts = {};
9999
posts.forEach(post => {
100100
post.tags.forEach(tag => {
101-
allTags.add(tag);
101+
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
102102
});
103103
});
104+
105+
// Sort tags by frequency (descending), then alphabetically for ties
106+
const sortedTags = Object.entries(tagCounts)
107+
.sort(([, countA, tagA], [, countB, tagB]) => {
108+
if (countB !== countA) {
109+
return countB - countA;
110+
}
111+
const tagAStr = String(tagA);
112+
const tagBStr = String(tagB);
113+
return tagAStr.localeCompare(tagBStr);
114+
})
115+
.map(([tag]) => tag);
116+
117+
// Start with the 'All' button
118+
let tagsHTML = '<button class="tag-filter active" data-tag="all">All</button>';
104119

105-
// Sort tags alphabetically
106-
const sortedTags = Array.from(allTags).sort();
107-
108-
// Create tag filter buttons
109-
tagFilterContainer.innerHTML = '<button class="tag-filter active" data-tag="all">All</button>';
120+
// Add a wrapper for all other tags
121+
tagsHTML += '<div class="all-tags-wrapper">';
110122
sortedTags.forEach(tag => {
111123
const tagSlug = tag.toLowerCase().replace(/\s+/g, '-');
112-
tagFilterContainer.innerHTML += `<button class="tag-filter" data-tag="${tagSlug}">${tag}</button>`;
124+
tagsHTML += `<button class="tag-filter" data-tag="${tagSlug}">${tag}</button>`;
113125
});
126+
tagsHTML += '</div>'; // Close wrapper
127+
128+
// Add "See more" button after the wrapper, initially hidden
129+
tagsHTML += '<button class="see-more-tags" data-state="more" style="display: none;">See more</button>';
130+
131+
tagFilterContainer.innerHTML = tagsHTML;
132+
133+
// Adjust visibility after tags are rendered
134+
// Use setTimeout to ensure layout calculation happens after render
135+
setTimeout(adjustVisibleTags, 0);
114136
}
115137

138+
/**
139+
* Adjusts the visibility of tags based on available width, showing only two rows initially.
140+
*/
141+
function adjustVisibleTags() {
142+
const tagFilterContainer = document.querySelector('.blog-filter-tags');
143+
if (!tagFilterContainer) return;
144+
145+
const tagsWrapper = tagFilterContainer.querySelector('.all-tags-wrapper');
146+
const seeMoreButton = tagFilterContainer.querySelector('.see-more-tags');
147+
const allTagButtons = tagsWrapper ? Array.from(tagsWrapper.querySelectorAll('.tag-filter')) : [];
148+
149+
if (!tagsWrapper || !seeMoreButton || allTagButtons.length === 0) {
150+
if (seeMoreButton) seeMoreButton.style.display = 'none'; // Hide if no tags
151+
return;
152+
}
153+
154+
// Reset styles to calculate natural layout
155+
tagsWrapper.style.maxHeight = '';
156+
tagsWrapper.style.overflow = '';
157+
seeMoreButton.style.display = 'none'; // Hide button initially
158+
159+
const firstTag = allTagButtons[0];
160+
const firstTagOffsetTop = firstTag.offsetTop;
161+
let thirdRowOffsetTop = -1;
162+
let secondRowFound = false;
163+
164+
// Find the offsetTop of the start of the third row
165+
for (const tag of allTagButtons) {
166+
if (!secondRowFound && tag.offsetTop > firstTagOffsetTop) {
167+
secondRowFound = true;
168+
}
169+
if (secondRowFound && tag.offsetTop > tag.previousElementSibling?.offsetTop && tag.offsetTop !== firstTagOffsetTop) {
170+
// Check if this tag starts a new row *after* the second row started
171+
const previousTag = tag.previousElementSibling;
172+
if (previousTag && tag.offsetTop > previousTag.offsetTop) {
173+
thirdRowOffsetTop = tag.offsetTop;
174+
break; // Found the start of the third row
175+
}
176+
}
177+
}
178+
179+
// Check if the container itself is taller than the start of the third row (handles wrapping)
180+
const containerScrollHeight = tagsWrapper.scrollHeight;
181+
const containerClientHeight = tagsWrapper.clientHeight; // Height when not constrained
182+
183+
if (thirdRowOffsetTop !== -1 && containerScrollHeight > (thirdRowOffsetTop - firstTagOffsetTop)) {
184+
// Calculate height for two rows. Use the offsetTop of the start of the third row.
185+
const twoRowsHeight = thirdRowOffsetTop - firstTagOffsetTop;
186+
187+
// Apply max-height and overflow
188+
tagsWrapper.style.maxHeight = `${twoRowsHeight - 1}px`; // Subtract 1px to ensure cutoff
189+
tagsWrapper.style.overflow = 'hidden';
190+
seeMoreButton.style.display = 'inline-block'; // Show the button
191+
seeMoreButton.textContent = 'See more';
192+
seeMoreButton.setAttribute('data-state', 'more');
193+
} else {
194+
// All tags fit within two rows or less
195+
tagsWrapper.style.maxHeight = '';
196+
tagsWrapper.style.overflow = '';
197+
seeMoreButton.style.display = 'none'; // Hide the button
198+
}
199+
}
200+
201+
116202
/**
117203
* Sets up event listeners for blog filtering
118204
*/
205+
// Debounce function
206+
function debounce(func, wait) {
207+
let timeout;
208+
return function executedFunction(...args) {
209+
const later = () => {
210+
clearTimeout(timeout);
211+
func(...args);
212+
};
213+
clearTimeout(timeout);
214+
timeout = setTimeout(later, wait);
215+
};
216+
}
217+
218+
let isInitialLoad = true; // Flag to prevent immediate execution on load for resize
219+
119220
function setupBlogFilters() {
120-
// Tag filtering
121-
document.addEventListener('click', function(e) {
221+
const tagFilterContainer = document.querySelector('.blog-filter-tags');
222+
if (!tagFilterContainer) return;
223+
224+
// --- Event Listener for Clicks ---
225+
tagFilterContainer.addEventListener('click', function(e) {
226+
// Handle tag filter clicks (applies to 'All' button and tags inside wrapper)
122227
if (e.target.classList.contains('tag-filter')) {
123228
const selectedTag = e.target.getAttribute('data-tag');
124229

125-
// Update active state on filter buttons
126-
document.querySelectorAll('.tag-filter').forEach(btn => {
230+
// Update active state on ALL filter buttons (incl. 'All')
231+
tagFilterContainer.querySelectorAll('.tag-filter').forEach(btn => {
127232
btn.classList.remove('active');
128233
});
129234
e.target.classList.add('active');
130235

131236
// Filter blog posts
132237
filterBlogPostsByTag(selectedTag);
133238
}
239+
240+
// Handle "See more/less" clicks
241+
if (e.target.classList.contains('see-more-tags')) {
242+
const button = e.target;
243+
const tagsWrapper = tagFilterContainer.querySelector('.all-tags-wrapper');
244+
const currentState = button.getAttribute('data-state');
245+
246+
if (currentState === 'more') {
247+
// Expand: Remove max-height and overflow
248+
tagsWrapper.style.maxHeight = '';
249+
tagsWrapper.style.overflow = '';
250+
button.textContent = 'See less';
251+
button.setAttribute('data-state', 'less');
252+
} else {
253+
// Collapse: Re-apply the two-row limit
254+
adjustVisibleTags(); // Recalculate and apply the correct max-height
255+
button.textContent = 'See more'; // adjustVisibleTags will set this if needed
256+
button.setAttribute('data-state', 'more'); // adjustVisibleTags will set this if needed
257+
258+
// Optional: If active tag is now hidden, scroll it into view or switch to 'All'
259+
const activeTag = tagsWrapper.querySelector('.tag-filter.active');
260+
if (activeTag && tagsWrapper.scrollHeight > tagsWrapper.clientHeight) {
261+
// Check if the active tag is actually hidden (offsetTop > wrapper visible height)
262+
const wrapperRect = tagsWrapper.getBoundingClientRect();
263+
const activeTagRect = activeTag.getBoundingClientRect();
264+
if (activeTagRect.bottom > wrapperRect.bottom) {
265+
// Option 1: Scroll active tag into view (might be slightly off due to overflow hidden)
266+
// activeTag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
267+
268+
// Option 2: Switch back to 'All' filter
269+
const allButton = tagFilterContainer.querySelector('.tag-filter[data-tag="all"]');
270+
if (allButton) {
271+
tagFilterContainer.querySelectorAll('.tag-filter').forEach(btn => btn.classList.remove('active'));
272+
allButton.classList.add('active');
273+
filterBlogPostsByTag('all');
274+
}
275+
}
276+
}
277+
}
278+
}
134279
});
280+
281+
// --- Event Listener for Resize ---
282+
// Debounce the resize handler
283+
const debouncedAdjustTags = debounce(adjustVisibleTags, 250);
284+
285+
window.addEventListener('resize', () => {
286+
// Don't run immediately on load if adjustVisibleTags is already called
287+
if (!isInitialLoad) {
288+
debouncedAdjustTags();
289+
}
290+
isInitialLoad = false; // Set flag after first potential resize event
291+
});
292+
293+
// Reset flag after initial setup allows resize to work
294+
setTimeout(() => { isInitialLoad = false; }, 100);
135295
}
136296

137297
/**

js/blog.js

Lines changed: 0 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ document.addEventListener('DOMContentLoaded', function() {
1515
displayFallbackBlogPosts();
1616
});
1717

18-
// Set up event listeners for blog post filtering
19-
setupBlogFilters();
2018
});
2119

2220
/**
@@ -84,84 +82,9 @@ function displayBlogPosts(posts) {
8482
blogContainer.appendChild(postCard);
8583
});
8684

87-
// Extract all unique tags for filtering from all valid posts, not just the displayed ones
88-
populateTagFilters(validPosts);
8985
}
9086
}
9187

92-
/**
93-
* Populates the tag filter dropdown with unique tags from all posts
94-
* @param {Array} posts - Array of blog post objects
95-
*/
96-
function populateTagFilters(posts) {
97-
const tagFilterContainer = document.querySelector('.blog-filter-tags');
98-
if (!tagFilterContainer) return;
99-
100-
// Extract all unique tags
101-
const allTags = new Set();
102-
posts.forEach(post => {
103-
post.tags.forEach(tag => {
104-
allTags.add(tag);
105-
});
106-
});
107-
108-
// Sort tags alphabetically
109-
const sortedTags = Array.from(allTags).sort();
110-
111-
// Create tag filter buttons
112-
tagFilterContainer.innerHTML = '<button class="tag-filter active" data-tag="all">All</button>';
113-
sortedTags.forEach(tag => {
114-
const tagSlug = tag.toLowerCase().replace(/\s+/g, '-');
115-
tagFilterContainer.innerHTML += `<button class="tag-filter" data-tag="${tagSlug}">${tag}</button>`;
116-
});
117-
}
118-
119-
/**
120-
* Sets up event listeners for blog filtering
121-
*/
122-
function setupBlogFilters() {
123-
// Tag filtering
124-
document.addEventListener('click', function(e) {
125-
if (e.target.classList.contains('tag-filter')) {
126-
const selectedTag = e.target.getAttribute('data-tag');
127-
128-
// Update active state on filter buttons
129-
document.querySelectorAll('.tag-filter').forEach(btn => {
130-
btn.classList.remove('active');
131-
});
132-
e.target.classList.add('active');
133-
134-
// Filter blog posts
135-
filterBlogPostsByTag(selectedTag);
136-
}
137-
});
138-
}
139-
140-
/**
141-
* Filters blog posts by selected tag
142-
* @param {string} tag - Tag to filter by
143-
*/
144-
function filterBlogPostsByTag(tag) {
145-
const blogCards = document.querySelectorAll('.blog-card');
146-
147-
blogCards.forEach(card => {
148-
if (tag === 'all') {
149-
card.style.display = 'block';
150-
} else {
151-
const cardTags = card.querySelectorAll('.blog-tag');
152-
let hasTag = false;
153-
154-
cardTags.forEach(cardTag => {
155-
if (cardTag.getAttribute('data-tag') === tag) {
156-
hasTag = true;
157-
}
158-
});
159-
160-
card.style.display = hasTag ? 'block' : 'none';
161-
}
162-
});
163-
}
164-
16588
// Note: The openBlogPost function has been removed as we now use direct links to blog post pages
16689

16790
/**

0 commit comments

Comments
 (0)