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, "<") 17 .replace(/>/g, ">") 18 .replace(/"/g, """); 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.