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.jpgBinary files differ new 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); +        } +    }); +}); | 
