1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
|
(function () {
"use strict";
// TOGGLE DARK/LIGHT MODE
function initThemeToggle() {
const rootHtml = document.documentElement;
const toggleThemeBtn = document.getElementById("theme-toggle");
// If no saved theme, determine user preference, otherwise default to light
const savedTheme = localStorage.getItem("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const initialTheme = savedTheme ?? (prefersDark ? "dark" : "light");
function setTheme(theme) {
const isDarkMode = theme === "dark";
// toggleThemeBtn dataset comes with translated labels for site's language
const label = isDarkMode ? toggleThemeBtn.dataset.labelLight : toggleThemeBtn.dataset.labelDark;
rootHtml.setAttribute("data-theme", theme);
toggleThemeBtn.setAttribute("aria-label", label); // display handled by CSS
}
// Apply initial theme
setTheme(initialTheme);
// Change theme on click and save user's choice
toggleThemeBtn.addEventListener("click", () => {
const newTheme = rootHtml.getAttribute("data-theme") === "dark" ? "light" : "dark";
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
});
}
// SEARCH
function initSearch() {
const input = document.getElementById("search-input");
const resetBtn = document.getElementById("search-reset");
const resultsCount = document.querySelector(".search-results__count");
const resultsList = document.querySelector(".search-results__list");
const template = document.getElementById("search-result-template");
// Only initialize search on the search page
if (!input || !template) return;
let allPosts = [];
let searchTimeout;
// Get JSON file with data of all posts
fetch(input.dataset.indexUrl ?? "/index.json")
.then((res) => res.json())
.then((data) => allPosts = data)
.catch((err) => console.error("Search index.json failed to load", err));
function clearResults() {
resultsCount.hidden = true;
resultsList.innerHTML = "";
resultsList.hidden = true;
}
function renderSearchResults(matches) {
clearResults();
// Display how many results found for query
resultsCount.hidden = false;
resultsCount.querySelector("#search-results-number").textContent = String(matches.length ?? 0);
// No posts matching query found
if (!matches.length) return;
// Hydrate post-card list item(s) from the template with JSON data
matches.forEach((post) => {
const li = template.content.firstElementChild.cloneNode(true);
const link = li.querySelector(".post-card__link");
link.href = post.url;
link.tile = post.title;
const date = li.querySelector(".post-card__publish-date");
date.textContent = new Date(post.date).toLocaleDateString();
date.setAttribute("datetime", post.date);
const summary = li.querySelector(".post-card__summary");
if (summary) summary.textContent = post.summary;
const tagsList = li.querySelector(".post-card__tags-list");
if (tagsList) {
tagsList.innerHTML = "";
if (post.tags && post.tags.length) {
post.tags.slice(0, 3).forEach((tag) => {
const liTag = document.createElement("li");
liTag.className = "post-card__tags-item";
liTag.textContent = `#${tag}`;
tagsList.appendChild(liTag);
});
if (post.tags.length > 3) {
const more = document.createElement("li");
more.className = "post-card__tags-item post-card__tags-more";
more.textContent = `+${post.tags.length - 3}`;
tagsList.appendChild(more);
}
}
}
resultsList.appendChild(li);
resultsList.hidden = false;
});
}
// Filter all posts for ones matching the user's search query
function searchPosts(query) {
const normalizedQuery = query.trim().toLowerCase();
// Only search if user entered at least 3 chars
if (normalizedQuery.length < 3) {
clearResults();
return;
}
const matches = allPosts.filter((post) => (
post.title.toLowerCase().includes(normalizedQuery) ||
(post.summary && post.summary.toLowerCase().includes(normalizedQuery))
));
// At least 1 post matching query found
renderSearchResults(matches);
}
input.addEventListener("input", (event) => {
// Debounce search
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchPosts(event.target.value);
}, 300);
});
resetBtn.addEventListener("click", () => {
input.value = "";
input.focus();
clearResults();
});
// Focus input on page load - it's what the user is here for
input.focus();
}
initThemeToggle();
initSearch();
})();
|