aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Leon Sandøy <[email protected]>2022-01-27 10:23:11 +0100
committerGravatar Leon Sandøy <[email protected]>2022-01-27 10:23:11 +0100
commit5f214647521a93fa6cb54fcac7d4e1ae66fb7cec (patch)
tree48f8bdfb0e2dd8e99532d28bde10ea5e8112d9cf
parentClean up dependency file and bump simple-bulma. (diff)
Resource filtering on the client, pt 1.
Here's the initial version of this system. We've got filtering, but only by clicking checkboxes. The overall look and style are pretty close to where we want them, but it's missing tons of polish to be complete. The following commits will contain that polish.
-rw-r--r--pydis_site/apps/resources/views/resources.py125
-rw-r--r--pydis_site/static/css/resources/resources.css48
-rw-r--r--pydis_site/static/js/resources.js178
-rw-r--r--pydis_site/templates/resources/resources.html192
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>&nbsp&nbsp{{ 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 %}