PageSourceSearch

https://www.pythondiscord.com/static/js/resources/resources.js

js pythondiscord.com collected 2026-10-02 05:04:30 UTC 13,509 bytes, 367 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.