PageSourceSearch

https://sxm-talks.com/js/local-news-view.js

js sxm-talks.com collected 2026-10-01 10:35:43 UTC 12,831 bytes, 386 lines download raw bytes

1(function () {
2  var STORAGE_KEY = "local-news-view";
3  var PAGE_SIZE = { grid: 9, list: 18 };
4
5  function formatDate(dateStr) {
6    return new Date(dateStr).toLocaleDateString("en-US", {
7      month: "long",
8      day: "2-digit",
9      year: "numeric",
10    });
11  }
12
13  function escapeHtml(value) {
14    return String(value)
15      .replace(/&/g, "&")
16      .replace(/</g, "&lt;")
17      .replace(/>/g, "&gt;")
18      .replace(/"/g, "&quot;");
19  }
20
21  function renderListItem(post) {
22    var url = "/" + post.categorySlug + "/" + post.slug;
23    return (
24      '<article class="group flex gap-3 py-4 border-b border-black/10">' +
25      '<a href="' +
26      url +
27      '" class="shrink-0 w-[72px] h-[72px] rounded-sm overflow-hidden img-zoom bg-gray-200" aria-label="' +
28      escapeHtml(post.title) +
29      '">' +
30      '<img src="' +
31      escapeHtml(post.thumbnail) +
32      '" alt="" width="72" height="72" class="w-full h-full object-cover" loading="lazy" decoding="async" />' +
33      "</a>" +
34      '<div class="min-w-0 flex flex-col gap-1">' +
35      '<a href="' +
36      url +
37      '" class="font-semibold text-[13px] leading-snug line-clamp-3 group-hover:underline">' +
38      escapeHtml(post.title) +
39      "</a>" +
40      '<p class="text-[11px] text-black/50">' +
41      escapeHtml(formatDate(post.postDate)) +
42      "</p>" +
43      "</div>" +
44      "</article>"
45    );
46  }
47
48  function renderPostCard(post, className) {
49    var url = "/" + post.categorySlug + "/" + post.slug;
50    return (
51      '<a href="' +
52      url +
53      '" class="group w-full h-max grid grid-cols-1 ' +
54      (className || "") +
55      '">' +
56      '<div class="w-full relative overflow-hidden img-zoom rounded-sm aspect-[4.62/2.4]">' +
57      '<img src="' +
58      escapeHtml(post.thumbnail) +
59      '" alt="" width="311" height="175" class="w-full h-full object-cover absolute top-0 left-0" loading="lazy" decoding="async" />' +
60      "</div>" +
61      '<p class="text-[11px] uppercase tracking-[0.08em] text-black/70 font-medium mt-6 mb-3">' +
62      escapeHtml(formatDate(post.postDate)) +
63      "</p>" +
64      '<p class="font-semibold line-clamp-2 text-xl leading-snug group-hover:underline">' +
65      escapeHtml(post.title) +
66      "</p>" +
67      (post.excerpt
68        ? '<p class="leading-relaxed text-black/75 line-clamp-2 text-[15px] mt-3">' +
69        escapeHtml(post.excerpt) +
70        "</p>"
71        : "") +
72      "</a>"
73    );
74  }
75
76  function renderSidebarItem(post, index) {
77    var url = "/" + post.categorySlug + "/" + post.slug;
78    var imageBlock =
79      index === 0
80        ? '<a class="w-full img-zoom rounded-sm block" href="' +
81        url +
82        '" aria-label="' +
83        escapeHtml(post.title) +
84        '">' +
85        '<img src="' +
86        escapeHtml(post.thumbnail) +
87        '" alt="" width="630" height="420" class="w-full object-cover aspect-[4.62/2.4]" loading="lazy" decoding="async" />' +
88        "</a>"
89        : "";
90    var excerpt =
91      index === 0 && post.excerpt
92        ? '<p class="text-[15px] text-black/75 line-clamp-2 leading-relaxed lg:hidden">' +
93        escapeHtml(post.excerpt) +
94        "</p>"
95        : "";
96    return (
97      '<div class="group w-full flex flex-col gap-4">' +
98      imageBlock +
99      '<p class="text-[11px] uppercase tracking-[0.08em] text-black/70 font-medium">' +
100      escapeHtml(formatDate(post.postDate)) +
101      "</p>" +
102      '<a class="font-semibold group-hover:underline text-[17px] line-clamp-2 leading-snug" href="' +
103      url +
104      '">' +
105      escapeHtml(post.title) +
106      "</a>" +
107      excerpt +
108      "</div>"
109    );
110  }
111
112  function renderGridPage(posts) {
113    if (!posts.length) {
114      return '<p class="py-8 text-[13px] text-black/40">No articles found.</p>';
115    }
116
117    var hero = posts[0];
118    var heroUrl = "/" + hero.categorySlug + "/" + hero.slug;
119    var cards = posts.slice(1, 3);
120    var sidebar = posts.slice(3, 9);
121
122    var cardsHtml = cards
123      .map(function (post, index) {
124        var border = index === 1 ? '<div class="hidden md:block bg-black w-px h-full"></div>' : "";
125        return border + renderPostCard(post, "");
126      })
127      .join("");
128
129    var sidebarHtml = sidebar.map(renderSidebarItem).join("");
130
131    return (
132      '<div class="grid h-full min-w-0">' +
133      '<div class="group grid min-w-0 lg:grid-cols-[minmax(0,280px)_minmax(0,1fr)] md:gap-10 gap-6">' +
134      '<div class="grid gap-8 h-max min-w-0 overflow-hidden max-lg:order-2">' +
135      '<div class="flex min-w-0 flex-col gap-3 overflow-hidden">' +
136      '<p class="text-[11px] uppercase tracking-[0.08em] text-black/70 font-medium">' +
137      escapeHtml(formatDate(hero.postDate)) +
138      "</p>" +
139      '<a href="' +
140      heroUrl +
141      '" class="font-semibold group-hover:underline block min-w-0 overflow-hidden wrap-break-word lg:text-[36px] text-2xl lg:line-clamp-4 line-clamp-2 leading-[1.15]">' +
142      escapeHtml(hero.title) +
143      "</a>" +
144      "</div>" +
145      (hero.excerpt
146        ? '<p class="text-[15px] text-black/75 min-w-0 overflow-hidden wrap-break-word md:line-clamp-3 line-clamp-2 leading-relaxed">' +
147        escapeHtml(hero.excerpt) +
148        "</p>"
149        : "") +
150      '<a href="' +
151      heroUrl +
152      '" class="inline-block px-5 py-3 border border-black/20 text-sm font-medium tracking-[-0.01em] hover:bg-black hover:text-white hover:border-black transition-all duration-300 w-max" aria-label="Read more: ' +
153      escapeHtml(hero.title) +
154      '">Read more<span class="sr-only">: ' +
155      escapeHtml(hero.title) +
156      "</span></a>" +
157      "</div>" +
158      '<a href="' +
159      heroUrl +
160      '" class="relative block w-full min-w-0 overflow-hidden img-zoom rounded-sm aspect-[4.62/2.4] max-lg:order-1 lg:aspect-auto lg:h-full lg:min-h-[240px]" aria-label="' +
161      escapeHtml(hero.title) +
162      '">' +
163      '<img src="' +
164      escapeHtml(hero.thumbnail) +
165      '" alt="" width="630" height="420" class="absolute inset-0 h-full w-full object-cover" loading="lazy" decoding="async" />' +
166      "</a>" +
167      "</div>" +
168      (cardsHtml ? '<div class="grid md:grid-cols-[1fr_1px_1fr] gap-10 mt-10">' + cardsHtml + "</div>" : "") +
169      "</div>" +
170      '<div class="hidden lg:block bg-black w-px h-full"></div>' +
171      '<div class="grid gap-8">' +
172      sidebarHtml +
173      "</div>"
174    );
175  }
176
177  function setToggleState(section, view) {
178    section.querySelectorAll("[data-local-news-view]").forEach(function (button) {
179      var isActive = button.getAttribute("data-local-news-view") === view;
180      button.classList.toggle("text-black", isActive);
181      button.classList.toggle("text-black/40", !isActive);
182      button.setAttribute("aria-pressed", isActive ? "true" : "false");
183    });
184  }
185
186  function initSection(section) {
187    if (section.dataset.localNewsInitialized === "true") return;
188    section.dataset.localNewsInitialized = "true";
189
190    var contentWrap = section.querySelector("[data-local-news-content]");
191    var gridView = section.querySelector("[data-local-news-grid]");
192    var listView = section.querySelector("[data-local-news-list]");
193    var listContainer = section.querySelector("[data-local-news-list-items]");
194    var loadingOverlay = section.querySelector("[data-local-news-loading]");
195    var prevBtn = section.querySelector("[data-local-news-prev]");
196    var nextBtn = section.querySelector("[data-local-news-next]");
197    var paginationWrap = section.querySelector("[data-local-news-pagination]");
198    var toggleButtons = section.querySelectorAll("[data-local-news-view]");
199
200    if (
201      !contentWrap ||
202      !gridView ||
203      !listView ||
204      !listContainer ||
205      !loadingOverlay ||
206      !prevBtn ||
207      !nextBtn
208    ) {
209      return;
210    }
211
212    var excludeIds = (section.getAttribute("data-exclude-ids") || "")
213      .split(",")
214      .filter(Boolean)
215      .map(function (id) {
216        return parseInt(id, 10);
217      })
218      .filter(function (id) {
219        return Number.isFinite(id);
220      });
221
222    var gridInitialHtml = gridView.innerHTML;
223    var currentView = "grid";
224    var pages = { grid: 1, list: 1 };
225    var totalPages = { grid: 1, list: 1 };
226    var loading = false;
227
228    function activeViewEl() {
229      return currentView === "grid" ? gridView : listView;
230    }
231
232    function updatePagination() {
233      var page = pages[currentView];
234      var total = totalPages[currentView];
235      if (paginationWrap) {
236        paginationWrap.classList.toggle("hidden", total <= 1);
237      }
238      prevBtn.disabled = loading || page <= 1;
239      nextBtn.disabled = loading || page >= total;
240    }
241
242    function setLoading(active) {
243      loading = active;
244      var viewEl = activeViewEl();
245      viewEl.classList.toggle("opacity-50", active);
246      viewEl.classList.toggle("pointer-events-none", active);
247      loadingOverlay.classList.toggle("hidden", !active);
248      loadingOverlay.classList.toggle("flex", active);
249      loadingOverlay.setAttribute("aria-hidden", active ? "false" : "true");
250      updatePagination();
251    }
252
253    function fetchPosts(view, page) {
254      var params = new URLSearchParams({
255        page: String(page),
256        limit: String(PAGE_SIZE[view]),
257      });
258      if (excludeIds.length > 0) {
259        params.set("excludeIds", excludeIds.join(","));
260      }
261      return fetch("/api/local-news?" + params.toString(), { cache: "no-store" }).then(
262        function (response) {
263          if (!response.ok) throw new Error("Request failed");
264          return response.json();
265        },
266      );
267    }
268
269    function renderView(view, page, data) {
270      pages[view] = data.page || page;
271      totalPages[view] = Math.max(1, data.totalPages || 1);
272
273      if (view === "grid") {
274        if (page === 1 && gridInitialHtml) {
275          gridView.innerHTML = gridInitialHtml;
276        } else {
277          gridView.innerHTML = renderGridPage(data.docs || []);
278        }
279      } else if (!data.docs || !data.docs.length) {
280        listContainer.innerHTML =
281          '<p class="py-8 text-[13px] text-black/40 col-span-full">No articles found.</p>';
282      } else {
283        listContainer.innerHTML = data.docs.map(renderListItem).join("");
284      }
285
286      if (view === currentView) {
287        updatePagination();
288      }
289    }
290
291    function loadView(view, page) {
292      if (loading) return;
293
294      var isFirstListLoad =
295        view === "list" && !listContainer.querySelector("article");
296
297      setLoading(true);
298
299      fetchPosts(view, page)
300        .then(function (data) {
301          renderView(view, page, data);
302        })
303        .catch(function () {
304          if (view === "grid" && page === 1 && gridInitialHtml) {
305            gridView.innerHTML = gridInitialHtml;
306          } else if (isFirstListLoad) {
307            listContainer.innerHTML =
308              '<p class="py-8 text-[13px] text-black/40 col-span-full">Unable to load articles.</p>';
309          }
310        })
311        .finally(function () {
312          setLoading(false);
313        });
314    }
315
316    function prefetchTotals(view) {
317      fetchPosts(view, 1)
318        .then(function (data) {
319          totalPages[view] = Math.max(1, data.totalPages || 1);
320          if (view === currentView) updatePagination();
321        })
322        .catch(function () {
323          /* keep default */
324        });
325    }
326
327    function setView(view) {
328      currentView = view === "list" ? "list" : "grid";
329      try {
330        localStorage.setItem(STORAGE_KEY, currentView);
331      } catch (error) {
332        /* ignore */
333      }
334
335      gridView.classList.toggle("hidden", currentView !== "grid");
336      listView.classList.toggle("hidden", currentView !== "list");
337      setToggleState(section, currentView);
338      updatePagination();
339
340      if (currentView === "list" && !listContainer.querySelector("article")) {
341        loadView("list", pages.list);
342      }
343    }
344
345    toggleButtons.forEach(function (button) {
346      button.addEventListener("click", function () {
347        setView(button.getAttribute("data-local-news-view") || "grid");
348      });
349    });
350
351    prevBtn.addEventListener("click", function () {
352      if (pages[currentView] <= 1) return;
353      loadView(currentView, pages[currentView] - 1);
354    });
355
356    nextBtn.addEventListener("click", function () {
357      if (pages[currentView] >= totalPages[currentView]) return;
358      loadView(currentView, pages[currentView] + 1);
359    });
360
361    prefetchTotals("grid");
362    prefetchTotals("list");
363
364    var savedView = "grid";
365    try {
366      savedView = localStorage.getItem(STORAGE_KEY) || "grid";
367    } catch (error) {
368      savedView = "grid";
369    }
370
371    setView(savedView);
372    updatePagination();
373  }
374
375  function init() {
376    document.querySelectorAll("[data-local-news-section]").forEach(initSection);
377  }
378
379  if (document.readyState === "loading") {
380    document.addEventListener("DOMContentLoaded", init);
381  } else {
382    init();
383  }
384
385  document.addEventListener("astro:page-load", init);
386})();

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.