1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
|
"use strict";
// Filters that are currently selected
var activeFilters = {
topics: [],
type: [],
"payment-tiers": [],
difficulty: []
};
// Options for fuzzysort
const fuzzysortOptions = {
allowTypo: true, // Allow our users to make typos
threshold: -10000, // The threshold for the fuzziness. Adjust for precision.
};
/* Add a filter, and update the UI */
function addFilter(filterName, filterItem) {
var filterIndex = activeFilters[filterName].indexOf(filterItem);
if (filterIndex === -1) {
activeFilters[filterName].push(filterItem);
}
updateUI();
}
/* Remove all filters, and update the UI */
function removeAllFilters() {
activeFilters = {
topics: [],
type: [],
"payment-tiers": [],
difficulty: []
};
updateUI();
}
/* Remove a filter, and update the UI */
function removeFilter(filterName, filterItem) {
var filterIndex = activeFilters[filterName].indexOf(filterItem);
if (filterIndex !== -1) {
activeFilters[filterName].splice(filterIndex, 1);
}
updateUI();
}
/* Check if there are no filters */
function noFilters() {
return (
activeFilters.topics.length === 0 &&
activeFilters.type.length === 0 &&
activeFilters["payment-tiers"].length === 0 &&
activeFilters.difficulty.length === 0
);
}
/* Get the params out of the URL and use them. This is run when the page loads. */
function deserializeURLParams() {
let searchParams = new window.URLSearchParams(window.location.search);
// Add the search query to the search bar.
if (searchParams.has("search")) {
let searchQuery = searchParams.get("search");
$("#resource-search input").val(searchQuery);
}
// Work through the parameters and add them to the filter object
$.each(Object.keys(activeFilters), function(_, filterType) {
let paramFilterContent = searchParams.get(filterType);
if (paramFilterContent !== null) {
// We use split here because we always want an array, not a string.
let paramFilterArray = paramFilterContent.split(",");
// Update the corresponding filter UI, so it reflects the internal state.
let filterAdded = false;
$(paramFilterArray).each(function(_, filter) {
// Catch special cases.
if (String(filter) === "rickroll" && filterType === "type") {
window.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
} else if (String(filter) === "sneakers" && filterType === "topics") {
window.location.href = "https://www.youtube.com/watch?v=NNZscmNE9QI";
// If the filter is valid, mirror it to the UI.
} else if (validFilters.hasOwnProperty(filterType) && validFilters[filterType].includes(String(filter))) {
let checkbox = $(`.filter-checkbox[data-filter-name='${filterType}'][data-filter-item='${filter}']`);
let filterTag = $(`.filter-box-tag[data-filter-name='${filterType}'][data-filter-item='${filter}']`);
let resourceTags = $(`.resource-tag[data-filter-name='${filterType}'][data-filter-item='${filter}']`);
checkbox.prop("checked", true);
filterTag.show();
resourceTags.addClass("active");
activeFilters[filterType].push(filter);
filterAdded = true;
}
});
// Ditch all the params from the URL, and recalculate the URL params
updateURL();
// If we've added a filter, hide stuff
if (filterAdded) {
$(".no-tags-selected.tag").hide();
$(".close-filters-button").show();
}
}
});
}
/* Update the URL with new parameters */
function updateURL() {
let searchQuery = $("#resource-search input").val();
// If there's no active filtering parameters, we can return early.
if (noFilters() && searchQuery.length === 0) {
window.history.replaceState(null, document.title, './');
return;
}
// Iterate through and get rid of empty ones
let searchParams = new URLSearchParams(activeFilters);
$.each(activeFilters, function(filterType, filters) {
if (filters.length === 0) {
searchParams.delete(filterType);
}
});
// Add the search query, if necessary.
if (searchQuery.length > 0) {
searchParams.set("search", searchQuery);
}
// Now update the URL
window.history.replaceState(null, document.title, `?${searchParams.toString()}`);
}
/* Apply search terms */
function filterBySearch(resourceItems) {
let searchQuery = $("#resource-search input").val();
/* Show and update the tag if there's a search query */
if (searchQuery) {
let tag = $(".tag.search-query");
let tagText = $(".tag.search-query span");
tagText.text(`Search: ${searchQuery}`);
tag.show();
}
resourceItems.filter(function() {
// Run a fuzzy search over the item. Does the search query match?
let name = $(this).attr("name");
let result = fuzzysort.single(searchQuery, name, fuzzysortOptions);
return Boolean(result) && result.score > fuzzysortOptions.threshold;
}).show();
}
/* Update the resources to match 'active_filters' */
function updateUI() {
let resources = $('.resource-box');
let filterTags = $('.filter-box-tag');
let resourceTags = $('.resource-tag');
let noTagsSelected = $(".no-tags-selected.tag");
let closeFiltersButton = $(".close-filters-button");
let searchQuery = $("#resource-search input").val();
let searchTag = $(".tag.search-query");
// Update the URL to match the new filters.
updateURL();
// If there's nothing in the filters, we can return early.
if (noFilters()) {
// If we have a searchQuery, we need to run all resources through a search.
if (searchQuery.length > 0) {
resources.hide();
noTagsSelected.hide();
filterBySearch(resources);
} else {
resources.show();
noTagsSelected.show();
$(".tag.search-query").hide();
}
filterTags.hide();
closeFiltersButton.hide();
resourceTags.removeClass("active");
$(`.filter-checkbox:checked`).prop("checked", false);
$(".no-resources-found").hide();
return;
} else {
// Hide everything
$('.filter-box-tag').hide();
$('.resource-tag').removeClass("active");
noTagsSelected.show();
closeFiltersButton.hide();
// Now conditionally show the stuff we want
$.each(activeFilters, function(filterType, filters) {
$.each(filters, function(index, filter) {
// Show a corresponding filter box tag
$(`.filter-box-tag[data-filter-name=${filterType}][data-filter-item=${filter}]`).show();
// Make corresponding resource tags active
$(`.resource-tag[data-filter-name=${filterType}][data-filter-item=${filter}]`).addClass("active");
// Hide the "No filters selected" tag.
noTagsSelected.hide();
// Show the close filters button
closeFiltersButton.show();
});
});
}
// Otherwise, hide everything and then filter the resources to decide what to show.
let hasMatches = false;
resources.hide();
let filteredResources = resources.filter(function() {
let validation = {
topics: false,
type: false,
'payment-tiers': false,
difficulty: false
};
let resourceBox = $(this);
// Validate the filters
$.each(activeFilters, function(filterType, filters) {
// If the filter list is empty, this passes validation.
if (filters.length === 0) {
validation[filterType] = true;
return;
}
// Otherwise, we need to check if one of the classes exist.
$.each(filters, function(index, filter) {
if (resourceBox.hasClass(`${filterType}-${filter}`)) {
validation[filterType] = true;
}
});
});
// If validation passes, show the resource.
if (Object.values(validation).every(Boolean)) {
hasMatches = true;
return true;
} else {
return false;
}
});
// Run the items we've found through the search filter, if necessary.
if (searchQuery.length > 0) {
filterBySearch(filteredResources);
} else {
filteredResources.show();
searchTag.hide();
}
// If there are no matches, show the no matches message
if (!hasMatches) {
$(".no-resources-found").show();
} else {
$(".no-resources-found").hide();
}
}
// Executed when the page has finished loading.
document.addEventListener("DOMContentLoaded", function () {
/* Check if the user has navigated to one of the old resource pages,
like pydis.com/resources/communities. In this case, we'll rewrite
the URL before we do anything else. */
let resourceTypeInput = $("#resource-type-input").val();
if (resourceTypeInput !== "None") {
window.history.replaceState(null, document.title, `../?type=${resourceTypeInput}`);
}
// Update the filters on page load to reflect URL parameters.
$('.filter-box-tag').hide();
deserializeURLParams();
updateUI();
// If this is a mobile device, collapse all the categories to win back some screen real estate.
if (screen.width < 480) {
let categoryHeaders = $(".filter-category-header .collapsible-content");
let icons = $('.filter-category-header button .card-header-icon i');
categoryHeaders.addClass("no-transition collapsed");
icons.removeClass(["far", "fa-window-minimize"]);
icons.addClass(["fas", "fa-angle-down"]);
// Wait 10ms before removing this class, or else the transition will animate due to a race condition.
setTimeout(() => { categoryHeaders.removeClass("no-transition"); }, 10);
}
// When you type into the search bar, trigger an UI update.
$("#resource-search input").on("input", function() {
updateUI();
});
// If you click on the div surrounding the filter checkbox, it clicks the corresponding checkbox.
$('.filter-panel').on("click",function(event) {
let hitsCheckbox = Boolean(String(event.target));
if (!hitsCheckbox) {
let checkbox = $(this).find(".filter-checkbox");
checkbox.prop("checked", !checkbox.prop("checked"));
checkbox.trigger("change");
}
});
// If you click on one of the tags in the filter box, it unchecks the corresponding checkbox.
$('.filter-box-tag').on("click", function() {
let filterItem = this.dataset.filterItem;
let filterName = this.dataset.filterName;
let checkbox = $(`.filter-checkbox[data-filter-name='${filterName}'][data-filter-item='${filterItem}']`);
removeFilter(filterName, filterItem);
checkbox.prop("checked", false);
});
// If you click on one of the tags in the resource cards, it clicks the corresponding checkbox.
$('.resource-tag').on("click", function() {
let filterItem = this.dataset.filterItem;
let filterName = this.dataset.filterName;
let checkbox = $(`.filter-checkbox[data-filter-name='${filterName}'][data-filter-item='${filterItem}']`);
if (!$(this).hasClass("active")) {
addFilter(filterName, filterItem);
checkbox.prop("checked", true);
} else {
removeFilter(filterName, filterItem);
checkbox.prop("checked", false);
}
});
// When you click the little gray x, remove all filters.
$(".close-filters-button").on("click", function() {
removeAllFilters();
});
// When checkboxes are toggled, trigger a filter update.
$('.filter-checkbox').on("change", function (event) {
let filterItem = this.dataset.filterItem;
let filterName = this.dataset.filterName;
if (this.checked && !activeFilters[filterName].includes(filterItem)) {
addFilter(filterName, filterItem);
} else if (!this.checked && activeFilters[filterName].includes(filterItem)) {
removeFilter(filterName, filterItem);
}
});
});
|