Skip to content

Commit b7396ff

Browse files
authored
Merge pull request #472 from jamstack/zl/filters
Adds sorting and filters to Headless CMS and Site Generator pages
2 parents 2cada29 + e58c23c commit b7396ff

38 files changed

+398
-62
lines changed

.eleventy.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
const lodashGet = require("lodash/get");
2+
const yaml = require("js-yaml");
23

34
module.exports = function(eleventyConfig) {
4-
55
// Support yaml data files
6-
const yaml = require("js-yaml");
76
eleventyConfig.addDataExtension("yaml", contents => yaml.safeLoad(contents))
87

98
// pass images directly through to the output
@@ -69,9 +68,37 @@ module.exports = function(eleventyConfig) {
6968

7069
// filter a data array based on the value of a property
7170
eleventyConfig.addFilter('select', (array, clause) => {
72-
const property = clause.split("=")[0];
73-
const value = clause.split("=")[1];
74-
return array.filter(item => item[property].includes(value));
71+
if(clause.indexOf("=") > -1) {
72+
const property = clause.split("=")[0];
73+
const value = clause.split("=")[1];
74+
return array.filter(item => lodashGet(item, property).includes(value));
75+
} else {
76+
return array.map(item => lodashGet(item, clause));
77+
}
78+
});
79+
80+
eleventyConfig.addFilter('flatten', (array) => {
81+
let results = [];
82+
for(let result of array) {
83+
if(result) {
84+
if(Array.isArray(result)) {
85+
results = [...results, ...result];
86+
} else {
87+
results.push(result);
88+
}
89+
}
90+
}
91+
return results;
92+
});
93+
94+
eleventyConfig.addFilter('unique', (array) => {
95+
let caseInsensitive = {};
96+
for(let val of array) {
97+
if(typeof val === "string") {
98+
caseInsensitive[val.toLowerCase()] = val;
99+
}
100+
}
101+
return Object.values(caseInsensitive);
75102
});
76103

77104
// Get a random selection of items from an array

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"postcss": "^8.1.10",
4040
"postcss-cli": "^8.3.0",
4141
"postcss-import": "^13.0.0",
42+
"spdx-correct": "^3.1.1",
4243
"tailwindcss": "^2.0.1"
4344
},
4445
"devDependencies": {

src/11ty/normalizeLicense.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const correct = require("spdx-correct");
2+
3+
module.exports = function(license, title) {
4+
let correctedLicenseName = correct(license);
5+
if(correctedLicenseName) {
6+
return correctedLicenseName;
7+
}
8+
console.log( `WARN License name (${license}) for ${title} not found on the approved SPDX License List: https://spdx.org/licenses/` );
9+
return license;
10+
};

src/css/tailwind.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,15 @@ details[open] .summary-swap-open {
314314
.tool-content img {
315315
@apply my-8;
316316
}
317+
318+
/* Filters */
319+
.filter-opensource--hide,
320+
.filter-typeofcms--hide,
321+
.filter-language--hide,
322+
.filter-template--hide,
323+
.filter-license--hide {
324+
display: none !important;
325+
}
317326
/* purgecss end ignore */
318327

319328
@tailwind utilities;

src/js/filter-container.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
class FilterContainer extends HTMLElement {
2+
constructor() {
3+
super();
4+
this.attrs = {
5+
bind: "data-filter-bind",
6+
delimiter: "data-filter-delimiter",
7+
results: "data-filter-results",
8+
};
9+
this.classes = {
10+
hidden: "filter--hide",
11+
}
12+
}
13+
14+
connectedCallback() {
15+
this.results = this.querySelector(`[${this.attrs.results}]`);
16+
let formElements = this.getAllFormElements();
17+
this.bindEvents(formElements);
18+
this.filterAll(formElements);
19+
}
20+
21+
getAllFormElements() {
22+
return this.querySelectorAll(`[${this.attrs.bind}]`);
23+
}
24+
25+
getAllKeys() {
26+
let keys = new Set();
27+
for(let formEl of this.getAllFormElements()) {
28+
keys.add(formEl.getAttribute(this.attrs.bind));
29+
}
30+
return Array.from(keys);
31+
}
32+
33+
getElementSelector(key) {
34+
return `data-filter-${key}`
35+
}
36+
37+
getAllFilterableElements() {
38+
let keys = this.getAllKeys();
39+
let selector = keys.map(key => {
40+
return `[${this.getElementSelector(key)}]`;
41+
}).join(",");
42+
return this.querySelectorAll(selector);
43+
}
44+
45+
bindEvents(formElements) {
46+
for(let el of formElements) {
47+
el.addEventListener("change", e => {
48+
this.filter(e.target);
49+
requestAnimationFrame(() => {
50+
this.renderResultCount();
51+
})
52+
}, false);
53+
}
54+
}
55+
56+
filterAll(formElements) {
57+
for(let el of formElements) {
58+
this.filter(el);
59+
}
60+
this.renderResultCount();
61+
}
62+
63+
filter(formElement) {
64+
let key = formElement.getAttribute(this.attrs.bind);
65+
let delimiter = formElement.getAttribute(this.attrs.delimiter);
66+
67+
let value = formElement.value;
68+
let elementsSelectorAttr = this.getElementSelector(key);
69+
70+
let elements = this.querySelectorAll(`[${elementsSelectorAttr}]`);
71+
let count = 0;
72+
let cls = `filter-${key}--hide`;
73+
for(let element of Array.from(elements)) {
74+
if(this.elementIsValid(element, elementsSelectorAttr, value, delimiter)) {
75+
element.classList.remove(cls);
76+
} else {
77+
element.classList.add(cls);
78+
}
79+
}
80+
}
81+
82+
elementIsValid(element, attributeName, value, delimiter) {
83+
if(!value && element.hasAttribute(attributeName)) {
84+
return true;
85+
}
86+
let attrValue = element.getAttribute(attributeName);
87+
if(delimiter && attrValue.split(delimiter).indexOf(value) > -1) {
88+
return true;
89+
}
90+
if(!delimiter && attrValue === value) {
91+
return true;
92+
}
93+
return false;
94+
}
95+
96+
elementIsVisible(element) {
97+
for(let cls of element.classList) {
98+
if(cls.startsWith("filter-") && cls.endsWith("--hide")) {
99+
return false;
100+
}
101+
}
102+
return true;
103+
}
104+
105+
renderResultCount() {
106+
let count = Array.from(this.getAllFilterableElements())
107+
.filter(entry => this.elementIsVisible(entry))
108+
.length;
109+
110+
this.results.innerHTML = `${count} Result${count !== 1 ? "s" : ""}`;
111+
}
112+
}
113+
114+
window.customElements.define("filter-container", FilterContainer);

src/js/sort-container.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
class SortContainer extends HTMLElement {
2+
constructor() {
3+
super();
4+
this.attrs = {
5+
select: "data-sort",
6+
children: "data-sort-children",
7+
};
8+
}
9+
10+
connectedCallback() {
11+
this.select = this.querySelector(`[${this.attrs.select}]`);
12+
this.bindEvents();
13+
}
14+
15+
bindEvents() {
16+
this.select.addEventListener("change", e => {
17+
this.sort(e.target.value);
18+
}, false);
19+
}
20+
21+
sort(key) {
22+
let container = this.querySelector(`[${this.attrs.children}]`);
23+
// Thanks https://github.com/component/sort (MIT License)
24+
let arr = [].slice.call(container.children).sort((a, b) => {
25+
let aVal = a.getAttribute(`data-sort-${key}`);
26+
let bVal = b.getAttribute(`data-sort-${key}`);
27+
28+
// numeric sorts
29+
if(key.endsWith("-numeric") || key.endsWith("-numeric-ascending") || key.endsWith("-numeric-descending")) {
30+
aVal = parseFloat(aVal) || 0;
31+
bVal = parseFloat(bVal) || 0;
32+
33+
if(key.endsWith("-numeric-descending")) {
34+
[aVal, bVal] = [bVal, aVal];
35+
}
36+
return aVal - bVal;
37+
}
38+
39+
if(bVal < aVal) {
40+
return 1;
41+
} else if(aVal < bVal) {
42+
return -1;
43+
}
44+
return 0;
45+
});
46+
let frag = document.createDocumentFragment();
47+
for (let i = 0; i < arr.length; i++) {
48+
frag.appendChild(arr[i]);
49+
}
50+
container.appendChild(frag);
51+
}
52+
}
53+
54+
window.customElements.define("sort-container", SortContainer);

src/site/_data/github.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,13 @@ async function getReposFromMarkdown(glob) {
102102
let split = fullRepo.split("/");
103103
let user = split[0];
104104
let repo = split[1];
105-
if(!matter.data.repohost || matter.data.repohost === "github")
106-
repos.push({ user, repo });
107-
} else {
108-
// TODO maybe just log this in production?
109-
// console.log( "GitHub full repo not found for", ssg );
105+
106+
if(!matter.data.repohost || matter.data.repohost === "github") {
107+
if(matter.data.disabled) {
108+
continue;
109+
}
110+
repos.push({ user, repo });
111+
}
110112
}
111113
}
112114

src/site/_includes/components/cards.njk

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
{% macro repo(item, loopIndex, githubData) %}
2-
<div id="{{item.id}}" class="generator-card flex flex-col h-full">
2+
<div id="{{item.id}}" class="generator-card flex flex-col h-full"
3+
data-sort-name="{{ item.data.title | lower }}"
4+
{%- if githubData[item.data.repo].stars !== "" %} data-sort-githubstars-numeric-descending="{{ githubData[item.data.repo].stars }}"{% endif %}
5+
{%- if item.data.typeofcms %} data-filter-typeofcms="{{ item.data.typeofcms | lower }}"{% endif %}
6+
{%- if item.data.opensource %} data-filter-opensource="{{ item.data.opensource | lower }}"{% endif %}
7+
data-filter-template="{{ (item.data.templates or []) | join(",") | lower }}"
8+
{%- if item.data.language %} data-filter-language="{{ item.data.language | join(",") | lower }}"{% endif %}
9+
{%- if item.data.license %} data-filter-license="{{ item.data.license | join(",") | lower }}"{% endif %}>
310
<a href="{{ item.url }}" class="flex-grow block border-0 bg-white text-gray-700 rounded-t-lg last:rounded-b-lg">
4-
<div class="text-xl md:text-2xl lg:text-3xl font-bold text-ellipsis text-black bg-gradient-to-b border-b rounded-t-lg p-4
11+
<div class="text-xl md:text-2xl lg:text-3xl font-bold text-ellipsis text-black bg-gradient-to-b border-b border-black rounded-t-lg p-4
512
{%- if loopIndex % 4 === 0 %} bg-gradient-card-sunrise
613
{%- elseif loopIndex % 4 === 1 %} bg-gradient-card-blue
714
{%- elseif loopIndex % 4 === 2 %} bg-gradient-card-seafoam
@@ -49,13 +56,13 @@
4956
<dt class="col-span-2 font-bold">Language:</dt>
5057
<dd class="col-span-3">{{ item.data.language }}</dd>
5158
{%- endif %}
52-
{%- if item.data.templates %}
59+
{%- if item.data.templates and item.data.templates.length > 0 %}
5360
<dt class="col-span-2 font-bold">Templates:</dt>
5461
<dd class="col-span-3">{{ item.data.templates | join(", ") }}</dd>
5562
{%- endif %}
56-
{%- if item.data.license %}
63+
{%- if item.data.license and item.data.license.length > 0 %}
5764
<dt class="col-span-2 font-bold">License:</dt>
58-
<dd class="col-span-3">{{ item.data.license }}</dd>
65+
<dd class="col-span-3">{{ item.data.license | join(", ") }}</dd>
5966
{%- endif %}
6067
{%- if item.data.typeofcms %}
6168
<dt class="col-span-2 font-bold">Type:</dt>
@@ -69,7 +76,7 @@
6976
</div>
7077
</a>
7178
{% if item.data.startertemplaterepo %}
72-
<a href="https://app.netlify.com/start/deploy?repository={{ item.data.startertemplaterepo}}" class="block px-4 py-3 text-gray-700 bg-white rounded-b-lg text-center md:text-lg whitespace-no-wrap border-t">
79+
<a href="https://app.netlify.com/start/deploy?repository={{ item.data.startertemplaterepo}}" class="block px-4 py-3 text-gray-700 bg-white rounded-b-lg text-center md:text-lg whitespace-nowrap border-t">
7380
<svg role="img" aria-hidden="true" focusable="false" width="30" height="30" viewBox="0 0 40 40" class="inline-block mr-1" fill="#36b0bb"><use xlink:href="#logo-netlify-gem"/></svg>
7481
Deploy to Netlify
7582
</a>

src/site/_includes/layouts/base.njk

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,7 @@ ogimage: "/img/og/default-og-image.png"
9292
</defs>
9393
</svg>
9494
<script type="module" src="/js/details-force-state.js"></script>
95+
<script type="module" src="/js/filter-container.js"></script>
96+
<script type="module" src="/js/sort-container.js"></script>
9597
</body>
9698
</html>

src/site/_includes/layouts/tool.njk

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,21 @@ layout: layouts/base.njk
3333
<dl class="inline-block mt-4 mb-4">
3434
{%- if homepage %}
3535
<dt class="sr-only">Home page</dt>
36-
<dd class="inline-block mr-6 whitespace-no-wrap">
36+
<dd class="inline-block mr-6 whitespace-nowrap">
3737
<svg role="img" aria-hidden="true" focusable="false" width="20" height="20" class="fill-current inline-block mr-1"><use xlink:href="#icon-home"/></svg>
3838
<a href="{{ homepage }}" target="_blank" rel="noopener">{{ homepage }}</a>
3939
</dd>
4040
{%- endif %}
4141
{%- if repo %}
4242
<dt class="sr-only">Repository</dt>
43-
<dd class="inline-block mr-6 whitespace-no-wrap">
43+
<dd class="inline-block mr-6 whitespace-nowrap">
4444
<svg role="img" aria-hidden="true" focusable="false" width="20" height="20" class="fill-current inline-block mr-1"><use xlink:href="#icon-github"/></svg>
4545
<a href="{% if not repohost or repohost == "github" %}https://github.com/{% endif %}{% if repohost == "gitlab" %}https://gitlab.com/{% endif %}{{ repo }}" target="_blank" rel="noopener">{{ repo }}</a>
4646
</dd>
4747
{%- endif %}
4848
{%- if twitter %}
4949
<dt class="sr-only">Twitter</dt>
50-
<dd class="inline-block mr-6 whitespace-no-wrap">
50+
<dd class="inline-block mr-6 whitespace-nowrap">
5151
<svg role="img" aria-hidden="true" focusable="false" width="20" height="20" class="fill-current inline-block mr-1"><use xlink:href="#icon-twitter"/></svg>
5252
<a href="https://twitter.com/{{ twitter }}/" target="_blank" rel="noopener">@{{ twitter }}</a>
5353
</dd>
@@ -62,11 +62,11 @@ layout: layouts/base.njk
6262
<dt class="inline-block font-bold">Language:</dt>
6363
<dd class="inline-block mr-6">{{ language }}</dd>
6464
{%- endif %}
65-
{%- if license %}
65+
{%- if license and license.length > 0 %}
6666
<dt class="inline-block font-bold">License:</dt>
67-
<dd class="inline-block mr-6">{{ license }}</dd>
67+
<dd class="inline-block mr-6">{{ license | join(", ") }}</dd>
6868
{%- endif %}
69-
{%- if templates %}
69+
{%- if templates and templates.length > 0 %}
7070
<dt class="inline-block font-bold">Templates:</dt>
7171
<dd class="inline mr-6">{{ templates | join(", ") }}</dd>
7272
{%- endif %}
@@ -89,7 +89,7 @@ layout: layouts/base.njk
8989

9090

9191
{% if startertemplaterepo %}
92-
<a href="https://app.netlify.com/start/deploy?repository={{ startertemplaterepo }}" class="inline-block my-8 px-4 py-3 text-gray-700 bg-white rounded-lg text-center md:text-lg whitespace-no-wrap border-t">
92+
<a href="https://app.netlify.com/start/deploy?repository={{ startertemplaterepo }}" class="inline-block my-8 px-4 py-3 text-gray-700 bg-white rounded-lg text-center md:text-lg whitespace-nowrap border-t">
9393
<svg role="img" aria-hidden="true" focusable="false" width="30" height="30" viewBox="0 0 40 40" class="inline-block mr-1" fill="#36b0bb"><use xlink:href="#logo-netlify-gem"/></svg>
9494
Deploy to Netlify
9595
</a>

0 commit comments

Comments
 (0)