aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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 %}