@@ -93,45 +93,205 @@ function displayBlogPosts(posts) {
9393function 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+
119220function 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/**
0 commit comments