@@ -178,68 +178,115 @@ <h2 class="project-group-title">{{ category.title }}</h2>
178178 {%- endfor -%}
179179 </ div >
180180
181- < script >
182- // Put projects with badge key "recently_added" first (per category list)
183- function hasRecentlyAddedBadge ( li ) {
184- let badges = [ ] ;
181+ < script >
182+ ( function ( ) {
183+ let isSorting = false ;
184+ let observer = null ;
185+ let debounceTimer = null ;
186+
187+ function safeParseBadges ( raw ) {
188+ raw = raw == null ? '' : String ( raw ) . trim ( ) ;
189+ if ( ! raw ) return [ ] ;
185190 try {
186- badges = JSON . parse ( li . dataset . badges || '[]' ) ;
187- } catch ( e ) {
188- badges = [ ] ;
189- }
190- return Array . isArray ( badges ) && badges . includes ( 'recently_added' ) ;
191+ const j = JSON . parse ( raw ) ;
192+ if ( Array . isArray ( j ) ) return j . map ( x => String ( x ) . trim ( ) ) . filter ( Boolean ) ;
193+ } catch ( e ) { }
194+ // comma-separated fallback
195+ const cleaned = raw . replace ( / ^ [ \[ \] ' " ] + | [ \[ \] ' " ] + $ / g, '' ) ;
196+ return cleaned . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ;
197+ }
198+
199+ function badgesFromDOM ( li ) {
200+ const badgesEl = li . querySelector ( '.project-badges' ) ;
201+ if ( ! badgesEl ) return [ ] ;
202+ // collect visible badge labels
203+ return Array . from ( badgesEl . querySelectorAll ( '*' ) )
204+ . map ( n => ( n . textContent || '' ) . trim ( ) )
205+ . filter ( Boolean ) ;
206+ }
207+
208+ function normalize ( arr ) {
209+ return ( arr || [ ] ) . map ( x => String ( x ) . trim ( ) . toLowerCase ( ) ) ;
210+ }
211+
212+ function badgeRank ( normed ) {
213+ const isTrending = normed . includes ( 'trending' ) ;
214+ const isRecent =
215+ normed . includes ( 'recently_added' ) ||
216+ normed . includes ( 'recently-added' ) ||
217+ normed . includes ( 'recentlyadded' ) ;
218+
219+ if ( isTrending && isRecent ) return 0 ; // Trending + Recently added
220+ if ( isRecent ) return 1 ; // Recently added
221+ if ( isTrending ) return 2 ; // Trending
222+ return 3 ; // Neither
191223 }
192224
193- function sortProjectsRecentlyAddedFirst ( ) {
225+ function getBadgesForItem ( li ) {
226+ const raw = li . getAttribute ( 'data-badges' ) || li . dataset . badges || '' ;
227+ let parsed = safeParseBadges ( raw ) ;
228+ if ( parsed . length === 0 ) parsed = badgesFromDOM ( li ) ;
229+ return normalize ( parsed ) ;
230+ }
231+
232+ function sortProjectLists ( ) {
233+ if ( isSorting ) return ;
234+ isSorting = true ;
235+
236+ // Prevent observer loop while we move nodes
237+ if ( observer ) observer . disconnect ( ) ;
238+
194239 document . querySelectorAll ( '.project-list' ) . forEach ( ul => {
195240 const items = Array . from ( ul . querySelectorAll ( ':scope > .project-item' ) ) ;
196241
197- // stable sort: decorate with original index
198- const decorated = items . map ( ( el , idx ) => ( {
199- el,
200- idx,
201- isRecent : hasRecentlyAddedBadge ( el )
202- } ) ) ;
203-
204- decorated . sort ( ( a , b ) => {
205- if ( a . isRecent !== b . isRecent ) return a . isRecent ? - 1 : 1 ; // recent first
206- return a . idx - b . idx ; // preserve original order otherwise
242+ const decorated = items . map ( ( el , idx ) => {
243+ const badges = getBadgesForItem ( el ) ;
244+ return { el, idx, rank : badgeRank ( badges ) } ;
207245 } ) ;
208246
247+ decorated . sort ( ( a , b ) => ( a . rank - b . rank ) || ( a . idx - b . idx ) ) ;
209248 decorated . forEach ( d => ul . appendChild ( d . el ) ) ;
210249 } ) ;
250+
251+ // Reconnect observer after we're done moving nodes
252+ startObserving ( ) ;
253+
254+ isSorting = false ;
211255 }
212256
213- document . addEventListener ( 'DOMContentLoaded' , ( ) => {
214- sortProjectsRecentlyAddedFirst ( ) ;
215- } ) ;
257+ function scheduleSort ( ) {
258+ if ( debounceTimer ) clearTimeout ( debounceTimer ) ;
259+ debounceTimer = setTimeout ( sortProjectLists , 100 ) ;
260+ }
216261
217- // --- Existing filtering (unchanged) ---
218- function filterProjects ( ) {
219- const checks = Array . from ( document . querySelectorAll ( '.filter-checkbox:checked' ) ) ;
220- if ( checks . length === 0 ) {
221- document . querySelectorAll ( '.project-item' ) . forEach ( el => el . style . display = '' ) ;
222- return ;
223- }
224- const activeFilters = checks . map ( cb => ( { cat : cb . dataset . category , val : cb . value } ) ) ;
225- document . querySelectorAll ( '.project-item' ) . forEach ( li => {
226- const plats = JSON . parse ( li . dataset . platforms || '[]' ) ;
227- const subs = JSON . parse ( li . dataset . subjects || '[]' ) ;
228- const swhws = JSON . parse ( li . dataset . swhw || '[]' ) ;
229- const levels = JSON . parse ( li . dataset . supportLevel || '[]' ) ;
230- const ok = activeFilters . every ( f => {
231- switch ( f . cat ) {
232- case 'platform' : return plats . includes ( f . val ) ;
233- case 'subject' : return subs . includes ( f . val ) ;
234- case 'sw-hw' : return swhws . includes ( f . val ) ;
235- case 'support-level' : return levels . includes ( f . val ) ;
236- default : return true ;
237- }
262+ function startObserving ( ) {
263+ const container = document . querySelector ( '.project-results' ) ;
264+ if ( ! container ) return ;
265+
266+ if ( ! observer ) {
267+ observer = new MutationObserver ( ( ) => {
268+ // Only schedule if we aren't already sorting
269+ if ( ! isSorting ) scheduleSort ( ) ;
238270 } ) ;
239- li . style . display = ok ? '' : 'none' ;
240- } ) ;
271+ }
272+
273+ // IMPORTANT: only observe childList changes, not attributes on the whole subtree.
274+ // Observing attributes widely can cause lots of extra triggers.
275+ observer . observe ( container , { childList : true , subtree : true } ) ;
241276 }
277+
278+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
279+ sortProjectLists ( ) ;
280+ // Optional second pass for late-rendered badges
281+ setTimeout ( sortProjectLists , 300 ) ;
282+ } ) ;
283+
284+ // Expose for manual debugging
285+ window . __arm_sortProjectLists = sortProjectLists ;
286+ } ) ( ) ;
242287</ script >
288+
289+
243290 {%- endif -%}
244291 </ div >
245292
@@ -253,3 +300,6 @@ <h2 class="project-group-title">{{ category.title }}</h2>
253300</ div >
254301
255302
303+
304+
305+
0 commit comments