diff options
Diffstat (limited to 'pydis_site/static')
| -rw-r--r-- | pydis_site/static/css/collapsibles.css | 11 | ||||
| -rw-r--r-- | pydis_site/static/css/content/page.css | 13 | ||||
| -rw-r--r-- | pydis_site/static/css/resources/resources.css | 261 | ||||
| -rw-r--r-- | pydis_site/static/css/resources/resources_list.css | 55 | ||||
| -rw-r--r-- | pydis_site/static/images/resources/duck_pond_404.jpg | bin | 0 -> 123824 bytes | |||
| -rw-r--r-- | pydis_site/static/js/collapsibles.js | 67 | ||||
| -rw-r--r-- | pydis_site/static/js/content/page.js | 13 | ||||
| -rw-r--r-- | pydis_site/static/js/resources/resources.js | 285 |
8 files changed, 607 insertions, 98 deletions
diff --git a/pydis_site/static/css/collapsibles.css b/pydis_site/static/css/collapsibles.css new file mode 100644 index 00000000..1d73fa00 --- /dev/null +++ b/pydis_site/static/css/collapsibles.css @@ -0,0 +1,11 @@ +.collapsible { + cursor: pointer; + width: 100%; + border: none; + outline: none; +} + +.collapsible-content { + transition: max-height 0.3s ease-out; + overflow: hidden; +} diff --git a/pydis_site/static/css/content/page.css b/pydis_site/static/css/content/page.css index 2d4bd325..d831f86d 100644 --- a/pydis_site/static/css/content/page.css +++ b/pydis_site/static/css/content/page.css @@ -77,16 +77,3 @@ ul.menu-list.toc { li img { margin-top: 0.5em; } - -.collapsible { - cursor: pointer; - width: 100%; - border: none; - outline: none; -} - -.collapsible-content { - overflow: hidden; - max-height: 0; - transition: max-height 0.2s ease-out; -} diff --git a/pydis_site/static/css/resources/resources.css b/pydis_site/static/css/resources/resources.css index cf4cb472..b8456e38 100644 --- a/pydis_site/static/css/resources/resources.css +++ b/pydis_site/static/css/resources/resources.css @@ -1,29 +1,256 @@ -.box, .tile.is-parent { - transition: 0.1s ease-out; +/* Colors for icons */ +i.resource-icon.is-orangered { + color: #FE640A; } -.box { - min-height: 15vh; +i.resource-icon.is-blurple { + color: #7289DA; } -.tile.is-parent:hover .box { - box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); +i.resource-icon.is-teal { + color: #95DBE5; } -.tile.is-parent:hover { - padding: 0.65rem 0.85rem 0.85rem 0.65rem; - filter: saturate(1.1) brightness(1.1); +i.resource-icon.is-youtube-red { + color: #BB0000; +} +i.resource-icon.is-black { + color: #2c3334; +} + +/* Colors when icons are hovered */ +i.resource-icon.is-hoverable:hover { + filter: brightness(125%); +} +i.resource-icon.is-hoverable.is-black:hover { + filter: brightness(170%); +} +i.resource-icon.is-hoverable.is-teal:hover { + filter: brightness(80%); +} + +/* Icon padding */ +.breadcrumb-section { + padding: 1rem; +} +i.has-icon-padding { + padding: 0 10px 25px 0; +} +#tab-content p { + display: none; +} +#tab-content p.is-active { +display: block; +} + +/* Disable highlighting for all text in the filters. */ +.filter-checkbox, +.filter-panel label, +.card-header span { + user-select: none +} + +/* Remove pointless margin in panel header */ +#filter-panel-header { + margin-bottom: 0; +} + +/* Full width filter cards */ +#resource-filtering-panel .card .collapsible-content .card-content { + padding:0 +} + +/* Don't round the corners of the collapsibles */ +.filter-category-header { + border-radius: 0; } -#readingBlock { - background-image: linear-gradient(141deg, #911eb4 0%, #b631de 71%, #cf4bf7 100%); +/* Make the checkboxes indent under the filter headers */ +.filter-category-header .card-header .card-header-title { + padding-left: 0; +} +.filter-panel { + padding-left: 1.5rem; +} +.filter-checkbox { + margin-right: 0.25em !important; +} + +/* Center the 404 div */ +.no-resources-found { + display: none; + flex-direction: column; + align-items: center; + margin-top: 1em; +} + +/* Make sure jQuery will use flex when setting `show()` again. */ +.no-resources-found[style*='display: block'] { + display: flex !important; +} + +/* Disable clicking on the checkbox itself. */ +/* Instead, we want to let the anchor tag handle clicks. */ +.filter-checkbox { + pointer-events: none; +} + +/* Blurple category icons */ +i.is-primary { + color: #7289da; +} + +/* A little space above the filter card, please! */ +.filter-tags { + padding-bottom: .5em; +} + +/* Style the close all filters button */ +.close-filters-button { + margin-left: auto; + display:none; +} +.close-filters-button a { + height: fit-content; + width: fit-content; + margin-right: 6px; +} +.close-filters-button a i { + color: #939bb3; +} +.close-filters-button a i:hover { + filter: brightness(115%); } -#interactiveBlock { - background-image: linear-gradient(141deg, #d05600 0%, #da722a 71%, #e68846 100%); +/* When hovering title anchors, just make the color a lighter primary, not black. */ +.resource-box a:hover { + filter: brightness(120%); + color: #7289DA; +} + + +/* Set default display to inline-flex, for centering. */ +span.filter-box-tag { + display: none; + align-items: center; + cursor: pointer; + user-select: none; +} + +/* Make sure jQuery will use inline-flex when setting `show()` again. */ +span.filter-box-tag[style*='display: block'] { + display: inline-flex !important; +} + +/* Make resource tags clickable */ +.resource-tag { + cursor: pointer; + user-select: none; +} + +/* Give the resource tags a bit of breathing room */ +.resource-tag-container { + padding-left: 1.5rem; +} + +/* When hovering tags, brighten them a bit. */ +.resource-tag:hover, +.filter-box-tag:hover { + filter: brightness(95%); +} + +/* Move the x down 1 pixel to align center */ +button.delete { + margin-top: 1px; +} + +/* Colors for delete button x's */ +button.delete.is-primary::before, +button.delete.is-primary::after { + background-color: #2a45a2; +} +button.delete.is-success::before, +button.delete.is-success::after { + background-color: #2c9659; +} +button.delete.is-danger::before, +button.delete.is-danger::after { + background-color: #c32841; +} +button.delete.is-info::before, +button.delete.is-info::after { + background-color: #237fbd; +} + +/* Give outlines to active tags */ +span.filter-box-tag, +span.resource-tag.active { + outline-width: 1px; + outline-style: solid; +} + +/* Disable transitions */ +.no-transition { + -webkit-transition: none !important; + -moz-transition: none !important; + -o-transition: none !important; + transition: none !important; +} + +/* Make filter tags sparkle when selected! */ +@keyframes glow_success { + from { box-shadow: 0 0 2px 2px #aef4af; } + 33% { box-shadow: 0 0 2px 2px #87af7a; } + 66% { box-shadow: 0 0 2px 2px #9ceaac; } + to { box-shadow: 0 0 2px 2px #7cbf64; } +} + +@keyframes glow_primary { + from { box-shadow: 0 0 2px 2px #aeb8f3; } + 33% { box-shadow: 0 0 2px 2px #909ed9; } + 66% { box-shadow: 0 0 2px 2px #6d7ed4; } + to { box-shadow: 0 0 2px 2px #6383b3; } +} + +@keyframes glow_danger { + from { box-shadow: 0 0 2px 2px #c9495f; } + 33% { box-shadow: 0 0 2px 2px #92486f; } + 66% { box-shadow: 0 0 2px 2px #d455ba; } + to { box-shadow: 0 0 2px 2px #ff8192; } +} +@keyframes glow_info { + from { box-shadow: 0 0 2px 2px #4592c9; } + 33% { box-shadow: 0 0 2px 2px #6196bb; } + 66% { box-shadow: 0 0 2px 2px #5adade; } + to { box-shadow: 0 0 2px 2px #6bcfdc; } +} + +span.resource-tag.active.is-primary { + animation: glow_primary 4s infinite alternate; +} +span.resource-tag.active.has-background-danger-light { + animation: glow_danger 4s infinite alternate; +} +span.resource-tag.active.has-background-success-light { + animation: glow_success 4s infinite alternate; +} +span.resource-tag.active.has-background-info-light { + animation: glow_info 4s infinite alternate; } -#communitiesBlock { - background-image: linear-gradient(141deg, #3b756f 0%, #3a847c 71%, #41948b 100%); +/* Smaller filter category headers when on mobile */ +@media screen and (max-width: 480px) { + .filter-category-header .card-header .card-header-title { + font-size: 14px; + padding: 0; + } + .filter-panel { + padding-top: 4px; + padding-bottom: 4px; + } } -#podcastsBlock { - background-image: linear-gradient(141deg, #232382 0%, #30309c 71%, #4343ad 100%); +/* Constrain the width of the filterbox */ +@media screen and (min-width: 769px) { + .filtering-column { + max-width: 25rem; + min-width: 18rem; + } } diff --git a/pydis_site/static/css/resources/resources_list.css b/pydis_site/static/css/resources/resources_list.css deleted file mode 100644 index 33129c87..00000000 --- a/pydis_site/static/css/resources/resources_list.css +++ /dev/null @@ -1,55 +0,0 @@ -.breadcrumb-section { - padding: 1rem; -} - -i.resource-icon.is-orangered { - color: #FE640A; -} - -i.resource-icon.is-orangered:hover { - color: #fe9840; -} - -i.resource-icon.is-blurple { - color: #7289DA; -} - -i.resource-icon.is-blurple:hover { - color: #93a8da; -} - -i.resource-icon.is-teal { - color: #95DBE5; -} - -i.resource-icon.is-teal:hover { - color: #a9f5ff; -} - -i.resource-icon.is-youtube-red { - color: #BB0000; -} - -i.resource-icon.is-youtube-red:hover { - color: #f80000; -} - -i.resource-icon.is-amazon-orange { - color: #FF9900; -} - -i.resource-icon.is-amazon-orange:hover { - color: #ffb71a; -} - -i.resource-icon.is-black { - color: #000000; -} - -i.resource-icon.is-black { - color: #191919; -} - -i.has-icon-padding { - padding: 0 10px 25px 0; -} diff --git a/pydis_site/static/images/resources/duck_pond_404.jpg b/pydis_site/static/images/resources/duck_pond_404.jpg Binary files differnew file mode 100644 index 00000000..29bcf1d6 --- /dev/null +++ b/pydis_site/static/images/resources/duck_pond_404.jpg diff --git a/pydis_site/static/js/collapsibles.js b/pydis_site/static/js/collapsibles.js new file mode 100644 index 00000000..1df0b9fe --- /dev/null +++ b/pydis_site/static/js/collapsibles.js @@ -0,0 +1,67 @@ +/* +A utility for creating simple collapsible cards. + +To see this in action, go to /resources or /pages/guides/pydis-guides/contributing/bot/ + +// HOW TO USE THIS // +First, import this file and the corresponding css file into your template. + + <link rel="stylesheet" href="{% static "css/collapsibles.css" %}"> + <script defer src="{% static "js/collapsibles.js" %}"></script> + +Next, you'll need some HTML that these scripts can interact with. + +<div class="card"> + <button type="button" class="card-header collapsible"> + <span class="card-header-title subtitle is-6 my-2 ml-2">Your headline</span> + <span class="card-header-icon"> + <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i> + </span> + </button> + <div class="collapsible-content collapsed"> + <div class="card-content"> + You can put anything you want here. Lists, more divs, flexboxes, images, whatever. + </div> + </div> +</div> + +That's it! Collapsing stuff should now work. + */ + +document.addEventListener("DOMContentLoaded", () => { + const contentContainers = document.getElementsByClassName("collapsible-content"); + for (const container of contentContainers) { + // Close any collapsibles that are marked as initially collapsed + if (container.classList.contains("collapsed")) { + container.style.maxHeight = "0px"; + // Set maxHeight to the size of the container on all other containers. + } else { + container.style.maxHeight = container.scrollHeight + "px"; + } + } + + // Listen for click events, and collapse or explode + const headers = document.getElementsByClassName("collapsible"); + for (const header of headers) { + const content = header.nextElementSibling; + const icon = header.querySelector(".card-header-icon i"); + + // Any collapsibles that are not initially collapsed needs an icon switch. + if (!content.classList.contains("collapsed")) { + icon.classList.remove("fas", "fa-angle-down"); + icon.classList.add("far", "fa-window-minimize"); + } + + header.addEventListener("click", () => { + if (content.style.maxHeight !== "0px"){ + content.style.maxHeight = "0px"; + icon.classList.remove("far", "fa-window-minimize"); + icon.classList.add("fas", "fa-angle-down"); + } else { + content.style.maxHeight = content.scrollHeight + "px"; + icon.classList.remove("fas", "fa-angle-down"); + icon.classList.add("far", "fa-window-minimize"); + } + }); + } +}); diff --git a/pydis_site/static/js/content/page.js b/pydis_site/static/js/content/page.js deleted file mode 100644 index 366a033c..00000000 --- a/pydis_site/static/js/content/page.js +++ /dev/null @@ -1,13 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { - const headers = document.getElementsByClassName("collapsible"); - for (const header of headers) { - header.addEventListener("click", () => { - var content = header.nextElementSibling; - if (content.style.maxHeight){ - content.style.maxHeight = null; - } else { - content.style.maxHeight = content.scrollHeight + "px"; - } - }); - } -}); diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js new file mode 100644 index 00000000..508849e1 --- /dev/null +++ b/pydis_site/static/js/resources/resources.js @@ -0,0 +1,285 @@ +"use strict"; + +// Filters that are currently selected +var activeFilters = { + topics: [], + type: [], + "payment-tiers": [], + difficulty: [] +}; + +/* 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); + + // 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) { + // Make sure the filter is valid before we do anything. + 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"; + } 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() { + // If there's nothing in the filters, we don't want anything in the URL. + if (noFilters()) { + 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); + } + }); + + // Now update the URL + window.history.replaceState(null, document.title, `?${searchParams.toString()}`); +} + +/* 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"); + + // Update the URL to match the new filters. + updateURL(); + + // If there's nothing in the filters, we can return early. + if (noFilters()) { + resources.show(); + filterTags.hide(); + noTagsSelected.show(); + 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(); + 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; + } + }).show(); + + + // 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); + } + + // 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); + } + }); +}); |