diff options
Diffstat (limited to '')
| -rw-r--r-- | pydis_site/apps/resources/views/resources.py | 125 | ||||
| -rw-r--r-- | pydis_site/static/css/resources/resources.css | 48 | ||||
| -rw-r--r-- | pydis_site/static/js/resources.js | 178 | ||||
| -rw-r--r-- | pydis_site/templates/resources/resources.html | 192 | 
4 files changed, 336 insertions, 207 deletions
diff --git a/pydis_site/apps/resources/views/resources.py b/pydis_site/apps/resources/views/resources.py index de6b2dac..57cb4f71 100644 --- a/pydis_site/apps/resources/views/resources.py +++ b/pydis_site/apps/resources/views/resources.py @@ -1,40 +1,103 @@ +from pathlib import Path + +import yaml +from django.core.handlers.wsgi import WSGIRequest  from django.http import HttpRequest, HttpResponse  from django.shortcuts import render +from django.views import View -from pydis_site.apps.resources.resource_search import RESOURCE_TABLE, get_resources_from_search +from pydis_site import settings -RESOURCE_META_TAGS = {k: set(v) for k, v in RESOURCE_TABLE.items()} +RESOURCES_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "resources", "resources") -def _parse_checkbox_options(options: str) -> set[str]: -    """Split up the comma separated query parameters for checkbox options into a list.""" -    return set(options.split(",")[:-1]) +class ResourceView(View): +    """Our curated list of good learning resources.""" +    def __init__(self, *args, **kwargs): +        """Set up all the resources.""" +        super().__init__(*args, **kwargs) -def resource_view(request: HttpRequest) -> HttpResponse: -    """View for resources index page.""" -    checkbox_options = { -        option: _parse_checkbox_options(request.GET.get(url_param, "")) -        for option, url_param in ( -            ('topics', 'topic'), -            ('type', 'type'), -            ('payment_tiers', 'payment'), -            ('complexity', 'complexity'), -        ) -    } - -    topics = sorted(RESOURCE_META_TAGS.get("topics")) - -    return render( -        request, -        template_name="resources/resources.html", -        context={ -            "checkboxOptions": checkbox_options, -            "topics_1": topics[:len(topics) // 2], -            "topics_2": topics[len(topics) // 2:], -            "tag_types": sorted(RESOURCE_META_TAGS.get("type")), -            "payment_tiers": sorted(RESOURCE_META_TAGS.get("payment_tiers")), -            "complexities": sorted(RESOURCE_META_TAGS.get("complexity")), -            "resources": get_resources_from_search(checkbox_options) +        # Load the resources from the yaml files in /resources/ +        self.resources = { +            path.stem: yaml.safe_load(path.read_text()) +            for path in RESOURCES_PATH.rglob("*.yaml") +        } + +        # Parse out all current tags +        resource_tags = { +            "topics": set(), +            "payment_tiers": set(), +            "complexity": set(), +            "type": set(),          } -    ) +        for resource_name, resource in self.resources.items(): +            css_classes = [] +            for tag_type in resource_tags.keys(): +                # Store the tags into `resource_tags` +                tags = resource.get("tags", {}).get(tag_type, []) +                for tag in tags: +                    tag = tag.title() +                    tag = tag.replace("And", "and") +                    resource_tags[tag_type].add(tag) + +                # Make a CSS class friendly representation too, while we're already iterating. +                for tag in tags: +                    css_tag = f"{tag_type}-{tag}" +                    css_tag = css_tag.replace("_", "-") +                    css_tag = css_tag.replace(" ", "-") +                    css_classes.append(css_tag) + +            # Now add the css classes back to the resource, so we can use them in the template. +            self.resources[resource_name]["css_classes"] = " ".join(css_classes) + +        # Set up all the filter checkbox metadata +        self.filters = { +            "Complexity": { +                "filters": sorted(resource_tags.get("complexity")), +                "icon": "fas fa-brain", +                "hidden": False, +            }, +            "Type": { +                "filters": sorted(resource_tags.get("type")), +                "icon": "fas fa-photo-video", +                "hidden": False, +            }, +            "Payment tiers": { +                "filters": sorted(resource_tags.get("payment_tiers")), +                "icon": "fas fa-dollar-sign", +                "hidden": True, +            }, +            "Topics": { +                "filters": sorted(resource_tags.get("topics")), +                "icon": "fas fa-lightbulb", +                "hidden": True, +            } +        } + +    @staticmethod +    def _get_filter_options(request: WSGIRequest) -> dict[str, set]: +        """Get the requested filter options out of the request object.""" +        return { +            option: set(request.GET.get(url_param, "").split(",")[:-1]) +            for option, url_param in ( +                ('topics', 'topics'), +                ('type', 'type'), +                ('payment_tiers', 'payment'), +                ('complexity', 'complexity'), +            ) +        } + +    def get(self, request: WSGIRequest) -> HttpResponse: +        """List out all the resources, and any filtering options from the URL.""" +        filter_options = self._get_filter_options(request) + +        return render( +            request, +            template_name="resources/resources.html", +            context={ +                "resources": self.resources, +                "filters": self.filters, +                "filter_options": filter_options, +            } +        ) diff --git a/pydis_site/static/css/resources/resources.css b/pydis_site/static/css/resources/resources.css index 488effc3..f70cbd64 100644 --- a/pydis_site/static/css/resources/resources.css +++ b/pydis_site/static/css/resources/resources.css @@ -1,29 +1,27 @@ -/*.box, .tile.is-parent {*/ -/*    transition: 0.1s ease-out;*/ -/*}*/ -/*.box {*/ -/*    min-height: 15vh;*/ -/*}*/ -/*.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);*/ -/*}*/ -/*.tile.is-parent:hover {*/ -/*    padding: 0.65rem 0.85rem 0.85rem 0.65rem;*/ -/*    filter: saturate(1.1) brightness(1.1);*/ -/*}*/ +/* Disable highlighting for all text in the filters. */ +.filter-checkbox, +.filter-panel label, +.card-header span { +    user-select: none +} -/*#readingBlock {*/ -/*    background-image: linear-gradient(141deg, #911eb4 0%, #b631de 71%, #cf4bf7 100%);*/ -/*}*/ +/* Remove pointless margin in panel header */ +#filter-panel-header { +    margin-bottom: 0; +} -/*#interactiveBlock {*/ -/*    background-image: linear-gradient(141deg, #d05600 0%, #da722a 71%, #e68846 100%);*/ -/*}*/ +/* Full width filter cards */ +#resource-filtering-panel .card .collapsible-content .card-content { +    padding:0 +} -/*#communitiesBlock {*/ -/*    background-image: linear-gradient(141deg, #3b756f 0%, #3a847c 71%, #41948b 100%);*/ -/*}*/ +/* Disable clicking on the checkbox itself. */ +/* Instead, we want to let the anchor tag handle clicks. */ +.filter-checkbox { +    pointer-events: none; +} -/*#podcastsBlock {*/ -/*    background-image: linear-gradient(141deg, #232382 0%, #30309c 71%, #4343ad 100%);*/ -/*}*/ +/* Blurple category icons */ +i.is-primary { +    color: #7289da; +} diff --git a/pydis_site/static/js/resources.js b/pydis_site/static/js/resources.js index 5c353f97..dee59e52 100644 --- a/pydis_site/static/js/resources.js +++ b/pydis_site/static/js/resources.js @@ -1,48 +1,156 @@  "use strict"; -const initialParams = new URLSearchParams(window.location.search); -const checkboxOptions = ['topic', 'type', 'payment', 'complexity']; -const createQuerySelect = (opt) => { -    return "input[name=" + opt + "]" -} +// Filters that are currently selected +var activeFilters = { +    topics: [], +    type: [], +    "payment-tiers": [], +    complexity: [] +}; -checkboxOptions.forEach((option) => { -    document.querySelectorAll(createQuerySelect(option)).forEach((checkbox) => { -        if (initialParams.get(option).includes(checkbox.value)) { -            checkbox.checked = true -        } -    }); -}); +/* Update the resources to match 'active_filters' */ +function update() { +    let resources = $('.resource-box'); -function buildQueryParams() { -    let params = new URLSearchParams(window.location.search); -    checkboxOptions.forEach((option) => { -        let tempOut = "" -        document.querySelectorAll(createQuerySelect(option)).forEach((checkbox) => { -            if (checkbox.checked) { -                tempOut += checkbox.value + ","; +    // If there's nothing in the filters, show everything and return. +    if ( +        activeFilters.topics.length === 0 && +        activeFilters.type.length === 0 && +        activeFilters["payment-tiers"].length === 0 && +        activeFilters.complexity.length === 0 +    ) { +        resources.show(); +        return; +    } + +    // Otherwise, hide everything and then filter the resources to decide what to show. +    resources.hide(); +    resources.filter(function() { +        let validation = { +            topics: false, +            type: false, +            'payment-tiers': false, +            complexity: false +        }; +        let resourceBox = $(this); + +        // Validate the filters +        $.each(activeFilters, function(filterType, activeFilters) { +            // If the filter list is empty, this passes validation. +            if (activeFilters.length === 0) { +                validation[filterType] = true; +                return;              } + +            // Otherwise, we need to check if one of the classes exist. +            $.each(activeFilters, function(index, filter) { +                if (resourceBox.hasClass(filter)) { +                    validation[filterType] = true; +                } +            });          }); -        params.set(option, tempOut); -    }); -    window.location.search = params; +        // If validation passes, show the resource. +        if (Object.values(validation).every(Boolean)) { +            return true; +        } else { +            return false; +        } +    }).show();  } -function clearQueryParams() { -    checkboxOptions.forEach((option) => { -        document.querySelectorAll(createQuerySelect(option)).forEach((checkbox) => { -            checkbox.checked = false; -        }); +// Executed when the page has finished loading. +document.addEventListener("DOMContentLoaded", function () { + +    // If you collapse or uncollapse a filter group, swap the icon. +    $('button.collapsible').click(function() { +        let icon = $(this).find(".card-header-icon i"); + +        if ($(icon).hasClass("fa-window-minimize")) { +            $(icon).removeClass(["far", "fa-window-minimize"]); +            $(icon).addClass(["fas", "fa-angle-down"]); +        } else { +            $(icon).removeClass(["fas", "fa-angle-down"]); +            $(icon).addClass(["far", "fa-window-minimize"]); +        } +    }); + +    // Update the filters on page load to reflect URL parameters. + +    // If you click on the div surrounding the filter checkbox, it clicks the checkbox. +    $('.filter-panel').click(function() { +        let checkbox = $(this).find(".filter-checkbox"); +        checkbox.prop("checked", !checkbox.prop("checked")); +        checkbox.change();      }); -} -function selectAllQueryParams(column) { -    checkboxOptions.forEach((option) => { -        document.querySelectorAll(createQuerySelect(option)).forEach((checkbox) => { -            if (checkbox.className == column) { -                checkbox.checked = true; +    // When checkboxes are toggled, trigger a filter update. +    $('.filter-checkbox').change(function () { +        let filterItem = this.dataset.filterItem; +        let filterName = this.dataset.filterName; +        let cssClass = filterName + "-" + filterItem; +        var filterIndex = activeFilters[filterName].indexOf(cssClass); + +        if (this.checked) { +            if (filterIndex === -1) { +                activeFilters[filterName].push(cssClass);              } -        }); +            update(); +        } else { +            if (filterIndex !== -1) { +                activeFilters[filterName].splice(filterIndex, 1); +            } +            update(); +        }      }); -} +}); + + + +// const initialParams = new URLSearchParams(window.location.search); +// const checkboxOptions = ['topic', 'type', 'payment', 'complexity']; +// +// const createQuerySelect = (opt) => { +//     return "input[name=" + opt + "]" +// } +// +// checkboxOptions.forEach((option) => { +//     document.querySelectorAll(createQuerySelect(option)).forEach((checkbox) => { +//         if (initialParams.get(option).includes(checkbox.value)) { +//             checkbox.checked = true +//         } +//     }); +// }); +// +// function buildQueryParams() { +//     let params = new URLSearchParams(window.location.search); +//     checkboxOptions.forEach((option) => { +//         let tempOut = "" +//         document.querySelectorAll(createQuerySelect(option)).forEach((checkbox) => { +//             if (checkbox.checked) { +//                 tempOut += checkbox.value + ","; +//             } +//         }); +//         params.set(option, tempOut); +//     }); +// +//     window.location.search = params; +// } +// +// function clearQueryParams() { +//     checkboxOptions.forEach((option) => { +//         document.querySelectorAll(createQuerySelect(option)).forEach((checkbox) => { +//             checkbox.checked = false; +//         }); +//     }); +// } +// +// function selectAllQueryParams(column) { +//     checkboxOptions.forEach((option) => { +//         document.querySelectorAll(createQuerySelect(option)).forEach((checkbox) => { +//             if (checkbox.className == column) { +//                 checkbox.checked = true; +//             } +//         }); +//     }); +// } diff --git a/pydis_site/templates/resources/resources.html b/pydis_site/templates/resources/resources.html index 427f417e..77723a8f 100644 --- a/pydis_site/templates/resources/resources.html +++ b/pydis_site/templates/resources/resources.html @@ -1,146 +1,106 @@  {% extends 'base/base.html' %}  {% load as_icon %} +{% load as_css_class %}  {% load static %}  {% block title %}Resources{% endblock %}  {% block head %} +    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> +    <script src="{% static "js/resources.js" %}"></script> +    <script src="{% static "js/content/page.js" %}"></script>      <link rel="stylesheet" href="{% static "css/resources/resources.css" %}">      <link rel="stylesheet" href="{% static "css/resources/resources_list.css" %}"> +    <link rel="stylesheet" href="{% static "css/content/page.css" %}">  {% endblock %}  {% block content %}      {% include "base/navbar.html" %} - -    <section class="section"> -        <div class="container"> +    {% if resources|length > 0 %} +        <section class="section"> +            {#  Headline #}              <div class="content">                  <h1 class="resource-title has-text-centered">Resources</h1>                  <hr/>              </div> -            <div class="panel is-hidden-mobile"> -                <p class="panel-heading has-text-centered">Search Options</p> - -                <div class="field"> -                    <div class="columns"> -                        <div class="column pl-6 is-two-fifths is-flex is-flex-direction-column"> -                            <div class="title is-5 pt-2 has-text-centered">Topic</div> -                            <div class="columns"> -                                <div class="column"> -                                    {% for topic in topics_1 %} -                                    <div class="field"> -                                        <label class="checkbox"> -                                            <input class="topic" name="topic" type="checkbox" value="{{ topic }}"> -                                            <span class="has-text-grey is-size-6">{{ topic }}</span> -                                        </label> -                                    </div> -                                    {% endfor %} -                                </div> -                                <div class="column"> -                                    {% for topic in topics_2 %} -                                    <div class="field"> -                                        <label class="checkbox"> -                                            <input class="topic" name="topic" type="checkbox" value="{{ topic }}"> -                                            <span class="has-text-grey is-size-6">{{ topic }}</span> -                                        </label> -                                    </div> -                                    {% endfor %} -                                </div> -                            </div> -                            <span class="control ml-2 is-flex is-align-items-end is-justify-content-center mt-auto"> -                                <button onclick="selectAllQueryParams('topic')" class="button is-success is-small">Select All</button> -                            </span> -                        </div> -                        <div class="column is-flex is-flex-direction-column pl-6"> -                            <div class="title is-5 pt-2 has-text-centered">Type</div> +            <div class="columns is-centered"> +                {# Filtering toolbox #} +                <div class="column is-one-third"> +                    <div class="content is-justify-content-center"> +                        <nav id="resource-filtering-panel" class="panel is-primary"> +                            <p class="panel-heading has-text-centered" id="filter-panel-header">Filters</p> -                            {% for tag_type in tag_types %} -                            <div class="field"> -                                <label class="checkbox ml-0"> -                                    <input class="type" name="type" type="checkbox" value="{{ tag_type }}"> -                                    <span class="has-text-grey is-size-6">{{ tag_type }}</span> -                                </label> -                            </div> +                            {# Filter checkboxes #} +                            {% for filter_name, filter_data in filters.items %} +                                    <div class="card"> +                                        <button type="button" class="card-header collapsible"> +                                            <span class="card-header-title subtitle is-6 my-2 ml-2"> +                                                <i class="{{ filter_data.icon }} is-primary" aria-hidden="true"></i>  {{ filter_name }} +                                            </span> +                                            <span class="card-header-icon"> +                                                {% if not filter_data.hidden %} +                                                    <i class="far fa-window-minimize is-6 title" aria-hidden="true"></i> +                                                {% else %} +                                                    <i class="fas fa-angle-down is-6 title" aria-hidden="true"></i> +                                                {% endif %} +                                            </span> +                                        </button> +                                        {# Checkboxes #} +                                        {% if filter_data.hidden %} +                                            <div class="collapsible-content"> +                                        {% else %} +                                            <div class="collapsible-content" style="max-height: 480px;"> +                                        {% endif %} +                                            <div class="card-content"> +                                                {% for filter_item in filter_data.filters %} +                                                    <a class="panel-block filter-panel"> +                                                        <label class="checkbox"> +                                                          <input +                                                            class="filter-checkbox" +                                                            type="checkbox" +                                                            data-filter-name="{{ filter_name|as_css_class }}" +                                                            data-filter-item="{{ filter_item|as_css_class }}" +                                                          > +                                                            {{ filter_item }} +                                                        </label> +                                                    </a> +                                                {% endfor %} +                                            </div> +                                        </div> +                                    </div>                              {% endfor %} -                            <span class="control ml-2 is-flex is-align-items-end is-justify-content-center mt-auto"> -                                <button onclick="selectAllQueryParams('type')" class="button is-success is-small">Select All</button> -                            </span> -                        </div> - -                        <div class="column is-flex is-flex-direction-column pl-6"> -                            <div class="title is-5 pt-2 has-text-centered">Payment</div> +                        </nav> +                    </div> +                </div> -                            {% for payment_tier in payment_tiers %} -                            <div class="field"> -                                <label class="checkbox ml-0"> -                                    <input class="payment" name="payment" type="checkbox" value="{{ payment_tier }}"> -                                     <span class="has-text-grey is-size-6">{{ payment_tier }}</span> -                               </label> -                            </div> +                {# Actual resources #} +                <div class="column is-two-thirds"> +                    <div class="content is-flex is-justify-content-center"> +                        <div> +                            {% for resource in resources.values %} +                                {% include "resources/resource_box.html" %}                              {% endfor %} -                            <span class="control ml-2 is-flex is-align-items-end is-justify-content-center mt-auto"> -                                <button onclick="selectAllQueryParams('payment')" class="button is-success is-small">Select All</button> -                            </span> -                        </div> -                        <div class="column is-flex is-flex-direction-column px-6"> -                            <div class="title is-5 pt-2 has-text-centered">Level</div> +                            {% for subcategory in subcategories %} +                                <h2 id="{{ subcategory.category_info.raw_name }}"> +                                    <a href="whatevenisthis#{{ subcategory.category_info.raw_name }}"> +                                        {{ subcategory.category_info.name }} +                                    </a> +                                </h2> +                                <p>{{ subcategory.category_info.description|safe }}</p> -                            {% for complexity in complexities %} -                            <div class="field"> -                                <label class="checkbox ml-0"> -                                    <input class="complexity" name="complexity" type="checkbox" value="{{ complexity }}"> -                                    <span class="has-text-grey is-size-6">{{ complexity }}</span> -                              </label> -                            </div> -                           {% endfor %} -                            <span class="control ml-2 is-flex is-align-items-end is-justify-content-center mt-auto"> -                                <button onclick="selectAllQueryParams('complexity')" class="button is-success is-small">Select All</button> -                            </span> +                                {% for resource in subcategory.resources %} +                                    {% with category_info=subcategory.category_info %} +                                        {% include "resources/resource_box.html" %} +                                    {% endwith %} +                                {% endfor %} +                            {% endfor %}                          </div>                      </div> - -                    <div class="is-flex is-justify-content-center pb-3"> -                        <span class="control mr-2"> -                            <button onclick="buildQueryParams()" class="button is-link is-small">Search</button> -                        </span> - -                        <span class="is-one-fifth control is-flex is-align-items-end is-justify-content-end mt-auto"> -                            <button onclick="clearQueryParams()" class="button is-danger is-small">Clear Search</button> -                        </span> -                    </div> -                </div> -            </div> -    </section> - -    {% if resources|length > 0 %} -    <section class="section"> -        <div class="container"> -            <div class="content is-flex is-justify-content-center"> -                <div> -                    {% for resource in resources %} -                        {% include "resources/resource_box.html" %} -                    {% endfor %} - -                    {% for subcategory in subcategories %} -                        <h2 id="{{ subcategory.category_info.raw_name }}"> -                            <a href="{% url "resources:resources" category=category_info.raw_name %}#{{ subcategory.category_info.raw_name }}"> -                                {{ subcategory.category_info.name }} -                            </a> -                        </h2> -                        <p>{{ subcategory.category_info.description|safe }}</p> - -                        {% for resource in subcategory.resources %} -                            {% with category_info=subcategory.category_info %} -                                {% include "resources/resource_box.html" %} -                            {% endwith %} -                        {% endfor %} -                    {% endfor %}                  </div>              </div> -        </div> -    </section> +        </section>      {% else %} -    <h2 class="title is-3 has-text-centered pt-0 pb-6 ">No resources matching search.</p> +        <h2 class="title is-3 has-text-centered pt-0 pb-6 ">No resources matching search.</p>      {% endif %}  {% endblock %}  |