diff options
Diffstat (limited to 'pydis_site/static/js/resources/resources.js')
-rw-r--r-- | pydis_site/static/js/resources/resources.js | 367 |
1 files changed, 367 insertions, 0 deletions
diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js new file mode 100644 index 00000000..d6cc8128 --- /dev/null +++ b/pydis_site/static/js/resources/resources.js @@ -0,0 +1,367 @@ +"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 + titleThreshold: -10000, // The threshold for the fuzziness on title matches. Closer to 0 is stricter. + descriptionThreshold: -500, // The threshold for the fuzziness on description matches. +}; + +/* 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: [] + }; + $("#resource-search input").val(""); + 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); + $(".close-filters-button").show(); + } + + // 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(); + } + } + }); +} + +/* Show or hide the duckies, depending on whether or not there are any resources visible. */ +function updateDuckies() { + let visibleResources = Boolean($(".resource-box:visible").length); + if (!visibleResources) { + $(".no-resources-found").show(); + } else { + $(".no-resources-found").hide(); + } +} + + +/* 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(); + $(".close-filters-button").show(); + } + + resourceItems.filter(function() { + // Get the resource title and description + let title = $(this).attr("data-resource-name"); + let description = $(this).find("p").text(); + + // Run a fuzzy search. Does the title or description match the query? + let titleMatch = fuzzysort.single(searchQuery, title, fuzzysortOptions); + titleMatch = Boolean(titleMatch) && titleMatch.score > fuzzysortOptions.titleThreshold; + + let descriptionMatch = fuzzysort.single(searchQuery, description, fuzzysortOptions); + descriptionMatch = Boolean(descriptionMatch) && descriptionMatch.score > fuzzysortOptions.descriptionThreshold; + + return titleMatch || descriptionMatch; + }).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(); + closeFiltersButton.hide(); + $(".tag.search-query").hide(); + } + + filterTags.hide(); + resourceTags.removeClass("active"); + $(`.filter-checkbox:checked`).prop("checked", false); + updateDuckies(); + + 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. + 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)) { + 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(); + } + + // Gotta update those duckies! + updateDuckies(); +} + +// 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); + } + }); +}); |