diff options
-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 %} |