aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site
diff options
context:
space:
mode:
Diffstat (limited to 'pydis_site')
-rw-r--r--pydis_site/apps/redirect/redirects.yaml5
-rw-r--r--pydis_site/apps/resources/resources/adafruit.yaml1
-rw-r--r--pydis_site/apps/resources/resources/corey_schafer.yaml1
-rw-r--r--pydis_site/apps/resources/resources/kivy.yaml1
-rw-r--r--pydis_site/apps/resources/resources/microsoft.yaml1
-rw-r--r--pydis_site/apps/resources/resources/pallets.yaml1
-rw-r--r--pydis_site/apps/resources/resources/panda3d.yaml1
-rw-r--r--pydis_site/apps/resources/resources/people_postgres_data.yaml1
-rw-r--r--pydis_site/apps/resources/resources/pyglet.yaml1
-rw-r--r--pydis_site/apps/resources/resources/python_discord_videos.yaml1
-rw-r--r--pydis_site/apps/resources/resources/real_python.yaml1
-rw-r--r--pydis_site/apps/resources/resources/sentdex.yaml1
-rw-r--r--pydis_site/apps/resources/resources/socratica.yaml1
-rw-r--r--pydis_site/apps/resources/resources/two_scoops_of_django.yaml2
-rw-r--r--pydis_site/static/css/resources/resources.css41
-rw-r--r--pydis_site/static/js/fuzzysort/LICENSE.md21
-rw-r--r--pydis_site/static/js/fuzzysort/fuzzysort.js636
-rw-r--r--pydis_site/static/js/resources/resources.js114
-rw-r--r--pydis_site/templates/resources/resource_box.html2
-rw-r--r--pydis_site/templates/resources/resource_box_header.html3
-rw-r--r--pydis_site/templates/resources/resources.html26
21 files changed, 835 insertions, 27 deletions
diff --git a/pydis_site/apps/redirect/redirects.yaml b/pydis_site/apps/redirect/redirects.yaml
index 9b64011b..4a48ba0c 100644
--- a/pydis_site/apps/redirect/redirects.yaml
+++ b/pydis_site/apps/redirect/redirects.yaml
@@ -83,6 +83,11 @@ good_questions_redirect_alt:
redirect_arguments: ["guides/pydis-guides/asking-good-questions"]
# Resources
+resources_old_communities_redirect:
+ original_path: pages/resources/communities/
+ redirect_route: "resources:index"
+ redirect_arguments: ["community"]
+
resources_index_redirect:
original_path: pages/resources/
redirect_route: "resources:index"
diff --git a/pydis_site/apps/resources/resources/adafruit.yaml b/pydis_site/apps/resources/resources/adafruit.yaml
index f9466bd8..c687f507 100644
--- a/pydis_site/apps/resources/resources/adafruit.yaml
+++ b/pydis_site/apps/resources/resources/adafruit.yaml
@@ -1,3 +1,4 @@
+name: Adafruit
description: Adafruit is an open-source electronics manufacturer
that makes all the components you need to start your own Python-powered hardware projects.
Their official community host regular show-and-tells,
diff --git a/pydis_site/apps/resources/resources/corey_schafer.yaml b/pydis_site/apps/resources/resources/corey_schafer.yaml
index f5af2cab..d66ea004 100644
--- a/pydis_site/apps/resources/resources/corey_schafer.yaml
+++ b/pydis_site/apps/resources/resources/corey_schafer.yaml
@@ -1,3 +1,4 @@
+name: Corey Schafer
description: 'Corey has a number of exceptionally high quality tutorial series
on everything from Python basics to Django and Flask:
<ul>
diff --git a/pydis_site/apps/resources/resources/kivy.yaml b/pydis_site/apps/resources/resources/kivy.yaml
index 47ff07ad..b1f57483 100644
--- a/pydis_site/apps/resources/resources/kivy.yaml
+++ b/pydis_site/apps/resources/resources/kivy.yaml
@@ -1,3 +1,4 @@
+name: Kivy
description: The Kivy project, through the Kivy framework and its sister projects,
aims to provide all the tools to create desktop and mobile applications in Python.
Allowing rapid development of multitouch applications with custom and exciting user interfaces.
diff --git a/pydis_site/apps/resources/resources/microsoft.yaml b/pydis_site/apps/resources/resources/microsoft.yaml
index e1d62955..290283cc 100644
--- a/pydis_site/apps/resources/resources/microsoft.yaml
+++ b/pydis_site/apps/resources/resources/microsoft.yaml
@@ -1,3 +1,4 @@
+name: Microsoft Python
description: Microsoft Python is a Discord server for discussing all things relating to using Python with Microsoft products,
they have channels for Azure, VS Code, IoT, Data Science and much more!
title_image: https://1000logos.net/wp-content/uploads/2017/04/Microsoft-Logo.png
diff --git a/pydis_site/apps/resources/resources/pallets.yaml b/pydis_site/apps/resources/resources/pallets.yaml
index 0da2a625..a330b756 100644
--- a/pydis_site/apps/resources/resources/pallets.yaml
+++ b/pydis_site/apps/resources/resources/pallets.yaml
@@ -1,3 +1,4 @@
+name: Pallets Projects
description: The Pallets Projects develop Python libraries such as the Flask web framework,
the Jinja templating library, and the Click command line toolkit. Join to discuss
and get help from the Pallets community.
diff --git a/pydis_site/apps/resources/resources/panda3d.yaml b/pydis_site/apps/resources/resources/panda3d.yaml
index 2040450d..eeb54465 100644
--- a/pydis_site/apps/resources/resources/panda3d.yaml
+++ b/pydis_site/apps/resources/resources/panda3d.yaml
@@ -1,3 +1,4 @@
+name: Panda3D
description: Panda3D is a Python-focused 3-D framework for rapid development of games,
visualizations, and simulations, written in C++ with an emphasis on performance and flexibility.
title_image: https://www.panda3d.org/wp-content/uploads/2019/01/panda3d_logo.png
diff --git a/pydis_site/apps/resources/resources/people_postgres_data.yaml b/pydis_site/apps/resources/resources/people_postgres_data.yaml
index 46db7095..9fec6634 100644
--- a/pydis_site/apps/resources/resources/people_postgres_data.yaml
+++ b/pydis_site/apps/resources/resources/people_postgres_data.yaml
@@ -1,3 +1,4 @@
+name: People, Postgres, Data
description: People, Postgres, Data specializes in building users of Postgres
and related ecosystem including but not limited to technologies such as RDS Postgres,
Aurora for Postgres, Google Postgres, PostgreSQL.Org Postgres, Greenplum, Timescale and ZomboDB.
diff --git a/pydis_site/apps/resources/resources/pyglet.yaml b/pydis_site/apps/resources/resources/pyglet.yaml
index a47c7e62..bdfb84cf 100644
--- a/pydis_site/apps/resources/resources/pyglet.yaml
+++ b/pydis_site/apps/resources/resources/pyglet.yaml
@@ -1,3 +1,4 @@
+name: Pyglet
description: Pyglet is a powerful,
yet easy to use Python library for developing games and other visually-rich applications on Windows,
Mac OS X and Linux. It supports windowing, user interface event handling, Joysticks, OpenGL graphics,
diff --git a/pydis_site/apps/resources/resources/python_discord_videos.yaml b/pydis_site/apps/resources/resources/python_discord_videos.yaml
index 15a04097..012ec8ea 100644
--- a/pydis_site/apps/resources/resources/python_discord_videos.yaml
+++ b/pydis_site/apps/resources/resources/python_discord_videos.yaml
@@ -1,3 +1,4 @@
+name: Python Discord YouTube Channel
description: It's our YouTube channel! We are slowly gathering content here directly related to Python,
our community and the events we host. Come check us out!
title_image: https://raw.githubusercontent.com/python-discord/branding/master/logos/logo_banner/logo_site_banner_dark_512.png
diff --git a/pydis_site/apps/resources/resources/real_python.yaml b/pydis_site/apps/resources/resources/real_python.yaml
index 2ddada03..93953004 100644
--- a/pydis_site/apps/resources/resources/real_python.yaml
+++ b/pydis_site/apps/resources/resources/real_python.yaml
@@ -1,3 +1,4 @@
+name: Real Python
description: Dan Bader's treasure trove of quizzes, tutorials and interactive content for learning Python.
An absolute goldmine.
title_image: https://i.imgur.com/WDqhZ36.png
diff --git a/pydis_site/apps/resources/resources/sentdex.yaml b/pydis_site/apps/resources/resources/sentdex.yaml
index 4f4712ac..7cb0a8a4 100644
--- a/pydis_site/apps/resources/resources/sentdex.yaml
+++ b/pydis_site/apps/resources/resources/sentdex.yaml
@@ -1,3 +1,4 @@
+name: Sentdex
description: 'An enormous amount of Python content for all skill levels
from the most popular Python YouTuber on the web.
<ul>
diff --git a/pydis_site/apps/resources/resources/socratica.yaml b/pydis_site/apps/resources/resources/socratica.yaml
index 43d033c0..45150b33 100644
--- a/pydis_site/apps/resources/resources/socratica.yaml
+++ b/pydis_site/apps/resources/resources/socratica.yaml
@@ -1,3 +1,4 @@
+name: Socratica
description: 'Socratica is a small studio focused on producing high quality STEM-related educational content,
including a series about Python. Their videos star actress Ulka Simone Mohanty, who plays an android-like
instructor explaining fundamental concepts in a concise and entertaining way.'
diff --git a/pydis_site/apps/resources/resources/two_scoops_of_django.yaml b/pydis_site/apps/resources/resources/two_scoops_of_django.yaml
index 96eafd28..f372d35d 100644
--- a/pydis_site/apps/resources/resources/two_scoops_of_django.yaml
+++ b/pydis_site/apps/resources/resources/two_scoops_of_django.yaml
@@ -1,7 +1,7 @@
description: Tips, tricks, and best practices for your Django project.
A highly recommended resource for Django web developers.
name: Two Scoops of Django
-title_url: https://www.feldroy.com/collections/everything/products/two-scoops-of-django-3-x
+title_url: https://www.feldroy.com/books/two-scoops-of-django-3-x
urls:
- icon: branding/goodreads
url: https://www.goodreads.com/book/show/55822151-two-scoops-of-django-3-x
diff --git a/pydis_site/static/css/resources/resources.css b/pydis_site/static/css/resources/resources.css
index b8456e38..96d06111 100644
--- a/pydis_site/static/css/resources/resources.css
+++ b/pydis_site/static/css/resources/resources.css
@@ -73,6 +73,11 @@ display: block;
margin-right: 0.25em !important;
}
+/* Style the search bar */
+#resource-search {
+ margin: 0.25em 0.25em 0 0.25em;
+}
+
/* Center the 404 div */
.no-resources-found {
display: none;
@@ -86,6 +91,35 @@ display: block;
display: flex !important;
}
+/* By default, we hide the search tag. We'll add it only when there's a search happening. */
+.tag.search-query {
+ display: none;
+ min-width: fit-content;
+ max-width: fit-content;
+ padding-right: 2em;
+}
+.tag.search-query .inner {
+ display: inline-block;
+ padding: 0;
+ max-width: 16.5rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ line-height: 2em;
+}
+.tag.search-query i {
+ margin: 0 !important;
+ display: inline-block;
+ line-height: 2em;
+ float: left;
+ padding-right: 1em;
+}
+
+/* Don't allow the tag pool to exceed its parent containers width. */
+#tag-pool {
+ max-width: 100%;
+}
+
/* Disable clicking on the checkbox itself. */
/* Instead, we want to let the anchor tag handle clicks. */
.filter-checkbox {
@@ -125,7 +159,6 @@ i.is-primary {
color: #7289DA;
}
-
/* Set default display to inline-flex, for centering. */
span.filter-box-tag {
display: none;
@@ -181,7 +214,8 @@ button.delete.is-info::after {
/* Give outlines to active tags */
span.filter-box-tag,
-span.resource-tag.active {
+span.resource-tag.active,
+.tag.search-query {
outline-width: 1px;
outline-style: solid;
}
@@ -245,6 +279,9 @@ span.resource-tag.active.has-background-info-light {
padding-top: 4px;
padding-bottom: 4px;
}
+ .tag.search-query .inner {
+ max-width: 16.2rem;
+ }
}
/* Constrain the width of the filterbox */
diff --git a/pydis_site/static/js/fuzzysort/LICENSE.md b/pydis_site/static/js/fuzzysort/LICENSE.md
new file mode 100644
index 00000000..a3b9d9d7
--- /dev/null
+++ b/pydis_site/static/js/fuzzysort/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Stephen Kamenar
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pydis_site/static/js/fuzzysort/fuzzysort.js b/pydis_site/static/js/fuzzysort/fuzzysort.js
new file mode 100644
index 00000000..ba01ae63
--- /dev/null
+++ b/pydis_site/static/js/fuzzysort/fuzzysort.js
@@ -0,0 +1,636 @@
+/*
+ fuzzysort.js https://github.com/farzher/fuzzysort
+ SublimeText-like Fuzzy Search
+
+ fuzzysort.single('fs', 'Fuzzy Search') // {score: -16}
+ fuzzysort.single('test', 'test') // {score: 0}
+ fuzzysort.single('doesnt exist', 'target') // null
+
+ fuzzysort.go('mr', [{file:'Monitor.cpp'}, {file:'MeshRenderer.cpp'}], {key:'file'})
+ // [{score:-18, obj:{file:'MeshRenderer.cpp'}}, {score:-6009, obj:{file:'Monitor.cpp'}}]
+
+ fuzzysort.go('mr', ['Monitor.cpp', 'MeshRenderer.cpp'])
+ // [{score: -18, target: "MeshRenderer.cpp"}, {score: -6009, target: "Monitor.cpp"}]
+
+ fuzzysort.highlight(fuzzysort.single('fs', 'Fuzzy Search'), '<b>', '</b>')
+ // <b>F</b>uzzy <b>S</b>earch
+*/
+
+// UMD (Universal Module Definition) for fuzzysort
+;(function(root, UMD) {
+ if(typeof define === 'function' && define.amd) define([], UMD)
+ else if(typeof module === 'object' && module.exports) module.exports = UMD()
+ else root.fuzzysort = UMD()
+})(this, function UMD() { function fuzzysortNew(instanceOptions) {
+
+ var fuzzysort = {
+
+ single: function(search, target, options) { ;if(search=='farzher')return{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6]}
+ if(!search) return null
+ if(!isObj(search)) search = fuzzysort.getPreparedSearch(search)
+
+ if(!target) return null
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo
+ : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo
+ : true
+ var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo
+ return algorithm(search, target, search[0])
+ },
+
+ go: function(search, targets, options) { ;if(search=='farzher')return[{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6],obj:targets?targets[0]:null}]
+ if(!search) return noResults
+ search = fuzzysort.prepareSearch(search)
+ var searchLowerCode = search[0]
+
+ var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991
+ var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991
+ var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo
+ : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo
+ : true
+ var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo
+ var resultsLen = 0; var limitedCount = 0
+ var targetsLen = targets.length
+
+ // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys]
+
+ // options.keys
+ if(options && options.keys) {
+ var scoreFn = options.scoreFn || defaultScoreFn
+ var keys = options.keys
+ var keysLen = keys.length
+ for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i]
+ var objResults = new Array(keysLen)
+ for (var keyI = keysLen - 1; keyI >= 0; --keyI) {
+ var key = keys[keyI]
+ var target = getValue(obj, key)
+ if(!target) { objResults[keyI] = null; continue }
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ objResults[keyI] = algorithm(search, target, searchLowerCode)
+ }
+ objResults.obj = obj // before scoreFn so scoreFn can use it
+ var score = scoreFn(objResults)
+ if(score === null) continue
+ if(score < threshold) continue
+ objResults.score = score
+ if(resultsLen < limit) { q.add(objResults); ++resultsLen }
+ else {
+ ++limitedCount
+ if(score > q.peek().score) q.replaceTop(objResults)
+ }
+ }
+
+ // options.key
+ } else if(options && options.key) {
+ var key = options.key
+ for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i]
+ var target = getValue(obj, key)
+ if(!target) continue
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ var result = algorithm(search, target, searchLowerCode)
+ if(result === null) continue
+ if(result.score < threshold) continue
+
+ // have to clone result so duplicate targets from different obj can each reference the correct obj
+ result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden
+
+ if(resultsLen < limit) { q.add(result); ++resultsLen }
+ else {
+ ++limitedCount
+ if(result.score > q.peek().score) q.replaceTop(result)
+ }
+ }
+
+ // no keys
+ } else {
+ for(var i = targetsLen - 1; i >= 0; --i) { var target = targets[i]
+ if(!target) continue
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ var result = algorithm(search, target, searchLowerCode)
+ if(result === null) continue
+ if(result.score < threshold) continue
+ if(resultsLen < limit) { q.add(result); ++resultsLen }
+ else {
+ ++limitedCount
+ if(result.score > q.peek().score) q.replaceTop(result)
+ }
+ }
+ }
+
+ if(resultsLen === 0) return noResults
+ var results = new Array(resultsLen)
+ for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
+ results.total = resultsLen + limitedCount
+ return results
+ },
+
+ goAsync: function(search, targets, options) {
+ var canceled = false
+ var p = new Promise(function(resolve, reject) { ;if(search=='farzher')return resolve([{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6],obj:targets?targets[0]:null}])
+ if(!search) return resolve(noResults)
+ search = fuzzysort.prepareSearch(search)
+ var searchLowerCode = search[0]
+
+ var q = fastpriorityqueue()
+ var iCurrent = targets.length - 1
+ var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991
+ var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991
+ var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo
+ : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo
+ : true
+ var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo
+ var resultsLen = 0; var limitedCount = 0
+ function step() {
+ if(canceled) return reject('canceled')
+
+ var startMs = Date.now()
+
+ // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys]
+
+ // options.keys
+ if(options && options.keys) {
+ var scoreFn = options.scoreFn || defaultScoreFn
+ var keys = options.keys
+ var keysLen = keys.length
+ for(; iCurrent >= 0; --iCurrent) {
+ if(iCurrent%1000/*itemsPerCheck*/ === 0) {
+ if(Date.now() - startMs >= 10/*asyncInterval*/) {
+ isNode?setImmediate(step):setTimeout(step)
+ return
+ }
+ }
+
+ var obj = targets[iCurrent]
+ var objResults = new Array(keysLen)
+ for (var keyI = keysLen - 1; keyI >= 0; --keyI) {
+ var key = keys[keyI]
+ var target = getValue(obj, key)
+ if(!target) { objResults[keyI] = null; continue }
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ objResults[keyI] = algorithm(search, target, searchLowerCode)
+ }
+ objResults.obj = obj // before scoreFn so scoreFn can use it
+ var score = scoreFn(objResults)
+ if(score === null) continue
+ if(score < threshold) continue
+ objResults.score = score
+ if(resultsLen < limit) { q.add(objResults); ++resultsLen }
+ else {
+ ++limitedCount
+ if(score > q.peek().score) q.replaceTop(objResults)
+ }
+ }
+
+ // options.key
+ } else if(options && options.key) {
+ var key = options.key
+ for(; iCurrent >= 0; --iCurrent) {
+ if(iCurrent%1000/*itemsPerCheck*/ === 0) {
+ if(Date.now() - startMs >= 10/*asyncInterval*/) {
+ isNode?setImmediate(step):setTimeout(step)
+ return
+ }
+ }
+
+ var obj = targets[iCurrent]
+ var target = getValue(obj, key)
+ if(!target) continue
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ var result = algorithm(search, target, searchLowerCode)
+ if(result === null) continue
+ if(result.score < threshold) continue
+
+ // have to clone result so duplicate targets from different obj can each reference the correct obj
+ result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden
+
+ if(resultsLen < limit) { q.add(result); ++resultsLen }
+ else {
+ ++limitedCount
+ if(result.score > q.peek().score) q.replaceTop(result)
+ }
+ }
+
+ // no keys
+ } else {
+ for(; iCurrent >= 0; --iCurrent) {
+ if(iCurrent%1000/*itemsPerCheck*/ === 0) {
+ if(Date.now() - startMs >= 10/*asyncInterval*/) {
+ isNode?setImmediate(step):setTimeout(step)
+ return
+ }
+ }
+
+ var target = targets[iCurrent]
+ if(!target) continue
+ if(!isObj(target)) target = fuzzysort.getPrepared(target)
+
+ var result = algorithm(search, target, searchLowerCode)
+ if(result === null) continue
+ if(result.score < threshold) continue
+ if(resultsLen < limit) { q.add(result); ++resultsLen }
+ else {
+ ++limitedCount
+ if(result.score > q.peek().score) q.replaceTop(result)
+ }
+ }
+ }
+
+ if(resultsLen === 0) return resolve(noResults)
+ var results = new Array(resultsLen)
+ for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
+ results.total = resultsLen + limitedCount
+ resolve(results)
+ }
+
+ isNode?setImmediate(step):step() //setTimeout here is too slow
+ })
+ p.cancel = function() { canceled = true }
+ return p
+ },
+
+ highlight: function(result, hOpen, hClose) {
+ if(typeof hOpen == 'function') return fuzzysort.highlightCallback(result, hOpen)
+ if(result === null) return null
+ if(hOpen === undefined) hOpen = '<b>'
+ if(hClose === undefined) hClose = '</b>'
+ var highlighted = ''
+ var matchesIndex = 0
+ var opened = false
+ var target = result.target
+ var targetLen = target.length
+ var matchesBest = result.indexes
+ for(var i = 0; i < targetLen; ++i) { var char = target[i]
+ if(matchesBest[matchesIndex] === i) {
+ ++matchesIndex
+ if(!opened) { opened = true
+ highlighted += hOpen
+ }
+
+ if(matchesIndex === matchesBest.length) {
+ highlighted += char + hClose + target.substr(i+1)
+ break
+ }
+ } else {
+ if(opened) { opened = false
+ highlighted += hClose
+ }
+ }
+ highlighted += char
+ }
+
+ return highlighted
+ },
+ highlightCallback: function(result, cb) {
+ if(result === null) return null
+ var target = result.target
+ var targetLen = target.length
+ var indexes = result.indexes
+ var highlighted = ''
+ var matchI = 0
+ var indexesI = 0
+ var opened = false
+ var result = []
+ for(var i = 0; i < targetLen; ++i) { var char = target[i]
+ if(indexes[indexesI] === i) {
+ ++indexesI
+ if(!opened) { opened = true
+ result.push(highlighted); highlighted = ''
+ }
+
+ if(indexesI === indexes.length) {
+ highlighted += char
+ result.push(cb(highlighted, matchI++)); highlighted = ''
+ result.push(target.substr(i+1))
+ break
+ }
+ } else {
+ if(opened) { opened = false
+ result.push(cb(highlighted, matchI++)); highlighted = ''
+ }
+ }
+ highlighted += char
+ }
+ return result
+ },
+
+ prepare: function(target) {
+ if(!target) return {target: '', _targetLowerCodes: [0/*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], _nextBeginningIndexes: null, score: null, indexes: null, obj: null} // hidden
+ return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:null, score:null, indexes:null, obj:null} // hidden
+ },
+ prepareSlow: function(target) {
+ if(!target) return {target: '', _targetLowerCodes: [0/*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], _nextBeginningIndexes: null, score: null, indexes: null, obj: null} // hidden
+ return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:fuzzysort.prepareNextBeginningIndexes(target), score:null, indexes:null, obj:null} // hidden
+ },
+ prepareSearch: function(search) {
+ if(!search) search = ''
+ return fuzzysort.prepareLowerCodes(search)
+ },
+
+
+
+ // Below this point is only internal code
+ // Below this point is only internal code
+ // Below this point is only internal code
+ // Below this point is only internal code
+
+
+
+ getPrepared: function(target) {
+ if(target.length > 999) return fuzzysort.prepare(target) // don't cache huge targets
+ var targetPrepared = preparedCache.get(target)
+ if(targetPrepared !== undefined) return targetPrepared
+ targetPrepared = fuzzysort.prepare(target)
+ preparedCache.set(target, targetPrepared)
+ return targetPrepared
+ },
+ getPreparedSearch: function(search) {
+ if(search.length > 999) return fuzzysort.prepareSearch(search) // don't cache huge searches
+ var searchPrepared = preparedSearchCache.get(search)
+ if(searchPrepared !== undefined) return searchPrepared
+ searchPrepared = fuzzysort.prepareSearch(search)
+ preparedSearchCache.set(search, searchPrepared)
+ return searchPrepared
+ },
+
+ algorithm: function(searchLowerCodes, prepared, searchLowerCode) {
+ var targetLowerCodes = prepared._targetLowerCodes
+ var searchLen = searchLowerCodes.length
+ var targetLen = targetLowerCodes.length
+ var searchI = 0 // where we at
+ var targetI = 0 // where you at
+ var typoSimpleI = 0
+ var matchesSimpleLen = 0
+
+ // very basic fuzzy match; to remove non-matching targets ASAP!
+ // walk through target. find sequential matches.
+ // if all chars aren't found then exit
+ for(;;) {
+ var isMatch = searchLowerCode === targetLowerCodes[targetI]
+ if(isMatch) {
+ matchesSimple[matchesSimpleLen++] = targetI
+ ++searchI; if(searchI === searchLen) break
+ searchLowerCode = searchLowerCodes[typoSimpleI===0?searchI : (typoSimpleI===searchI?searchI+1 : (typoSimpleI===searchI-1?searchI-1 : searchI))]
+ }
+
+ ++targetI; if(targetI >= targetLen) { // Failed to find searchI
+ // Check for typo or exit
+ // we go as far as possible before trying to transpose
+ // then we transpose backwards until we reach the beginning
+ for(;;) {
+ if(searchI <= 1) return null // not allowed to transpose first char
+ if(typoSimpleI === 0) { // we haven't tried to transpose yet
+ --searchI
+ var searchLowerCodeNew = searchLowerCodes[searchI]
+ if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char
+ typoSimpleI = searchI
+ } else {
+ if(typoSimpleI === 1) return null // reached the end of the line for transposing
+ --typoSimpleI
+ searchI = typoSimpleI
+ searchLowerCode = searchLowerCodes[searchI + 1]
+ var searchLowerCodeNew = searchLowerCodes[searchI]
+ if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char
+ }
+ matchesSimpleLen = searchI
+ targetI = matchesSimple[matchesSimpleLen - 1] + 1
+ break
+ }
+ }
+ }
+
+ var searchI = 0
+ var typoStrictI = 0
+ var successStrict = false
+ var matchesStrictLen = 0
+
+ var nextBeginningIndexes = prepared._nextBeginningIndexes
+ if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target)
+ var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
+
+ // Our target string successfully matched all characters in sequence!
+ // Let's try a more advanced and strict test to improve the score
+ // only count it as a match if it's consecutive or a beginning character!
+ if(targetI !== targetLen) for(;;) {
+ if(targetI >= targetLen) {
+ // We failed to find a good spot for this search char, go back to the previous search char and force it forward
+ if(searchI <= 0) { // We failed to push chars forward for a better match
+ // transpose, starting from the beginning
+ ++typoStrictI; if(typoStrictI > searchLen-2) break
+ if(searchLowerCodes[typoStrictI] === searchLowerCodes[typoStrictI+1]) continue // doesn't make sense to transpose a repeat char
+ targetI = firstPossibleI
+ continue
+ }
+
+ --searchI
+ var lastMatch = matchesStrict[--matchesStrictLen]
+ targetI = nextBeginningIndexes[lastMatch]
+
+ } else {
+ var isMatch = searchLowerCodes[typoStrictI===0?searchI : (typoStrictI===searchI?searchI+1 : (typoStrictI===searchI-1?searchI-1 : searchI))] === targetLowerCodes[targetI]
+ if(isMatch) {
+ matchesStrict[matchesStrictLen++] = targetI
+ ++searchI; if(searchI === searchLen) { successStrict = true; break }
+ ++targetI
+ } else {
+ targetI = nextBeginningIndexes[targetI]
+ }
+ }
+ }
+
+ { // tally up the score & keep track of matches for highlighting later
+ if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen }
+ else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen }
+ var score = 0
+ var lastTargetI = -1
+ for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i]
+ // score only goes down if they're not consecutive
+ if(lastTargetI !== targetI - 1) score -= targetI
+ lastTargetI = targetI
+ }
+ if(!successStrict) {
+ score *= 1000
+ if(typoSimpleI !== 0) score += -20/*typoPenalty*/
+ } else {
+ if(typoStrictI !== 0) score += -20/*typoPenalty*/
+ }
+ score -= targetLen - searchLen
+ prepared.score = score
+ prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i]
+
+ return prepared
+ }
+ },
+
+ algorithmNoTypo: function(searchLowerCodes, prepared, searchLowerCode) {
+ var targetLowerCodes = prepared._targetLowerCodes
+ var searchLen = searchLowerCodes.length
+ var targetLen = targetLowerCodes.length
+ var searchI = 0 // where we at
+ var targetI = 0 // where you at
+ var matchesSimpleLen = 0
+
+ // very basic fuzzy match; to remove non-matching targets ASAP!
+ // walk through target. find sequential matches.
+ // if all chars aren't found then exit
+ for(;;) {
+ var isMatch = searchLowerCode === targetLowerCodes[targetI]
+ if(isMatch) {
+ matchesSimple[matchesSimpleLen++] = targetI
+ ++searchI; if(searchI === searchLen) break
+ searchLowerCode = searchLowerCodes[searchI]
+ }
+ ++targetI; if(targetI >= targetLen) return null // Failed to find searchI
+ }
+
+ var searchI = 0
+ var successStrict = false
+ var matchesStrictLen = 0
+
+ var nextBeginningIndexes = prepared._nextBeginningIndexes
+ if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target)
+ var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
+
+ // Our target string successfully matched all characters in sequence!
+ // Let's try a more advanced and strict test to improve the score
+ // only count it as a match if it's consecutive or a beginning character!
+ if(targetI !== targetLen) for(;;) {
+ if(targetI >= targetLen) {
+ // We failed to find a good spot for this search char, go back to the previous search char and force it forward
+ if(searchI <= 0) break // We failed to push chars forward for a better match
+
+ --searchI
+ var lastMatch = matchesStrict[--matchesStrictLen]
+ targetI = nextBeginningIndexes[lastMatch]
+
+ } else {
+ var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
+ if(isMatch) {
+ matchesStrict[matchesStrictLen++] = targetI
+ ++searchI; if(searchI === searchLen) { successStrict = true; break }
+ ++targetI
+ } else {
+ targetI = nextBeginningIndexes[targetI]
+ }
+ }
+ }
+
+ { // tally up the score & keep track of matches for highlighting later
+ if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen }
+ else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen }
+ var score = 0
+ var lastTargetI = -1
+ for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i]
+ // score only goes down if they're not consecutive
+ if(lastTargetI !== targetI - 1) score -= targetI
+ lastTargetI = targetI
+ }
+ if(!successStrict) score *= 1000
+ score -= targetLen - searchLen
+ prepared.score = score
+ prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i]
+
+ return prepared
+ }
+ },
+
+ prepareLowerCodes: function(str) {
+ var strLen = str.length
+ var lowerCodes = [] // new Array(strLen) sparse array is too slow
+ var lower = str.toLowerCase()
+ for(var i = 0; i < strLen; ++i) lowerCodes[i] = lower.charCodeAt(i)
+ return lowerCodes
+ },
+ prepareBeginningIndexes: function(target) {
+ var targetLen = target.length
+ var beginningIndexes = []; var beginningIndexesLen = 0
+ var wasUpper = false
+ var wasAlphanum = false
+ for(var i = 0; i < targetLen; ++i) {
+ var targetCode = target.charCodeAt(i)
+ var isUpper = targetCode>=65&&targetCode<=90
+ var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
+ var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
+ wasUpper = isUpper
+ wasAlphanum = isAlphanum
+ if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
+ }
+ return beginningIndexes
+ },
+ prepareNextBeginningIndexes: function(target) {
+ var targetLen = target.length
+ var beginningIndexes = fuzzysort.prepareBeginningIndexes(target)
+ var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
+ var lastIsBeginning = beginningIndexes[0]
+ var lastIsBeginningI = 0
+ for(var i = 0; i < targetLen; ++i) {
+ if(lastIsBeginning > i) {
+ nextBeginningIndexes[i] = lastIsBeginning
+ } else {
+ lastIsBeginning = beginningIndexes[++lastIsBeginningI]
+ nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
+ }
+ }
+ return nextBeginningIndexes
+ },
+
+ cleanup: cleanup,
+ new: fuzzysortNew,
+ }
+ return fuzzysort
+} // fuzzysortNew
+
+// This stuff is outside fuzzysortNew, because it's shared with instances of fuzzysort.new()
+var isNode = typeof require !== 'undefined' && typeof window === 'undefined'
+var MyMap = Map||function(){var s=Object.create(null);this.get=function(k){return s[k]};this.set=function(k,val){s[k]=val;return this};this.clear=function(){s=Object.create(null)}}
+var preparedCache = new MyMap()
+var preparedSearchCache = new MyMap()
+var noResults = []; noResults.total = 0
+var matchesSimple = []; var matchesStrict = []
+function cleanup() { preparedCache.clear(); preparedSearchCache.clear(); matchesSimple = []; matchesStrict = [] }
+function defaultScoreFn(a) {
+ var max = -9007199254740991
+ for (var i = a.length - 1; i >= 0; --i) {
+ var result = a[i]; if(result === null) continue
+ var score = result.score
+ if(score > max) max = score
+ }
+ if(max === -9007199254740991) return null
+ return max
+}
+
+// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
+// prop = 'key1.key2' 10ms
+// prop = ['key1', 'key2'] 27ms
+function getValue(obj, prop) {
+ var tmp = obj[prop]; if(tmp !== undefined) return tmp
+ var segs = prop
+ if(!Array.isArray(prop)) segs = prop.split('.')
+ var len = segs.length
+ var i = -1
+ while (obj && (++i < len)) obj = obj[segs[i]]
+ return obj
+}
+
+function isObj(x) { return typeof x === 'object' } // faster as a function
+
+// Hacked version of https://github.com/lemire/FastPriorityQueue.js
+var fastpriorityqueue=function(){var r=[],o=0,e={};function n(){for(var e=0,n=r[e],c=1;c<o;){var f=c+1;e=c,f<o&&r[f].score<r[c].score&&(e=f),r[e-1>>1]=r[e],c=1+(e<<1)}for(var a=e-1>>1;e>0&&n.score<r[a].score;a=(e=a)-1>>1)r[e]=r[a];r[e]=n}return e.add=function(e){var n=o;r[o++]=e;for(var c=n-1>>1;n>0&&e.score<r[c].score;c=(n=c)-1>>1)r[n]=r[c];r[n]=e},e.poll=function(){if(0!==o){var e=r[0];return r[0]=r[--o],n(),e}},e.peek=function(e){if(0!==o)return r[0]},e.replaceTop=function(o){r[0]=o,n()},e};
+var q = fastpriorityqueue() // reuse this, except for async, it needs to make its own
+
+return fuzzysortNew()
+}) // UMD
+
+// TODO: (performance) wasm version!?
+// TODO: (performance) threads?
+// TODO: (performance) avoid cache misses
+// TODO: (performance) preparedCache is a memory leak
+// TODO: (like sublime) backslash === forwardslash
+// TODO: (like sublime) spaces: "a b" should do 2 searches 1 for a and 1 for b
+// TODO: (scoring) garbage in targets that allows most searches to strict match need a penality
+// TODO: (performance) idk if allowTypo is optimized
diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js
index 508849e1..d6cc8128 100644
--- a/pydis_site/static/js/resources/resources.js
+++ b/pydis_site/static/js/resources/resources.js
@@ -8,6 +8,13 @@ var activeFilters = {
difficulty: []
};
+// Options for fuzzysort
+const fuzzysortOptions = {
+ allowTypo: true, // Allow our users to make typos
+ titleThreshold: -10000, // The threshold for the fuzziness on title matches. Closer to 0 is stricter.
+ descriptionThreshold: -500, // The threshold for the fuzziness on description matches.
+};
+
/* Add a filter, and update the UI */
function addFilter(filterName, filterItem) {
var filterIndex = activeFilters[filterName].indexOf(filterItem);
@@ -25,6 +32,7 @@ function removeAllFilters() {
"payment-tiers": [],
difficulty: []
};
+ $("#resource-search input").val("");
updateUI();
}
@@ -51,6 +59,13 @@ function noFilters() {
function deserializeURLParams() {
let searchParams = new window.URLSearchParams(window.location.search);
+ // Add the search query to the search bar.
+ if (searchParams.has("search")) {
+ let searchQuery = searchParams.get("search");
+ $("#resource-search input").val(searchQuery);
+ $(".close-filters-button").show();
+ }
+
// Work through the parameters and add them to the filter object
$.each(Object.keys(activeFilters), function(_, filterType) {
let paramFilterContent = searchParams.get(filterType);
@@ -62,11 +77,13 @@ function deserializeURLParams() {
// 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.
+ // Catch special cases.
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";
+
+ // If the filter is valid, mirror it to the UI.
} 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}']`);
@@ -91,10 +108,23 @@ function deserializeURLParams() {
});
}
+/* Show or hide the duckies, depending on whether or not there are any resources visible. */
+function updateDuckies() {
+ let visibleResources = Boolean($(".resource-box:visible").length);
+ if (!visibleResources) {
+ $(".no-resources-found").show();
+ } else {
+ $(".no-resources-found").hide();
+ }
+}
+
+
/* 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()) {
+ let searchQuery = $("#resource-search input").val();
+
+ // If there's no active filtering parameters, we can return early.
+ if (noFilters() && searchQuery.length === 0) {
window.history.replaceState(null, document.title, './');
return;
}
@@ -107,10 +137,44 @@ function updateURL() {
}
});
+ // Add the search query, if necessary.
+ if (searchQuery.length > 0) {
+ searchParams.set("search", searchQuery);
+ }
+
// Now update the URL
window.history.replaceState(null, document.title, `?${searchParams.toString()}`);
}
+/* Apply search terms */
+function filterBySearch(resourceItems) {
+ let searchQuery = $("#resource-search input").val();
+
+ /* Show and update the tag if there's a search query */
+ if (searchQuery) {
+ let tag = $(".tag.search-query");
+ let tagText = $(".tag.search-query span");
+ tagText.text(`Search: ${searchQuery}`);
+ tag.show();
+ $(".close-filters-button").show();
+ }
+
+ resourceItems.filter(function() {
+ // Get the resource title and description
+ let title = $(this).attr("data-resource-name");
+ let description = $(this).find("p").text();
+
+ // Run a fuzzy search. Does the title or description match the query?
+ let titleMatch = fuzzysort.single(searchQuery, title, fuzzysortOptions);
+ titleMatch = Boolean(titleMatch) && titleMatch.score > fuzzysortOptions.titleThreshold;
+
+ let descriptionMatch = fuzzysort.single(searchQuery, description, fuzzysortOptions);
+ descriptionMatch = Boolean(descriptionMatch) && descriptionMatch.score > fuzzysortOptions.descriptionThreshold;
+
+ return titleMatch || descriptionMatch;
+ }).show();
+}
+
/* Update the resources to match 'active_filters' */
function updateUI() {
let resources = $('.resource-box');
@@ -118,19 +182,31 @@ function updateUI() {
let resourceTags = $('.resource-tag');
let noTagsSelected = $(".no-tags-selected.tag");
let closeFiltersButton = $(".close-filters-button");
+ let searchQuery = $("#resource-search input").val();
+ let searchTag = $(".tag.search-query");
// Update the URL to match the new filters.
updateURL();
// If there's nothing in the filters, we can return early.
if (noFilters()) {
- resources.show();
+ // If we have a searchQuery, we need to run all resources through a search.
+ if (searchQuery.length > 0) {
+ resources.hide();
+ noTagsSelected.hide();
+ filterBySearch(resources);
+ } else {
+ resources.show();
+ noTagsSelected.show();
+ closeFiltersButton.hide();
+ $(".tag.search-query").hide();
+ }
+
filterTags.hide();
- noTagsSelected.show();
- closeFiltersButton.hide();
resourceTags.removeClass("active");
$(`.filter-checkbox:checked`).prop("checked", false);
- $(".no-resources-found").hide();
+ updateDuckies();
+
return;
} else {
// Hide everything
@@ -158,9 +234,8 @@ function updateUI() {
}
// Otherwise, hide everything and then filter the resources to decide what to show.
- let hasMatches = false;
resources.hide();
- resources.filter(function() {
+ let filteredResources = resources.filter(function() {
let validation = {
topics: false,
type: false,
@@ -187,20 +262,22 @@ function updateUI() {
// 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();
+ // Run the items we've found through the search filter, if necessary.
+ if (searchQuery.length > 0) {
+ filterBySearch(filteredResources);
} else {
- $(".no-resources-found").hide();
+ filteredResources.show();
+ searchTag.hide();
}
+
+ // Gotta update those duckies!
+ updateDuckies();
}
// Executed when the page has finished loading.
@@ -230,6 +307,11 @@ document.addEventListener("DOMContentLoaded", function () {
setTimeout(() => { categoryHeaders.removeClass("no-transition"); }, 10);
}
+ // When you type into the search bar, trigger an UI update.
+ $("#resource-search input").on("input", function() {
+ updateUI();
+ });
+
// 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));
diff --git a/pydis_site/templates/resources/resource_box.html b/pydis_site/templates/resources/resource_box.html
index e26203e9..5ca46296 100644
--- a/pydis_site/templates/resources/resource_box.html
+++ b/pydis_site/templates/resources/resource_box.html
@@ -2,7 +2,7 @@
{% load to_kebabcase %}
{% load get_category_icon %}
-<div class="box resource-box {{ resource.css_classes }}">
+<div class="box resource-box {{ resource.css_classes }}" data-resource-name="{{ resource.name }}">
{% if 'title_url' in resource %}
<a href="{{ resource.title_url }}">
{% include "resources/resource_box_header.html" %}
diff --git a/pydis_site/templates/resources/resource_box_header.html b/pydis_site/templates/resources/resource_box_header.html
index 84e1a79b..dfbdd92f 100644
--- a/pydis_site/templates/resources/resource_box_header.html
+++ b/pydis_site/templates/resources/resource_box_header.html
@@ -17,8 +17,7 @@
<span class="is-size-4 has-text-weight-bold">
{% if 'title_image' in resource %}
<img src="{{ resource.title_image }}" alt="" style="height: 50px; {{ resource.title_image_style }}">
- {% endif %}
- {% if 'name' in resource %}
+ {% elif 'name' in resource %}
{{ resource.name }}
{% endif %}
</span>
diff --git a/pydis_site/templates/resources/resources.html b/pydis_site/templates/resources/resources.html
index 70fad097..101f9965 100644
--- a/pydis_site/templates/resources/resources.html
+++ b/pydis_site/templates/resources/resources.html
@@ -16,6 +16,7 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script defer src="{% static "js/resources/resources.js" %}"></script>
<script defer src="{% static "js/collapsibles.js" %}"></script>
+ <script defer src="{% static "js/fuzzysort/fuzzysort.js" %}"></script>
{% endblock %}
{% block content %}
@@ -27,18 +28,33 @@
<div class="column filtering-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">Filter Resources</p>
+ <p class="panel-heading has-text-centered" id="filter-panel-header">Filter resources</p>
+
+ {# Search bar #}
+ <p id="resource-search" class="control has-icons-left">
+ <input class="input" placeholder="Search resources ">
+ <span class="icon is-small is-left">
+ <i class="fas fa-magnifying-glass"></i>
+ </span>
+ </p>
+
{# Filter box tags #}
<div class="card filter-tags">
<div class="is-flex ml-auto">
- <div>
+ <div id="tag-pool">
{# A filter tag for when there are no filters active #}
- <span class="no-tags-selected tag has-background-disabled has-text-disabled ml-2 mt-2">
- <i class="fas fa-ban mr-1"></i>
+ <span class="tag no-tags-selected is-secondary ml-2 mt-2">
+ <i class="fas fa-fw fa-ban mr-1"></i>
No filters selected
</span>
+ {# A filter tag for search queries #}
+ <span class="tag search-query is-secondary ml-2 mt-2">
+ <i class="fas fa-fw fa-magnifying-glass mr-1"></i>
+ <span class="tag inner">Search: ...</span>
+ </span>
+
{% for filter_name, filter_data in filters.items %}
{% for filter_item in filter_data.filters %}
{% if filter_name == "Difficulty" %}
@@ -152,7 +168,7 @@
{# Resource cards #}
<div class="content is-flex is-justify-content-center">
- <div>
+ <div class="container is-fullwidth">
{% for resource in resources.values %}
{% include "resources/resource_box.html" %}
{% endfor %}