1/* 2 * Home page episode filtering. 3 * 4 * The server renders page 1 of the episode list. As soon as the visitor types a 5 * query or picks a tag chip we swap in results filtered from /episodes.json, 6 * and swap the server-rendered markup back when the filter is cleared. Without 7 * JS the chips are still plain links to their tag pages. 8 */ 9(function () { 10 "use strict"; 11 12 var home = document.getElementById("home"); 13 if (!home) return; 14 15 var input = document.getElementById("ep-search"); 16 var tagRow = document.getElementById("tag-row"); 17 var list = document.getElementById("episode-list"); 18 var status = document.getElementById("list-count"); 19 var paginationWrap = document.getElementById("pagination-wrap"); 20 if (!input || !tagRow || !list || !status) return; 21 22 var PLAY_ICON = 23 '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" ' + 24 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-play">' + 25 '<polygon points="5 3 19 12 5 21 5 3"></polygon></svg>'; 26 27 var totalEpisodes = parseInt(home.dataset.episodeCount, 10) || 0; 28 var pristineList = list.innerHTML; 29 var episodes = null; 30 var loading = null; 31 var query = ""; 32 var activeTag = ""; 33 var debounce = null; 34 35 function escapeHtml(value) { 36 return String(value) 37 .replace(/&/g, "&") 38 .replace(/</g, "<") 39 .replace(/>/g, ">") 40 .replace(/"/g, """); 41 } 42 43 function load() { 44 if (episodes) return Promise.resolve(episodes); 45 if (!loading) { 46 var version = home.dataset.episodesVersion; 47 loading = fetch("/episodes.json" + (version ? "?v=" + version : "")) 48 .then(function (res) { 49 return res.json(); 50 }) 51 .then(function (data) { 52 episodes = data; 53 return data; 54 }) 55 .catch(function () { 56 episodes = []; 57 return episodes; 58 }); 59 } 60 return loading; 61 } 62 63 function matches(ep) { 64 if (activeTag) { 65 var wanted = activeTag.toLowerCase(); 66 var hit = (ep.tg || []).some(function (t) { 67 return String(t).toLowerCase() === wanted; 68 }); 69 if (!hit) return false; 70 } 71 if (!query) return true; 72 var haystack = [ep.t, ep.g, ep.b, ep.l, (ep.tg || []).join(" ")].join(" ").toLowerCase(); 73 return query.split(/\s+/).every(function (term) { 74 return haystack.indexOf(term) !== -1; 75 }); 76 } 77 78 function rowHtml(ep) { 79 var tags = (ep.tg || []).slice(0, 4); 80 return ( 81 '<a class="ep" href="' + escapeHtml(ep.u) + '">' + 82 '<div class="cover' + (ep.f ? "" : " is-fallback") + '">' + 83 '<img src="' + escapeHtml(ep.c) + '"' + 84 (ep.ss ? ' srcset="' + escapeHtml(ep.ss) + '" sizes="(max-width: 650px) 72px, (max-width: 1025px) 144px, 160px"' : "") + 85 ' alt="" loading="lazy"' + 86 (ep.w ? ' width="' + ep.w + '" height="' + ep.h + '"' : "") + 87 " />" + 88 '<div class="overlay"><span class="btn">' + PLAY_ICON + "</span></div>" + 89 "</div>" + 90 '<div class="body">' + 91 '<div class="meta">' + 92 (ep.l ? '<span class="num">' + escapeHtml(ep.l) + '</span><span class="dot"></span>' : "") + 93 "<span>" + escapeHtml(ep.d) + "</span>" + 94 "</div>" + 95 '<h3 class="title">' + escapeHtml(ep.t) + "</h3>" + 96 (ep.g ? '<div class="guest"><span class="with">with </span>' + escapeHtml(ep.g) + "</div>" : "") + 97 (ep.b ? '<p class="blurb">' + escapeHtml(ep.b) + "</p>" : "") + 98 (tags.length 99 ? '<div class="taglist">' + 100 tags 101 .map(function (t) { 102 return '<span class="t">#' + escapeHtml(t) + "</span>"; 103 }) 104 .join("") + 105 "</div>" 106 : "") + 107 "</div></a>" 108 ); 109 } 110 111 function setPaginationVisible(visible) { 112 if (paginationWrap) paginationWrap.hidden = !visible; 113 } 114 115 function showPristine() { 116 list.innerHTML = pristineList; 117 status.textContent = totalEpisodes + " episodes"; 118 setPaginationVisible(true); 119 } 120 121 function render() { 122 if (!query && !activeTag) { 123 showPristine(); 124 return; 125 } 126 127 setPaginationVisible(false); 128 load().then(function (data) { 129 if (!query && !activeTag) { 130 showPristine(); 131 return; 132 } 133 134 var hits = data.filter(matches); 135 list.innerHTML = hits.length 136 ? hits.map(rowHtml).join("") 137 : '<p class="eps-empty">No episodes match that. Try a different search or tag.</p>'; 138 139 var label; 140 if (query && activeTag) { 141 label = hits.length + ' results for "' + query + '" in #' + activeTag; 142 } else if (query) { 143 label = hits.length + ' results for "' + query + '"'; 144 } else { 145 label = hits.length + " episodes tagged #" + activeTag; 146 } 147 status.textContent = label; 148 }); 149 } 150 151 function syncChips() { 152 var chips = tagRow.querySelectorAll(".chip"); 153 for (var i = 0; i < chips.length; i++) { 154 var tag = chips[i].dataset.tag || ""; 155 chips[i].classList.toggle("active", tag.toLowerCase() === activeTag.toLowerCase()); 156 } 157 } 158 159 tagRow.addEventListener("click", function (event) { 160 var chip = event.target.closest(".chip"); 161 if (!chip || !("tag" in chip.dataset)) return; 162 event.preventDefault(); 163 var tag = chip.dataset.tag || ""; 164 activeTag = tag.toLowerCase() === activeTag.toLowerCase() ? "" : tag; 165 syncChips(); 166 render(); 167 }); 168 169 input.addEventListener("input", function () { 170 window.clearTimeout(debounce); 171 debounce = window.setTimeout(function () { 172 query = input.value.trim().toLowerCase(); 173 render(); 174 }, 120); 175 }); 176 177 input.addEventListener("keydown", function (event) { 178 if (event.key === "Escape") { 179 input.value = ""; 180 query = ""; 181 render(); 182 } 183 }); 184 185 document.addEventListener("keydown", function (event) { 186 if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { 187 event.preventDefault(); 188 input.focus(); 189 input.select(); 190 } 191 }); 192 193 // Warm the index once the page is idle so the first keystroke feels instant. 194 if ("requestIdleCallback" in window) { 195 window.requestIdleCallback(load); 196 } else { 197 window.setTimeout(load, 1500); 198 } 199})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.