1const proto = (await import("./protoDeterminer.js")).determineProto(); 2 3const debounce = (fn, wait) => { 4 let t; 5 return (...args) => { 6 clearTimeout(t); 7 t = setTimeout(() => fn(...args), wait); 8 }; 9}; 10 11const initializeTooltips = () => { 12 document 13 .querySelectorAll('[data-bs-toggle="tooltip"]') 14 .forEach((el) => new bootstrap.Tooltip(el)); 15}; 16 17const blogManager = { 18 config: { 19 pageNameAlt: "Blog", 20 postsPerPage: 12, 21 blogPostsEndpoint: `https://rails-admin.spillover.com/esites-api/v1/${proto}/site_posts?number_of_posts=1000`, 22 fetchedPostsLog: "Fetched blog posts:", 23 errorLoadingLog: "Error loading blog posts:", 24 postNotFoundLog: "Post not found.", 25 urlCopiedLog: "URL copied to clipboard:", 26 blogLinkNotification: "Blog Link Copied to Clipboard!", 27 shareHover: "Share blog to Facebook", 28 copyHover: "Copy blog link to clipboard", 29 }, 30 31 blogPosts: [], 32 selectedTags: [], 33 34 getBlogPostsContainer() { 35 return document.getElementById("blogPostsContainer"); 36 }, 37 38 isReferralMode() { 39 const container = this.getBlogPostsContainer(); 40 if (!container) return false; 41 42 return ( 43 container.hasAttribute("data-use-referral-links") || 44 container.hasAttribute("data-referral-links") || 45 container.hasAttribute("use-referral-links") 46 ); 47 }, 48 49 normalizeExternalUrl(url) { 50 const raw = (url ?? "").toString().trim(); 51 if (!raw) return ""; 52 53 if (/^https?:\/\//i.test(raw)) return raw; 54 if (/^\/\//.test(raw)) return `https:${raw}`; 55 56 return `https://${raw}`; 57 }, 58 59 getExcludedTags() { 60 const container = this.getBlogPostsContainer(); 61 if (!container) return []; 62 63 const raw = 64 container.getAttribute("data-exclude-tags") || 65 container.getAttribute("data-hide-tags") || 66 container.getAttribute("exclude-tags") || 67 ""; 68 69 return raw 70 .split(",") 71 .map((t) => t.trim().toLowerCase()) 72 .filter(Boolean); 73 }, 74 75 postHasExcludedTag(post) { 76 const excluded = this.getExcludedTags(); 77 if (!excluded.length) return false; 78 79 const tags = Array.isArray(post?.tags) ? post.tags : []; 80 const normalized = tags.map((t) => (t ?? "").toString().trim().toLowerCase()).filter(Boolean); 81 82 return normalized.some((t) => excluded.includes(t)); 83 }, 84 85 updateTagDropdownVisibility(tagCount) { 86 const blogTags = document.getElementById("blogTags"); 87 if (!blogTags) return; 88 89 const dropdownButton = document.getElementById("tagDropdownButton"); 90 if (!dropdownButton) return; 91 92 if (tagCount <= 1) { 93 dropdownButton.style.display = "none"; 94 blogTags.style.display = "none"; 95 } else { 96 dropdownButton.style.display = ""; 97 blogTags.style.display = ""; 98 } 99 }, 100 101 getPostById(id) { 102 const postId = parseInt(id, 10); 103 return this.blogPosts.find((p) => p.id === postId); 104 }, 105 106 getInternalPostUrl(postId) { 107 return `?post=${postId}`; 108 }, 109 110 async fetchBlogPosts() { 111 try { 112 const res = await fetch(this.config.blogPostsEndpoint); 113 if (!res.ok) throw new Error("Network response was not ok"); 114 115 const allPosts = await res.json(); 116 this.blogPosts = Array.isArray(allPosts) 117 ? allPosts.filter((p) => !this.postHasExcludedTag(p)) 118 : []; 119 } catch (e) { 120 console.error(this.config.errorLoadingLog, e); 121 } 122 }, 123 124 getAllTags() { 125 return Array.from(new Set(this.blogPosts.flatMap((p) => p.tags))); 126 }, 127 128 getTagsFromURL: () => 129 new URLSearchParams(window.location.search).get("tags")?.split(",") || [], 130 131 updateCheckboxesBasedOnTags(tags) {
132 tags.forEach((tag) => { 133 const cb = document.getElementById(`tag${tag}Checkbox`); 134 if (cb) cb.checked = true; 135 }); 136 }, 137 138 renderBlogPosts(posts) { 139 const container = this.getBlogPostsContainer(); 140 if (!container) return; 141 142 const referralMode = this.isReferralMode(); 143 144 container.innerHTML = posts 145 .map((p) => { 146 const normalizedReferral = this.normalizeExternalUrl(p.referral_link); 147 const hasReferral = Boolean(normalizedReferral); 148 const useReferral = referralMode && hasReferral; 149 150 const titleHref = useReferral ? normalizedReferral : this.getInternalPostUrl(p.id); 151 const readMoreHref = useReferral ? normalizedReferral : this.getInternalPostUrl(p.id); 152 153 const titleTarget = useReferral ? ` target="_blank" rel="noopener noreferrer"` : ""; 154 const readMoreTarget = useReferral ? ` target="_blank" rel="noopener noreferrer"` : ""; 155 156 const externalA11y = useReferral 157 ? ` aria-label="Open ${String(p.headline).replace(/"/g, """)} in a new tab"` 158 : ""; 159 160 const referralAttr = hasReferral 161 ? ` data-referral-link="${String(normalizedReferral).replace(/"/g, """)}"` 162 : ""; 163 164 return ` 165 <article class="col-lg-3 col-md-6 col-12" tabindex="0" aria-labelledby="post-title-${p.id}" role="region"> 166 <div class="uk-panel blog-post-card pointer" data-post-id="${p.id}"${referralAttr}> 167 <img src="${p.post_image}" class="blog-img" width="100%" height="100%" alt="${String(p.headline).replace(/"/g, """)}"> 168 <div class="blog-top"> 169 <p class="blog-tags pointer"> 170 ${p.tags 171 .map( 172 (tag) => 173 `<span class="blog-tag" data-tag="${String(tag).replace(/"/g, """)}" aria-label="Tag: ${String(tag).replace( 174 /"/g, 175 """ 176 )}">${tag}</span>` 177 ) 178 .join(", ")} 179 </p> 180 </div> 181 <div class="blog-bottom"> 182 <a href="${titleHref}" class="title-wrapper" aria-describedby="post-title-${p.id}"${titleTarget}${externalA11y}> 183 <p id="post-title-${p.id}" class="blog-title pointer truncate">${p.headline}</p> 184 </a> 185 <a href="${readMoreHref}" class="badge badge-template blog-button"${readMoreTarget}${externalA11y}>Read More</a> 186 <i class="fas fa-share ms-2 text-white pointer share-button" role="button" tabindex="0" aria-label="Share ${String(p.headline).replace( 187 /"/g, 188 """ 189 )}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="${this.config.shareHover}"></i> 190 <i class="fas fa-copy ms-2 text-white pointer copy-button" role="button" tabindex="0" aria-label="Copy link to ${String(p.headline).replace( 191 /"/g, 192 """ 193 )}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="${this.config.copyHover}"></i> 194 </div> 195 </div> 196 </article> 197`; 198 }) 199 .join(""); 200 201 this.addDynamicEventListeners(); 202 initializeTooltips(); 203 }, 204 205 generateTagChecklist() { 206 const list = document.getElementById("tagChecklist"); 207 const tags = this.getAllTags().sort(); 208 209 this.updateTagDropdownVisibility(tags.length); 210 211 if (!tags.length) { 212 const tagsWrap = document.getElementById("blogTags"); 213 if (tagsWrap) tagsWrap.style.display = "none"; 214 return; 215 } 216 217 if (!list) return; 218 219 list.innerHTML = tags 220 .map( 221 (t) => ` 222 <div class="form-check"> 223 <input type="checkbox" class="form-check-input tag-checkbox" value="${String(t).replace(/"/g, """)}" id="tag${String(t).replace( 224 /"/g, 225 """ 226 )}Checkbox" aria-label="Filter posts by ${String(t).replace(/"/g, """)} tag"> 227 <label class="form-check-label" for="tag${String(t).replace(/"/g, """)}Checkbox">${t}</label> 228 </div> 229` 230 ) 231 .join(""); 232 233 this.addDynamicEventListeners(); 234 }, 235 236 createSharePostLink(postId) { 237 const post = this.getPostById(postId); 238 if (!post) return console.error(this.config.postNotFoundLog); 239 240 const referralMode = this.isReferralMode();
241 const referralLink = this.normalizeExternalUrl(post.referral_link); 242 const hasReferral = Boolean(referralLink); 243 244 const urlToShare = 245 referralMode && hasReferral 246 ? referralLink 247 : `${window.location.href.split("?")[0]}?post=${post.id}`; 248 249 const shareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(urlToShare)}`; 250 window.open(shareUrl, "_blank", "noopener,noreferrer"); 251 }, 252 253 copyBlogLinkToClipboard(postId) { 254 const post = this.getPostById(postId); 255 if (!post) return console.error(this.config.postNotFoundLog); 256 257 const referralMode = this.isReferralMode(); 258 const referralLink = this.normalizeExternalUrl(post.referral_link); 259 const hasReferral = Boolean(referralLink); 260 261 const urlToCopy = 262 referralMode && hasReferral 263 ? referralLink 264 : `${window.location.href.split("?")[0]}?post=${post.id}`; 265 266 navigator.clipboard 267 .writeText(urlToCopy) 268 .then(() => this.showNotification(this.config.blogLinkNotification)) 269 .catch((e) => console.error("Error copying URL to clipboard:", e)); 270 }, 271 272 showNotification(message) { 273 const html = ` 274 <div class="alert alert-success alert-dismissible fade show" role="alert"> 275 ${message} 276 <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> 277 </div>`; 278 279 ["notificationContainer", "modalNotificationContainer"].forEach((id) => { 280 const container = document.getElementById(id); 281 if (!container) return; 282 283 const wrap = document.createElement("div"); 284 wrap.innerHTML = html; 285 container.appendChild(wrap); 286 287 setTimeout(() => { 288 if (wrap.parentNode === container) container.removeChild(wrap); 289 }, 3000); 290 }); 291 }, 292 293 showFullArticle(postId, pageTitle) { 294 const post = this.getPostById(postId); 295 if (!post) return console.error(this.config.postNotFoundLog); 296 297 document.title = `${ 298 document.querySelector('meta[property="og:site_name"]')?.content 299 ? `${document.querySelector('meta[property="og:site_name"]')?.content} | ` 300 : "" 301 }${post.headline}`; 302 303 const pageTitleEl = document.querySelector('[data-type="page-title"]'); 304 if (pageTitleEl) pageTitleEl.textContent = post.headline; 305 306 const published = new Date(post.published_at).toLocaleDateString(); 307 308 document.getElementById("blog").classList.add("showing-post"); 309 document.getElementById("blog").innerHTML = ` 310 <div class="container mt-5"> 311 <div id="notificationContainer" class="notification-container my-3"></div> 312 <div class="article-header" data-post-id="${post.id}"> 313 <div class="article-top"> 314 <nav aria-label="breadcrumb"> 315 <ol class="breadcrumb"> 316 <li class="breadcrumb-item"><a href="#" id="backToBlogs">${pageTitle}</a></li> 317 <li class="breadcrumb-item active">${post.headline}</li> 318 </ol> 319 </nav> 320 <div class="article-actions"> 321 <i class="fas fa-share ms-2 pointer share-button" role="button" tabindex="0" aria-label="Share ${String(post.headline).replace( 322 /"/g, 323 """ 324 )}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="${this.config.shareHover}"></i> 325 <i class="fas fa-copy ms-2 pointer copy-button" role="button" tabindex="0" aria-label="Copy link to ${String(post.headline).replace( 326 /"/g, 327 """ 328 )}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="${this.config.copyHover}"></i> 329 </div> 330 </div> 331 <div class="article-details"> 332 <p class="article-date"><span class="fw-bolder">Published:</span> ${published}</p> 333 <div class="blog-tags"> 334 ${post.tags 335 .map((t) => `<a href="?tags=${encodeURIComponent(t)}" class="badge badge-template me-1">${t}</a>`) 336 .join(" ")} 337 </div> 338 </div> 339 </div> 340 <div class="row justify-content-center"> 341 <div class="col-lg-10 d-flex justify-content-center"> 342 <div> 343 <img src="${post.post_image}" class="img-fluid mb-4" alt="${String(post.headline).replace(/"/g, """)}"> 344 <div>${post.caption}</div> 345 ${ 346 post.referral_link 347 ? `<div class="mt-4">
347<a href="${this.normalizeExternalUrl(post.referral_link)}" target="_blank" rel="noopener noreferrer" class="btn btn-template" aria-label="Open referral link in a new tab">See More</a></div>` 348 : "" 349 } 350 </div> 351 </div> 352 </div> 353 </div> 354`; 355 356 const back = document.getElementById("backToBlogs"); 357 if (back) { 358 back.addEventListener("click", (e) => { 359 e.preventDefault(); 360 this.backToBlogListing(); 361 }); 362 } 363 364 this.addDynamicEventListeners(); 365 initializeTooltips(); 366 }, 367 368 backToBlogListing() { 369 const url = new URL(window.location); 370 url.searchParams.delete("post"); 371 window.history.replaceState({}, document.title, url.pathname); 372 window.location.reload(); 373 }, 374 375 calculateTotalPages(posts) { 376 return Math.ceil(posts.length / this.config.postsPerPage); 377 }, 378 379 initFilterAndRenderBlogs() { 380 this.filterAndRenderBlogs = debounce( 381 function (page = 1) { 382 this.selectedTags = Array.from(document.querySelectorAll(".tag-checkbox:checked")).map((cb) => cb.value); 383 384 const url = new URL(window.location); 385 if (this.selectedTags.length) url.searchParams.set("tags", this.selectedTags.join(",")); 386 else url.searchParams.delete("tags"); 387 window.history.replaceState({}, document.title, url); 388 389 const filtered = this.selectedTags.length 390 ? this.blogPosts.filter((p) => this.selectedTags.some((tag) => p.tags.includes(tag))) 391 : this.blogPosts; 392 393 requestAnimationFrame(() => { 394 this.renderPaginatedBlogPosts(page, filtered); 395 this.renderPagination(this.calculateTotalPages(filtered), page); 396 this.updateRemoveFiltersButtonVisibility(); 397 this.setupReadFullArticleButtons(); 398 this.addDynamicEventListeners(); 399 400 const container = this.getBlogPostsContainer(); 401 if (container) { 402 window.scrollTo({ 403 top: container.getBoundingClientRect().top + window.scrollY - 250, 404 behavior: "smooth", 405 }); 406 } 407 }); 408 }.bind(this), 409 200 410 ); 411 }, 412 413 filterByTag(tag) { 414 const cb = document.getElementById(`tag${tag}Checkbox`); 415 if (cb) cb.checked = !cb.checked; 416 this.filterAndRenderBlogs(); 417 }, 418 419 updateRemoveFiltersButtonVisibility() { 420 const btn = document.getElementById("removeFiltersButton"); 421 if (!btn) return; 422 423 if (this.selectedTags.length) { 424 btn.style.display = "inline-block"; 425 setTimeout(() => btn.classList.add("visible"), 50); 426 } else { 427 btn.classList.remove("visible"); 428 setTimeout(() => (btn.style.display = "none"), 300); 429 } 430 }, 431 432 removeFilters() { 433 this.selectedTags = []; 434 document.querySelectorAll(".tag-checkbox").forEach((cb) => (cb.checked = false)); 435 436 requestAnimationFrame(() => { 437 const url = new URL(window.location); 438 url.searchParams.delete("tags"); 439 window.history.replaceState({}, document.title, url.pathname); 440 this.filterAndRenderBlogs(); 441 this.updateRemoveFiltersButtonVisibility(); 442 }); 443 }, 444 445 renderPaginatedBlogPosts(page, posts) { 446 const slice = posts.slice((page - 1) * this.config.postsPerPage, page * this.config.postsPerPage); 447 this.renderBlogPosts(slice); 448 }, 449 450 renderPagination(totalPages, currentPage) { 451 const pagination = document.getElementById("pagination"); 452 if (!pagination) return; 453 454 if (totalPages <= 1) { 455 pagination.innerHTML = ""; 456 return; 457 } 458 459 pagination.innerHTML = ` 460 <ul class="pagination justify-content-center"> 461 <li class="page-item ${currentPage === 1 ? "disabled" : ""}"> 462 <button class="page-link page-button" data-page="${currentPage - 1}" ${currentPage === 1 ? "disabled" : ""}>Previous</button>
463 </li> 464 ${Array.from({ length: totalPages }, (_, i) => { 465 const page = i + 1; 466 return `<li class="page-item ${currentPage === page ? "active" : ""}"> 467 <button class="page-link page-button" data-page="${page}">${page}</button> 468 </li>`; 469 }).join("")} 470 <li class="page-item ${currentPage === totalPages ? "disabled" : ""}"> 471 <button class="page-link page-button" data-page="${currentPage + 1}" ${currentPage === totalPages ? "disabled" : ""}>Next</button> 472 </li> 473 </ul> 474`; 475 476 document.querySelectorAll(".page-button").forEach((btn) => { 477 btn.addEventListener("click", (e) => { 478 const page = parseInt(e.target.dataset.page, 10); 479 this.filterAndRenderBlogs(page); 480 }); 481 }); 482 }, 483 484 setupReadFullArticleButtons() { 485 const title = document.querySelector('[data-type="page-title"]')?.textContent || this.config.pageNameAlt; 486 487 document.querySelectorAll(".read-full-article").forEach((btn) => { 488 btn.addEventListener("click", (e) => { 489 this.showFullArticle(e.target.dataset.index, title); 490 }); 491 }); 492 }, 493 494 handleAction(e) { 495 e.preventDefault(); 496 e.stopPropagation(); 497 498 const card = e.currentTarget.closest(".blog-post-card, .article-header"); 499 if (!card) return; 500 501 const postId = card.dataset.postId; 502 const isShare = e.currentTarget.classList.contains("share-button"); 503 504 if (isShare) this.createSharePostLink(postId); 505 else this.copyBlogLinkToClipboard(postId); 506 }, 507 508 handleTagChange() { 509 this.filterAndRenderBlogs(); 510 }, 511 512 handlePageClick(e) { 513 e.preventDefault(); 514 const page = parseInt(e.currentTarget.dataset.page, 10); 515 this.filterAndRenderBlogs(page); 516 }, 517 518 handleTagClick(e) { 519 e.stopPropagation(); 520 this.filterByTag(e.currentTarget.textContent.trim()); 521 }, 522 523 handleTagKeydown(e) { 524 if (e.key === "Enter" || e.key === " ") { 525 e.preventDefault(); 526 this.filterByTag(e.currentTarget.textContent.trim()); 527 } 528 }, 529 530 handleCardClick(e) { 531 if (e.target.closest(".blog-tag, .share-button, .copy-button, #removeFiltersButton, .page-button, .pagination")) return; 532 533 const card = e.currentTarget; 534 const referralMode = this.isReferralMode(); 535 const referralLink = this.normalizeExternalUrl(card.dataset.referralLink); 536 537 if (referralMode && referralLink) { 538 window.open(referralLink, "_blank", "noopener,noreferrer"); 539 return; 540 } 541 542 const postId = card.dataset.postId; 543 window.location.href = this.getInternalPostUrl(postId); 544 }, 545 546 addDynamicEventListeners() { 547 const bindOnce = (selector, event, handler) => { 548 document.querySelectorAll(`${selector}:not([data-bound])`).forEach((el) => { 549 el.addEventListener(event, handler.bind(this)); 550 el.dataset.bound = "true"; 551 }); 552 }; 553 554 bindOnce(".tag-checkbox", "change", this.handleTagChange); 555 bindOnce(".blog-post-card", "click", this.handleCardClick); 556 bindOnce(".page-button", "click", this.handlePageClick); 557 bindOnce(".blog-tag", "click", this.handleTagClick); 558 bindOnce(".blog-tag", "keydown", this.handleTagKeydown); 559 560 const removeBtn = document.getElementById("removeFiltersButton"); 561 if (removeBtn && !removeBtn.dataset.bound) { 562 removeBtn.addEventListener("click", (e) => { 563 e.stopPropagation(); 564 this.removeFilters(); 565 }); 566 removeBtn.dataset.bound = "true"; 567 } 568 569 document.querySelectorAll(".share-button, .copy-button").forEach((el) => { 570 if (el.dataset.shareCopyBound) return; 571 572 el.addEventListener("click", this.handleAction.bind(this)); 573 el.addEventListener("keydown", (e) => { 574 if (e.key === "Enter" || e.key === " ") this.handleAction(e); 575 }); 576 577 el.dataset.shareCopyBound = "true"; 578 }); 579 }, 580 581 initializeBlog() { 582 this.initFilterAndRenderBlogs(); 583 584 const pageTitle = document.querySelector('[data-type="page-title"]')?.textContent || this.config.pageNameAlt; 585 586 this.fetchBlogPosts().then(() => { 587 const totalPages = this.calculateTotalPages(this.blogPosts); 588 589 this.generateTagChecklist(); 590 this.renderPaginatedBlogPosts(1, this.blogPosts);
591 this.renderPagination(totalPages, 1); 592 this.setupReadFullArticleButtons(); 593 594 const params = new URLSearchParams(window.location.search); 595 const post = params.get("post"); 596 const tags = params.get("tags"); 597 598 if (tags) { 599 this.selectedTags = tags.split(","); 600 this.updateCheckboxesBasedOnTags(this.selectedTags); 601 } 602 603 this.filterAndRenderBlogs(1); 604 605 if (post) this.showFullArticle(post, pageTitle); 606 607 this.addDynamicEventListeners(); 608 }); 609 }, 610}; 611 612window.blogManager = blogManager; 613 614if (document.getElementById("blogPostsContainer")) { 615 blogManager.initializeBlog(); 616}
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.