Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof of concept for client-side search support (WIP) #221

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ static/screenshots
static/*.js
static/*.xml
static/*.html
static/*.json
TODO.txt

#WebStorm IDE
.idea
.idea
8 changes: 7 additions & 1 deletion lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ async function getHelpersFromFiles() {
const texts = await Promise.all(
files.map(name => readFile(path.join(helpersDir, name)))
);
const helpers = texts.map(text => JSON.parse(text));

// Add an 'id' property for each helper
const helpers = texts.map(text => {
const helper = JSON.parse(text);
return { ...helper, id: toSlug(helper.name) }
});

return Object.fromEntries(helpers.map(h => [h.name, h]));
}

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tiny-helpers",
"version": "1.0.0",
"version": "1.1.0",
"description": "A collection of free single-purpose online tools for web developers...",
"main": "index.js",
"scripts": {
Expand Down
20 changes: 17 additions & 3 deletions scripts/create-feeds.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { toSlug } = require('../lib/slug');
(async () => {
const helpers = await getHelpers();
try {
const feed = new Feed({
const rssFeed = new Feed({
title: 'Tiny Helpers',
description,
id: 'https://tiny-helpers.dev/',
Expand All @@ -29,10 +29,13 @@ const { toSlug } = require('../lib/slug');
}
});

const searchItems = [];

helpers
.sort((a, b) => (new Date(a.addedAt) < new Date(b.addedAt) ? 1 : -1))
.forEach(({ addedAt, name, desc, url }) => {
feed.addItem({

rssFeed.addItem({
title: `New helper added: ${name} – ${desc}.`,
id: toSlug(name),
link: url,
Expand All @@ -41,10 +44,21 @@ const { toSlug } = require('../lib/slug');
date: new Date(addedAt),
image: `https://tiny-helpers.dev/screenshots/${toSlug(name)}@1.jpg`
});

// Lowercase the properties for search so it's safe to just match on lowercase
searchItems.push({
id: toSlug(name),
name: name.toLowerCase(),
desc: desc.toLowerCase(),
});
});

console.log('Writing rss feed');
writeFile(join('.', 'static', 'feed.xml'), feed.rss2());
await writeFile(join('.', 'static', 'feed.xml'), rssFeed.rss2());

console.log('Writing search data');
await writeFile(join('.', 'static', 'searchData.json'), JSON.stringify(searchItems), 'utf8');

} catch (error) {
console.error(error);
process.exit(1);
Expand Down
17 changes: 16 additions & 1 deletion site/_includes/base.njk
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@
<style>
{{ styles | safe }}
</style>

<script>
{
// Only if JS is enabled will the search input be displayed
document.write('<style>.js-search-visible { display: block !important; }</style>');

const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('q')) {
// If JS is enabled and there is a querystring search term then hide the helpers until we've filtered
document.write('<style>.js-helpers-hidden { display: none; }</style>');
}
}
</script>
</head>

<body>
Expand All @@ -60,6 +73,8 @@
{% include "js/main.js" %}
{% endset %}
<script type="module">{{ js | jsmin | safe }}</script>

<script src="/search.js"></script>
</body>

</html>
</html>
77 changes: 77 additions & 0 deletions site/_includes/js/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// This JavaScript is bundled together with the variable 'searchItems' from the
// file 'static/searchData.json'. This variable is an array and contains items with
// 'name' and 'desc' properties that are lowercase.

(function () {
const helpers = searchItems;

// Get the search term from the querystring
const urlParams = new URLSearchParams(window.location.search);
const hasSearchTerm = urlParams.has('q');
const querySearchTerm = hasSearchTerm ? urlParams.get('q').toLowerCase() : null;

const searchElement = document.getElementById('search');

// If no search input then not showing all items / filtering by tag so do nothing
if (searchElement) {
// Add the search input event listener with debounced handler and set value
const debounceSearchFunc = debounce(filterItems, 250);
searchElement.addEventListener('keydown', debounceSearchFunc);
searchElement.value = querySearchTerm;

// If there is a querystring search term then perform the initial filter
if (querySearchTerm) {
filterItems();
}
}

// Unhide the helpers list
const helpersList = document.getElementById('helpers-list');
helpersList.classList.remove('js-helpers-hidden');

function filterItems() {
const searchTerm = searchElement.value.toLowerCase();

console.time('Filtering helpers');

helpers.forEach(helper => {
const element = document.getElementById(helper.id);
const match = !searchTerm || helper.name.includes(searchTerm) || helper.desc.includes(searchTerm);

if (!match) {
element.style.display = 'none';
} else {
element.style.display = 'flex';
}
});

console.timeEnd('Filtering helpers');
}

// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
function debounce(func, wait, immediate) {
let timeout;

return function executedFunction() {
const context = this;
const args = arguments;

const later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};

const callNow = immediate && !timeout;

clearTimeout(timeout);

timeout = setTimeout(later, wait);

if (callNow) func.apply(context, args);
};
}

})();
16 changes: 14 additions & 2 deletions site/_includes/templates/_list.njk
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
<h1 class="fsi-1-5">Tools for tag "{{ tag.name }}"</h1>
{% endif %}

<div class="js-search-visible" style="display: none;">
TEMP SEARCH UI
{% if tag.name == "All" %}
<form>
<label for="search">Search</label>
<input type="text" id="search" style="font-size: 1.1rem; padding: 6px 8px;">
</form>
{% else %}
Search not displayed as not on "All" page
{% endif %}
</div>

<div class="btnGroup">
<a
class="btnGroup--btn {% if renderData.listIsSortedBy == "name" %}isActive{% endif %}"
Expand All @@ -15,9 +27,9 @@
href="{{ tag.slug | route({ listIsSortedBy: 'addedAt' }) }}">Sort by date</a>
</div>
</div>
<ol class="main-grid">
<ol id="helpers-list" class="main-grid js-helpers-hidden">
{% for item in tag.items | sort(renderData.listIsReversed, false, renderData.listIsSortedBy )%}
<li class="shadow-full bg-lighter flex-column p-relative">
<li id="{{ item.id }}" class="shadow-full bg-lighter flex-column p-relative">
<img
srcset="/screenshots/{{ item.slug }}@1.jpg 1x,
/screenshots/{{ item.slug }}@2.jpg 2x"
Expand Down
10 changes: 10 additions & 0 deletions site/search-javascript.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
permalink: search.js
eleventyExcludeFromCollections: true
---
const searchItems = {% include "../static/searchData.json" %};

{% set js %}
{% include "js/search.js" %}
{% endset %}
{{ js | safe }}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do jsmin here to reduce the JS size down.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we should def do that. :)