PageSourceSearch

https://unhandledexceptionpodcast.com/js/site.js?v=fcd397bd

js unhandledexceptionpodcast.com collected 2026-10-02 05:30:56 UTC 6,232 bytes, 199 lines download raw bytes

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, "&amp;")
38      .replace(/</g, "&lt;")
39      .replace(/>/g, "&gt;")
40      .replace(/"/g, "&quot;");
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.