Skip to content

Commit 684056b

Browse files
committed
refactor: consolidate filtering logic into single module
- Create ConferenceFilter as single source of truth - Consolidate scattered filtering logic from 5+ files - Maintain URL parameters + localStorage state management - Add event-driven architecture for component communication - Support badge clicks, multiselect, and search filtering - Include hover effects and toggle behavior for badges
1 parent 9c8c179 commit 684056b

File tree

3 files changed

+317
-27
lines changed

3 files changed

+317
-27
lines changed

_includes/utils.js

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,45 @@
11
function update_filtering(data) {
2-
// Defensive check for data parameter
3-
if (!data || typeof data !== 'object') {
4-
console.error('update_filtering called with invalid data:', data);
5-
return;
6-
}
2+
// Delegate to consolidated ConferenceFilter module
3+
if (window.ConferenceFilter && window.ConferenceFilter.updateFromMultiselect) {
4+
window.ConferenceFilter.updateFromMultiselect(data.subs);
5+
} else {
6+
// Fallback for legacy support
7+
console.warn('ConferenceFilter module not loaded, using legacy filtering');
8+
9+
// Defensive check for data parameter
10+
if (!data || typeof data !== 'object') {
11+
console.error('update_filtering called with invalid data:', data);
12+
return;
13+
}
714

8-
// Ensure required properties exist
9-
if (!data.subs || !Array.isArray(data.subs)) {
10-
console.error('update_filtering: data.subs is not an array:', data.subs);
11-
return;
12-
}
15+
// Ensure required properties exist
16+
if (!data.subs || !Array.isArray(data.subs)) {
17+
console.error('update_filtering: data.subs is not an array:', data.subs);
18+
return;
19+
}
1320

14-
if (!data.all_subs || !Array.isArray(data.all_subs)) {
15-
console.error('update_filtering: data.all_subs is not an array:', data.all_subs);
16-
return;
17-
}
21+
var page_url = window.location.pathname;
22+
store.set('{{site.domain}}-subs', { subs: data.subs, timestamp: new Date().getTime() });
1823

19-
var page_url = window.location.pathname;
20-
store.set('{{site.domain}}-subs', { subs: data.subs, timestamp: new Date().getTime() });
24+
$('.ConfItem').hide();
2125

22-
$('.confItem').hide();
26+
// Loop through selected values in data.subs
27+
for (const s of data.subs) {
28+
// Show elements with class .s-conf (where s is the selected value)
29+
$('.' + s + '-conf').show();
30+
}
2331

24-
// Loop through selected values in data.subs
25-
for (const s of data.subs) {
26-
// Show elements with class .s-conf (where s is the selected value)
27-
$('.' + s + '-conf').show();
28-
}
32+
// Notify CountdownManager about filter changes for lazy loading optimization
33+
if (window.CountdownManager && window.CountdownManager.onFilterUpdate) {
34+
window.CountdownManager.onFilterUpdate();
35+
}
2936

30-
if (data.subs.length === 0 || data.subs.length == data.all_subs.length) {
31-
window.history.pushState('', '', page_url);
32-
} else {
33-
// Join the selected values into a query parameter
34-
window.history.pushState('', '', page_url + '?sub=' + data.subs.join());
37+
if (data.subs.length === 0 || data.subs.length == data.all_subs.length) {
38+
window.history.pushState('', '', page_url);
39+
} else {
40+
// Join the selected values into a query parameter
41+
window.history.pushState('', '', page_url + '?sub=' + data.subs.join());
42+
}
3543
}
3644
}
3745

_layouts/home.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
layout: default
3+
include_footer: true
4+
---
5+
6+
{% capture fp_title %}
7+
{% t titles.default %}
8+
{% endcapture %}
9+
{% capture fp_hook %}
10+
{% tf frontpage/hook.md %}
11+
{% endcapture %}
12+
{% capture fp_description %}
13+
{% tf frontpage/description.md %}
14+
{% endcapture %}
15+
16+
{% include masthead.html url=page.url title=fp_title hook=fp_hook description=fp_description %}
17+
18+
{{ content }}
19+
20+
<!-- Consolidated Conference Filtering -->
21+
<script type="text/javascript" src="{{ "/static/js/conference-filter.js" | prepend:site.baseurl_root }}?t={{site.time | date: '%s'}}"></script>
22+
23+
<!-- Lazy Loading Countdown System -->
24+
<script type="text/javascript" src="{{ "/static/js/countdown-lazy.js" | prepend:site.baseurl_root }}?t={{site.time | date: '%s'}}"></script>
25+
26+
<!-- Lazy Loading for Performance -->
27+
<script type="text/javascript" src="{{ "/static/js/lazy-load.js" | prepend:site.baseurl_root }}?t={{site.time | date: '%s'}}"></script>

static/js/conference-filter.js

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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

Comments
 (0)