1+ // Consolidated Conference Filtering Module
2+ // Single source of truth for all filtering operations
3+ ( function ( ) {
4+ 'use strict' ;
5+
6+ const FilterManager = {
7+ // Configuration
8+ STORAGE_KEY : 'pythondeadlines-filter-state' ,
9+ STORAGE_DOMAIN : window . location . hostname ,
10+
11+ // State
12+ currentFilters : {
13+ subs : [ ] ,
14+ searchQuery : '' ,
15+ dateRange : null
16+ } ,
17+
18+ allSubs : [ ] ,
19+
20+ // Initialize the filter manager
21+ init : function ( ) {
22+ this . loadState ( ) ;
23+ this . bindEvents ( ) ;
24+ this . applyInitialFilters ( ) ;
25+ } ,
26+
27+ // Load filter state from localStorage and URL
28+ loadState : function ( ) {
29+ // Get all available subcategories
30+ this . allSubs = [ ] ;
31+ $ ( '#subject-select option' ) . each ( ( i , opt ) => {
32+ if ( opt . value ) this . allSubs . push ( opt . value ) ;
33+ } ) ;
34+
35+ // Check URL parameters first (highest priority)
36+ const urlParams = new URLSearchParams ( window . location . search ) ;
37+ const urlSubs = urlParams . get ( 'sub' ) ;
38+
39+ if ( urlSubs ) {
40+ this . currentFilters . subs = urlSubs . split ( ',' ) . map ( s => s . trim ( ) ) ;
41+ } else {
42+ // Fall back to localStorage
43+ const stored = store . get ( this . STORAGE_DOMAIN + '-subs' ) ;
44+ if ( stored && ! this . isDataExpired ( stored ) ) {
45+ this . currentFilters . subs = stored . subs || [ ] ;
46+ } else {
47+ // Default to all categories
48+ this . currentFilters . subs = [ ...this . allSubs ] ;
49+ }
50+ }
51+ } ,
52+
53+ // Save filter state
54+ saveState : function ( ) {
55+ store . set ( this . STORAGE_DOMAIN + '-subs' , {
56+ subs : this . currentFilters . subs ,
57+ timestamp : new Date ( ) . getTime ( )
58+ } ) ;
59+
60+ this . updateURL ( ) ;
61+ } ,
62+
63+ // Update URL with current filters
64+ updateURL : function ( ) {
65+ const page_url = window . location . pathname ;
66+
67+ if ( this . currentFilters . subs . length === 0 ||
68+ this . currentFilters . subs . length === this . allSubs . length ) {
69+ // Show all - remove query parameter
70+ window . history . pushState ( '' , '' , page_url ) ;
71+ } else {
72+ // Show filtered - add query parameter
73+ window . history . pushState ( '' , '' ,
74+ page_url + '?sub=' + this . currentFilters . subs . join ( ',' ) ) ;
75+ }
76+ } ,
77+
78+ // Apply filters to conference cards
79+ applyFilters : function ( ) {
80+ // Hide all conferences first
81+ $ ( '.ConfItem' ) . hide ( ) ;
82+
83+ // Show filtered conferences
84+ if ( this . currentFilters . subs . length === 0 ||
85+ this . currentFilters . subs . length === this . allSubs . length ) {
86+ // Show all
87+ $ ( '.ConfItem' ) . show ( ) ;
88+ } else {
89+ // Show only selected subcategories
90+ this . currentFilters . subs . forEach ( sub => {
91+ $ ( '.' + sub + '-conf' ) . show ( ) ;
92+ } ) ;
93+ }
94+
95+ // Apply search filter if present
96+ if ( this . currentFilters . searchQuery ) {
97+ this . applySearchFilter ( ) ;
98+ }
99+
100+ // Notify other systems about filter change
101+ this . notifyFilterChange ( ) ;
102+ } ,
103+
104+ // Apply search filter on top of category filters
105+ applySearchFilter : function ( ) {
106+ const query = this . currentFilters . searchQuery . toLowerCase ( ) ;
107+
108+ $ ( '.ConfItem:visible' ) . each ( function ( ) {
109+ const $item = $ ( this ) ;
110+ const text = $item . text ( ) . toLowerCase ( ) ;
111+
112+ if ( ! text . includes ( query ) ) {
113+ $item . hide ( ) ;
114+ }
115+ } ) ;
116+ } ,
117+
118+ // Notify other components about filter changes
119+ notifyFilterChange : function ( ) {
120+ // Notify countdown manager for lazy loading optimization
121+ if ( window . CountdownManager && window . CountdownManager . onFilterUpdate ) {
122+ window . CountdownManager . onFilterUpdate ( ) ;
123+ }
124+
125+ // Trigger custom event for other components
126+ $ ( document ) . trigger ( 'conference-filter-change' , [ this . currentFilters ] ) ;
127+ } ,
128+
129+ // Update filters from multiselect dropdown
130+ updateFromMultiselect : function ( selectedValues ) {
131+ this . currentFilters . subs = selectedValues || [ ] ;
132+ this . saveState ( ) ;
133+ this . applyFilters ( ) ;
134+ } ,
135+
136+ // Filter by single subcategory (from badge click)
137+ filterBySub : function ( sub ) {
138+ // Check if this is the only selected item - if so, select all (toggle behavior)
139+ const $select = $ ( '#subject-select' ) ;
140+ const currentlySelected = $select . val ( ) || [ ] ;
141+
142+ if ( currentlySelected . length === 1 && currentlySelected [ 0 ] === sub ) {
143+ // If clicking the same single selected item, select all
144+ this . clearFilters ( ) ;
145+ } else {
146+ // Otherwise filter by this subcategory only
147+ this . currentFilters . subs = [ sub ] ;
148+
149+ // Update multiselect UI
150+ $select . multiselect ( 'deselectAll' , false ) ;
151+ $select . multiselect ( 'select' , sub ) ;
152+ $select . multiselect ( 'refresh' ) ;
153+
154+ this . saveState ( ) ;
155+ this . applyFilters ( ) ;
156+ }
157+ } ,
158+
159+ // Search conferences
160+ search : function ( query ) {
161+ this . currentFilters . searchQuery = query ;
162+ this . applyFilters ( ) ;
163+ } ,
164+
165+ // Clear all filters
166+ clearFilters : function ( ) {
167+ this . currentFilters . subs = [ ...this . allSubs ] ;
168+ this . currentFilters . searchQuery = '' ;
169+
170+ // Update multiselect UI
171+ const $select = $ ( '#subject-select' ) ;
172+ $select . multiselect ( 'selectAll' , false ) ;
173+ $select . multiselect ( 'refresh' ) ;
174+
175+ this . saveState ( ) ;
176+ this . applyFilters ( ) ;
177+ } ,
178+
179+ // Check if stored data is expired
180+ isDataExpired : function ( data ) {
181+ const EXPIRATION_PERIOD = 24 * 60 * 60 * 1000 ; // 1 day
182+ const now = new Date ( ) . getTime ( ) ;
183+ return data . timestamp && ( now - data . timestamp > EXPIRATION_PERIOD ) ;
184+ } ,
185+
186+ // Bind events
187+ bindEvents : function ( ) {
188+ const self = this ;
189+
190+ // Handle multiselect changes
191+ $ ( document ) . on ( 'change' , '#subject-select' , function ( ) {
192+ const selected = $ ( this ) . val ( ) || [ ] ;
193+ self . updateFromMultiselect ( selected ) ;
194+ } ) ;
195+
196+ // Handle badge clicks
197+ $ ( document ) . on ( 'click' , '.conf-sub' , function ( e ) {
198+ e . preventDefault ( ) ;
199+ e . stopPropagation ( ) ;
200+ const sub = $ ( this ) . data ( 'sub' ) ;
201+ if ( sub ) {
202+ self . filterBySub ( sub ) ;
203+ }
204+ } ) ;
205+
206+ // Add hover effects to indicate clickability
207+ $ ( document ) . on ( 'mouseenter' , '.conf-sub' , function ( ) {
208+ $ ( this ) . css ( 'opacity' , '0.8' ) ;
209+ } ) ;
210+
211+ $ ( document ) . on ( 'mouseleave' , '.conf-sub' , function ( ) {
212+ $ ( this ) . css ( 'opacity' , '1' ) ;
213+ } ) ;
214+
215+ // Handle browser back/forward
216+ window . addEventListener ( 'popstate' , function ( ) {
217+ self . loadState ( ) ;
218+ self . applyInitialFilters ( ) ;
219+ } ) ;
220+ } ,
221+
222+ // Apply initial filters on page load
223+ applyInitialFilters : function ( ) {
224+ // Update multiselect to match current state
225+ const $select = $ ( '#subject-select' ) ;
226+ if ( $select . length ) {
227+ $select . multiselect ( 'deselectAll' , false ) ;
228+ this . currentFilters . subs . forEach ( sub => {
229+ $select . multiselect ( 'select' , sub ) ;
230+ } ) ;
231+ $select . multiselect ( 'refresh' ) ;
232+ }
233+
234+ // Apply filters
235+ this . applyFilters ( ) ;
236+ }
237+ } ;
238+
239+ // Public API
240+ window . ConferenceFilter = {
241+ init : ( ) => FilterManager . init ( ) ,
242+ filterBySub : ( sub ) => FilterManager . filterBySub ( sub ) ,
243+ search : ( query ) => FilterManager . search ( query ) ,
244+ clearFilters : ( ) => FilterManager . clearFilters ( ) ,
245+ getCurrentFilters : ( ) => FilterManager . currentFilters ,
246+ updateFromMultiselect : ( values ) => FilterManager . updateFromMultiselect ( values )
247+ } ;
248+
249+ // Auto-initialize when DOM is ready
250+ if ( document . readyState === 'loading' ) {
251+ document . addEventListener ( 'DOMContentLoaded' , ( ) => FilterManager . init ( ) ) ;
252+ } else {
253+ FilterManager . init ( ) ;
254+ }
255+ } ) ( ) ;
0 commit comments