1// Posts Enhancer - Apex Theme 2// Enhances posts index with 3-column grid and posts show page with sidebar 3 4// Translation helper - reads from window.APEX_LANG.posts 5function _postsLang(key, fallback) { 6 var T = (window.APEX_LANG && window.APEX_LANG.posts) || {}; 7 return T[key] !== undefined ? T[key] : fallback; 8} 9 10document.addEventListener("DOMContentLoaded", function () { 11 // Determine which page we're on 12 const pathname = window.location.pathname; 13 const isPostsIndexPage = 14 pathname === "/news" || 15 pathname === "/news/" || 16 pathname.endsWith("/news") || 17 pathname.endsWith("/news/"); 18 const isPostShowPage = /\/news\/[^/]+\/?$/.test(pathname); 19 20 console.log("Posts Enhancer - Pathname:", pathname); 21 console.log("Posts Enhancer - isPostsIndexPage:", isPostsIndexPage); 22 console.log("Posts Enhancer - isPostShowPage:", isPostShowPage); 23 24 // ââ Inject Global Styles âââââââââââââââââââââââââââââââââââââââââââââââââ 25 try { 26 const style = document.createElement("style"); 27 style.id = "posts-enhancer-styles"; 28 style.textContent = ` 29 /* ============================================ 30 POSTS ENHANCER - Index Page Grid 31 ============================================ */ 32 33 /* Enhanced posts grid - 3 columns */ 34 .posts-enhanced-grid { 35 display: grid; 36 grid-template-columns: repeat(3, 1fr); 37 gap: 1.5rem; 38 } 39 40 @media (max-width: 991.98px) { 41 .posts-enhanced-grid { 42 grid-template-columns: repeat(2, 1fr); 43 } 44 } 45 46 @media (max-width: 575.98px) { 47 .posts-enhanced-grid { 48 grid-template-columns: 1fr; 49 } 50 } 51 52 /* Post card styling - inspired by news-component */ 53 .posts-enhanced-grid .post-enhanced { 54 overflow: hidden; 55 transition: transform 0.3s ease, box-shadow 0.3s ease; 56 display: flex; 57 flex-direction: column; 58 height: 100%; 59 } 60 61 .posts-enhanced-grid .post-enhanced:hover { 62 transform: translateY(-6px); 63 box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45); 64 } 65 66 /* Post image wrapper */ 67 .posts-enhanced-grid .post-image-wrapper { 68 position: relative; 69 overflow: hidden; 70 } 71 72 .posts-enhanced-grid .post-image { 73 width: 100%; 74 height: 200px; 75 object-fit: cover; 76 transition: transform 0.3s ease; 77 } 78 79 .posts-enhanced-grid .post-enhanced:hover .post-image { 80 transform: scale(1.05); 81 } 82 83 /* Date badge overlay */ 84 .posts-enhanced-grid .post-date { 85 position: absolute; 86 bottom: 16px; 87 left: 16px; 88 background: rgba(0, 0, 0, 0.26); 89 border: 0.5px solid rgba(255, 255, 255, 0.16); 90 backdrop-filter: blur(12px); 91 width: 70px; 92 height: 70px; 93 display: flex; 94 flex-direction: column; 95 justify-content: center; 96 align-items: center; 97 border-radius: 11px; 98 transition: transform 0.3s ease; 99 z-index: 1; 100 font-family: var(--font-heading, 'Lexend'), sans-serif; 101 } 102 103 .posts-enhanced-grid .post-enhanced:hover .post-date { 104 transform: scale(1.1) rotate(-8deg); 105 } 106 107 .posts-enhanced-grid .post-date-day { 108 font-size: 26px; 109 line-height: 24px; 110 font-weight: bold; 111 color: var(--color2, white); 112 } 113 114 .posts-enhanced-grid .post-date-month-year { 115 font-size: 0.7rem; 116 opacity: 0.85; 117 color: var(--color2, white); 118 } 119 120 /* Post content */ 121 .posts-enhanced-grid .post-content { 122 padding: 1.25rem; 123 display: flex; 124 flex-direction: column; 125 flex-grow: 1; 126 } 127 128 .posts-enhanced-grid .post-title { 129 font-family: var(--font-heading, 'Lexend'), sans-serif; 130 font-weight: 600; 131 font-size: 1.1rem; 132 margin-bottom: 0.75rem; 133 line-height: 1.35; 134 } 135 136 .posts-enhanced-grid .post-title a { 137 color: var(--color2, inherit); 138 text-decoration: none; 139 transition: color 0.2s ease; 140 } 141 142 .posts-enhanced-grid .post-title a:hover { 143 color: var(--color5, #7c4dfd); 144 } 145 146 .posts-enhanced-grid .post-description { 147 color: var(--color2, #6c757d); 148 opacity: 0.8; 149 font-size: 0.875rem; 150 line-height: 1.6; 151 margin-bottom: 1rem; 152 flex-grow: 1; 153 } 154 155 /* Post footer with button and stats */ 156 .posts-enhanced-grid .post-footer { 157 display: flex; 158 justify-content: space-between; 159 align-items: center; 160 margin-top: auto; 161 padding-top: 0.75rem; 162 border-top: 1px solid rgba(255, 255, 255, 0.08); 163 } 164 165 .posts-enhanced-grid .post-stats { 166 display: flex; 167 gap: 1rem; 168 font-size: 0.85rem; 169 opacity: 0.7; 170 } 171 172 .posts-enhanced-grid .post-stat-item { 173 display: flex; 174 align-items: center; 175 gap: 0.3rem; 176 } 177 178 .posts-enhanced-grid .btn-primary { 179 font-size: 0.85rem; 180 padding: 0.5rem 1rem; 181 } 182 183 /* Remove default card footer */ 184 .posts-enhanced-grid .card-footer { 185 display: none; 186 } 187 188 /* Search bar enhancement */ 189 .posts-search-enhanced { 190 margin-bottom: 1.5rem; 191 } 192 193 .posts-search-enhanced .input-group { 194 max-width: 350px; 195 } 196 197 /* ============================================ 198 POSTS ENHANCER - Show Page Layout 199 ============================================ */ 200 201 .posts-show-layout { 202 display: grid; 203 grid-template-columns: 1fr 350px; 204 gap: 2rem; 205 align-items: start; 206 } 207 208 @media (max-width: 991.98px) { 209 .posts-show-layout { 210 grid-template-columns: 1fr; 211 } 212 } 213 214 /* Main content area */ 215 .posts-show-main { 216 min-width: 0; 217 } 218 219 /* Image banner for show page */ 220 .posts-show-main .post-hero-image { 221 width: 100%; 222 border-radius: 14px; 223 margin-bottom: 1.5rem; 224 box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); 225 } 226 227 /* Sidebar image with margin */ 228 .posts-show-sidebar .post-hero-image { 229 margin-bottom: 1.25rem; 230 } 231 232 /* Content text styling */ 233 .posts-show-main .card-text, 234 .posts-show-main .card-body p, 235 .posts-show-main .card-body { 236 font-family: var(--font-body, 'Nunito'), sans-serif; 237 font-size: 0.95rem; 238 line-height: 1.7; 239 } 240 241 .posts-show-main .card-text * { 242 font-family: var(--font-body, 'Nunito'), sans-serif; 243 } 244 245 .posts-show-main .card-text h1, 246 .posts-show-main .card-text h2, 247 .posts-show-main .card-text h3, 248 .posts-show-main .card-text h4, 249 .posts-show-main .card-text h5, 250 .posts-show-main .card-text h6 { 251 font-family: var(--font-heading, 'Lexend'), sans-serif; 252 } 253 254 /* Title styling - inside card */ 255 .posts-show-main h1, 256 .posts-show-main .post-title-inside { 257 font-family: var(--font-heading, 'Lexend'), sans-serif; 258 font-weight: 700; 259 font-size: 1.75rem; 260 margin-bottom: 1.25rem; 261 padding-bottom: 1rem; 262 border-bottom: 1px solid rgba(255, 255, 255, 0.1); 263 } 264 265 /* Sidebar */ 266 .posts-show-sidebar { 267 position: sticky; 268 top: 20px; 269 } 270
271 /* Author card - Enhanced design */ 272 .posts-author-card { 273 margin-bottom: 1.5rem; 274 overflow: hidden; 275 } 276 277 .posts-author-card .card-body { 278 padding: 1.25rem; 279 } 280 281 .posts-author-wrapper { 282 display: flex; 283 flex-direction: column; 284 margin-bottom: 1rem; 285 padding-bottom: 1rem; 286 border-bottom: 1px solid rgba(255, 255, 255, 0.1); 287 } 288 289 .posts-author-details { 290 display: flex; 291 flex-direction: column; 292 gap: 0.15rem; 293 } 294 295 .posts-author-label { 296 font-size: 0.75rem; 297 text-transform: uppercase; 298 letter-spacing: 0.05em; 299 opacity: 0.6; 300 } 301 302 .posts-author-name { 303 font-family: var(--font-heading, 'Lexend'), sans-serif; 304 font-weight: 600; 305 font-size: 1.1rem; 306 color: var(--color2, white); 307 } 308 309 .posts-author-date { 310 font-size: 0.8rem; 311 opacity: 0.65; 312 } 313 314 .posts-like-section { 315 display: flex; 316 justify-content: center; 317 } 318 319 .posts-like-section .posts-like-btn { 320 width: 100%; 321 justify-content: center; 322 } 323 324 .posts-like-section .posts-like-btn.active { 325 background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%) !important; 326 border-color: #e74c3c !important; 327 } 328 329 /* Recent posts sidebar */ 330 .posts-recent-section h5 { 331 font-family: var(--font-heading, 'Lexend'), sans-serif; 332 font-weight: 600; 333 margin-bottom: 1rem; 334 } 335 336 .posts-recent-card { 337 margin-bottom: 1rem; 338 transition: transform 0.2s ease; 339 } 340 341 .posts-recent-card:hover { 342 transform: translateX(4px); 343 } 344 345 .posts-recent-card .card-body { 346 display: flex; 347 gap: 1rem; 348 padding: 0.75rem; 349 } 350 351 .posts-recent-image-wrapper { 352 width: 80px; 353 height: 60px; 354 flex-shrink: 0; 355 border-radius: 8px; 356 overflow: hidden; 357 } 358 359 .posts-recent-image { 360 width: 100%; 361 height: 100%; 362 object-fit: cover; 363 } 364 365 .posts-recent-content { 366 display: flex; 367 flex-direction: column; 368 justify-content: center; 369 min-width: 0; 370 } 371 372 .posts-recent-title { 373 font-family: var(--font-heading, 'Lexend'), sans-serif; 374 font-weight: 600; 375 font-size: 0.9rem; 376 line-height: 1.3; 377 margin-bottom: 0.25rem; 378 overflow: hidden; 379 text-overflow: ellipsis; 380 display: -webkit-box; 381 -webkit-line-clamp: 2; 382 -webkit-box-orient: vertical; 383 } 384 385 .posts-recent-title a { 386 color: var(--color2, inherit); 387 text-decoration: none; 388 transition: color 0.2s; 389 } 390 391 .posts-recent-title a:hover { 392 color: var(--color5, #7c4dfd); 393 } 394 395 .posts-recent-date { 396 font-size: 0.75rem; 397 opacity: 0.65; 398 } 399 400 /* Comments section enhancement */ 401 #comments .card { 402 transition: transform 0.2s ease; 403 } 404 405 #comments .card:hover { 406 transform: translateY(-2px); 407 } 408 409 /* Like button animation */ 410 .posts-show-main .btn[data-like-url] { 411 transition: transform 0.2s ease, box-shadow 0.2s ease; 412 } 413 414 .posts-show-main .btn[data-like-url]:hover:not(:disabled) { 415 transform: scale(1.05); 416 } 417 418 .posts-show-main .btn[data-like-url].active { 419 background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); 420 border-color: #e74c3c; 421 } 422 423 /* Loading skeleton for recent posts */ 424 .posts-recent-skeleton { 425 background: linear-gradient(90deg, 426 rgba(255,255,255,0.05) 25%, 427 rgba(255,255,255,0.1) 50%, 428 rgba(255,255,255,0.05) 75% 429 ); 430 background-size: 200% 100%; 431 animation: skeleton-shimmer 1.5s infinite; 432 border-radius: 8px; 433 } 434 435 @keyframes skeleton-shimmer { 436 0% { background-position: 200% 0; } 437 100% { background-position: -200% 0; } 438 } 439 440 /* ============================================ 441 POSTS ENHANCER - Animations 442 ============================================ */ 443 444 @keyframes postFadeIn { 445 from { 446 opacity: 0; 447 transform: translateY(20px); 448 } 449 to { 450 opacity: 1;
451 transform: translateY(0); 452 } 453 } 454 455 .posts-enhanced-grid .post-enhanced { 456 animation: postFadeIn 0.5s ease-out; 457 animation-fill-mode: both; 458 } 459 460 .posts-enhanced-grid .post-enhanced:nth-child(1) { animation-delay: 0.05s; } 461 .posts-enhanced-grid .post-enhanced:nth-child(2) { animation-delay: 0.1s; } 462 .posts-enhanced-grid .post-enhanced:nth-child(3) { animation-delay: 0.15s; } 463 .posts-enhanced-grid .post-enhanced:nth-child(4) { animation-delay: 0.2s; } 464 .posts-enhanced-grid .post-enhanced:nth-child(5) { animation-delay: 0.25s; } 465 .posts-enhanced-grid .post-enhanced:nth-child(6) { animation-delay: 0.3s; } 466 .posts-enhanced-grid .post-enhanced:nth-child(7) { animation-delay: 0.35s; } 467 .posts-enhanced-grid .post-enhanced:nth-child(8) { animation-delay: 0.4s; } 468 .posts-enhanced-grid .post-enhanced:nth-child(9) { animation-delay: 0.45s; } 469 `; 470 document.head.appendChild(style); 471 } catch (error) { 472 console.warn("Posts Enhancer - Styles injection failed:", error); 473 } 474 475 // ââ Posts Index Page Enhancement âââââââââââââââââââââââââââââââââââââââââ 476 if (isPostsIndexPage) { 477 console.log("Posts Enhancer - Enhancing index page..."); 478 try { 479 // Find the posts grid - try multiple selectors 480 let postsRow = document.querySelector(".row.gy-3"); 481 482 // Fallback: find by post-preview cards 483 if (!postsRow) { 484 const firstPostCard = document.querySelector(".post-preview"); 485 if (firstPostCard) { 486 postsRow = firstPostCard.closest(".row"); 487 } 488 } 489 490 console.log("Posts Enhancer - postsRow found:", postsRow); 491 492 if (postsRow) { 493 // Fetch post data from API to get structured dates 494 fetchPostsForIndex(postsRow); 495 } 496 497 // Enhance search bar 498 const searchForm = document.querySelector( 499 '.page-content form[role="search"]', 500 ); 501 if (searchForm) { 502 const searchCol = searchForm.closest(".col-md-3"); 503 if (searchCol) { 504 searchCol.classList.add("posts-search-enhanced"); 505 searchCol.classList.remove("col-md-3"); 506 searchCol.classList.add("col-12", "mb-4"); 507 } 508 } 509 } catch (error) { 510 console.warn("Posts Enhancer - Index enhancement failed:", error); 511 } 512 } 513 514 // ââ Posts Show Page Enhancement ââââââââââââââââââââââââââââââââââââââââââ 515 if (isPostShowPage) { 516 console.log("Posts Enhancer - Enhancing show page..."); 517 try { 518 const pageContent = document.querySelector(".page-content"); 519 if (!pageContent) { 520 console.log("Posts Enhancer - pageContent not found"); 521 return; 522 } 523 524 // In Apex theme, content is inside .page-content > .row 525 const contentRow = pageContent.querySelector(":scope > .row"); 526 const contentContainer = contentRow || pageContent; 527 528 // Find main elements - search within the entire content area 529 const h1 = contentContainer.querySelector("h1"); 530 const heroImage = contentContainer.querySelector("img.img-fluid.rounded"); 531 const mainCard = contentContainer.querySelector( 532 ".card.mb-4:has(.card-text)", 533 ); 534 const commentsSection = contentContainer.querySelector("#comments"); 535 const commentForm = contentContainer.querySelector( 536 '.card:has(form[action*="comments"])', 537 ); 538 const guestAlert = Array.from( 539 contentContainer.querySelectorAll(".alert.alert-info"), 540 ).pop(); 541 const allAlerts = contentContainer.querySelectorAll(".alert.alert-info"); 542 const unpublishedAlert = allAlerts.length > 0 ? allAlerts[0] : null; 543 const deleteModal = document.querySelector("#confirmDeleteModal"); 544 545 console.log("Posts Enhancer - Elements found:", { 546 h1: !!h1, 547 heroImage: !!heroImage, 548 mainCard: !!mainCard, 549 }); 550 551 // Create layout wrapper 552 const layout = document.createElement("div"); 553 layout.className = "posts-show-layout"; 554 555 // Create main content area 556 const mainArea = document.createElement("div"); 557 mainArea.className = "posts-show-main"; 558 559 // Create sidebar 560 const sidebar = document.createElement("div"); 561 sidebar.className = "posts-show-sidebar"; 562 563 // Move hero image to sidebar (desktop) like deluxe 564 if (heroImage) { 565 heroImage.classList.add("post-hero-image"); 566 heroImage.classList.remove("mb-3"); 567 sidebar.appendChild(heroImage.cloneNode(true)); 568 heroImage.classList.add("d-md-none"); 569 heroImage.classList.remove("post-hero-image"); 570 } 571 572 // Create author card for sidebar 573 if (mainCard) { 574 const cardBody = mainCard.querySelector(".card-body"); 575 const likeBtn = cardBody?.querySelector("button[data-like-url]"); 576 const authorInfoP = cardBody?.querySelector( 577 ".d-md-flex p, .d-md-flex .d-md-inline-block", 578 ); 579 580 // Get author from meta tag (more reliable) 581 const authorMeta = document.querySelector( 582 'meta[property="og:article:author:username"]', 583 ); 584 const publishedMeta = document.querySelector( 585 'meta[property="og:article:published_time"]', 586 ); 587 588 const authorName = 589 authorMeta?.getAttribute("content") || 590 _postsLang("author_fallback", "Author"); 591 let publishedDate = ""; 592 593 if (publishedMeta) { 594 const date = new Date(publishedMeta.getAttribute("content")); 595 publishedDate = date.toLocaleDateString( 596 document.documentElement.lang || "en", 597 { 598 day: "numeric", 599 month: "long", 600 year: "numeric", 601 }, 602 ); 603 } 604 605 const authorCard = document.createElement("div"); 606 authorCard.className = "card posts-author-card"; 607 608 const authorCardBody = document.createElement("div"); 609 authorCardBody.className = "card-body"; 610 611 // Author info layout (without icon) 612 const authorWrapper = document.createElement("div"); 613 authorWrapper.className = "posts-author-wrapper"; 614 615 // Author details 616 const authorDetails = document.createElement("div"); 617 authorDetails.className = "posts-author-details"; 618 619 const authorLabel = document.createElement("span"); 620 authorLabel.className = "posts-author-label"; 621 authorLabel.textContent = _postsLang("written_by", "Written by"); 622 authorDetails.appendChild(authorLabel); 623 624 const authorNameEl = document.createElement("span"); 625 authorNameEl.className = "posts-author-name"; 626 authorNameEl.textContent = authorName; 627 authorDetails.appendChild(authorNameEl); 628 629 if (publishedDate) { 630 const authorDate = document.createElement("span"); 631 authorDate.className = "posts-author-date"; 632 authorDate.textContent = publishedDate; 633 authorDetails.appendChild(authorDate); 634 } 635 636 authorWrapper.appendChild(authorDetails); 637 authorCardBody.appendChild(authorWrapper); 638 639 // Like button section - move original, don't clone 640 if (likeBtn) { 641 const likeSection = document.createElement("div"); 642 likeSection.className = "posts-like-section"; 643 644 // Move the original button to keep all event listeners 645 likeBtn.classList.remove("mb-3"); 646 likeBtn.classList.add("posts-like-btn"); 647 648 // Remove text label (e.g., "Likes:", "J'aime :"), keep only icons and count 649 // Iterate through child nodes and remove text nodes that contain the label
650 Array.from(likeBtn.childNodes).forEach((node) => { 651 if (node.nodeType === Node.TEXT_NODE) { 652 // Remove any text node (this is where "Likes:" or "J'aime :" lives) 653 node.remove(); 654 } 655 }); 656 657 likeSection.appendChild(likeBtn); 658 659 authorCardBody.appendChild(likeSection); 660 } 661 662 authorCard.appendChild(authorCardBody); 663 sidebar.appendChild(authorCard); 664 } 665 666 // Add recent posts section 667 const recentSection = document.createElement("div"); 668 recentSection.className = "posts-recent-section"; 669 recentSection.innerHTML = ` 670 <h5><i class="bi bi-newspaper me-2"></i>${_postsLang("recent_posts", "Recent posts")}</h5> 671 <div class="posts-recent-list"> 672 <div class="card posts-recent-card"> 673 <div class="card-body"> 674 <div class="posts-recent-skeleton" style="width: 80px; height: 60px;"></div> 675 <div class="posts-recent-content" style="flex: 1;"> 676 <div class="posts-recent-skeleton" style="height: 16px; width: 80%; margin-bottom: 8px;"></div> 677 <div class="posts-recent-skeleton" style="height: 12px; width: 50%;"></div> 678 </div> 679 </div> 680 </div> 681 <div class="card posts-recent-card"> 682 <div class="card-body"> 683 <div class="posts-recent-skeleton" style="width: 80px; height: 60px;"></div> 684 <div class="posts-recent-content" style="flex: 1;"> 685 <div class="posts-recent-skeleton" style="height: 16px; width: 70%; margin-bottom: 8px;"></div> 686 <div class="posts-recent-skeleton" style="height: 12px; width: 40%;"></div> 687 </div> 688 </div> 689 </div> 690 <div class="card posts-recent-card"> 691 <div class="card-body"> 692 <div class="posts-recent-skeleton" style="width: 80px; height: 60px;"></div> 693 <div class="posts-recent-content" style="flex: 1;"> 694 <div class="posts-recent-skeleton" style="height: 16px; width: 90%; margin-bottom: 8px;"></div> 695 <div class="posts-recent-skeleton" style="height: 12px; width: 45%;"></div> 696 </div> 697 </div> 698 </div> 699 </div> 700 `; 701 sidebar.appendChild(recentSection); 702 703 // Fetch recent posts from API 704 fetchRecentPosts(recentSection.querySelector(".posts-recent-list")); 705 706 // Move main content to main area 707 if ( 708 unpublishedAlert && 709 unpublishedAlert.textContent.includes("unpublished") 710 ) { 711 mainArea.appendChild(unpublishedAlert); 712 } 713 if (heroImage) mainArea.appendChild(heroImage); // Mobile version 714 if (mainCard) { 715 // Remove the author/like row from main card since it's in sidebar 716 const cardBody = mainCard.querySelector(".card-body"); 717 const flexRow = cardBody?.querySelector(".d-md-flex"); 718 if (flexRow) flexRow.remove(); 719 720 // Move title inside the card body (at the beginning) 721 if (h1 && cardBody) { 722 h1.classList.add("post-title-inside"); 723 cardBody.insertBefore(h1, cardBody.firstChild); 724 } 725 726 mainArea.appendChild(mainCard); 727 } 728 if (commentForm) mainArea.appendChild(commentForm); 729 if (commentsSection) mainArea.appendChild(commentsSection); 730 if (guestAlert && guestAlert !== unpublishedAlert) 731 mainArea.appendChild(guestAlert); 732 733 // Assemble layout 734 layout.appendChild(mainArea); 735 layout.appendChild(sidebar); 736 737 // Find a good insertion point in the content container 738 // We need to insert as a col-12 wrapper in the Apex theme 739 const layoutWrapper = document.createElement("div"); 740 layoutWrapper.className = "col-12"; 741 layoutWrapper.appendChild(layout); 742 743 // Insert layout into the content row (after the profile/sidebar columns) 744 if (contentRow) { 745 // Find the last apex-editable-zone (the profile widget) 746 const lastEditableZone = Array.from( 747 contentRow.querySelectorAll(".apex-editable-zone"), 748 ).pop(); 749 if (lastEditableZone && lastEditableZone.parentElement) { 750 lastEditableZone.parentElement.after(layoutWrapper); 751 } else { 752 contentRow.appendChild(layoutWrapper); 753 } 754 } else { 755 pageContent.appendChild(layoutWrapper); 756 } 757 758 // Move modal outside of layout 759 if (deleteModal) { 760 document.body.appendChild(deleteModal); 761 } 762 763 console.log("Posts Enhancer - Show page layout created successfully"); 764 } catch (error) { 765 console.warn("Posts Enhancer - Show page enhancement failed:", error); 766 } 767 } 768 769 /** 770 * Fetch posts from API and transform index page grid 771 */ 772 async function fetchPostsForIndex(postsRow) { 773 try { 774 const response = await fetch("/api/posts"); 775 if (!response.ok) { 776 throw new Error("API request failed: " + response.status); 777 } 778 779 const data = await response.json(); 780 const postsData = data.data || data; 781 782 // Create a map of post slug to post data for quick lookup 783 const postsMap = {}; 784 postsData.forEach((post) => { 785 postsMap[post.slug] = post; 786 }); 787 788 // Transform row to enhanced grid 789 postsRow.classList.remove("row", "gy-3"); 790 postsRow.classList.add("posts-enhanced-grid"); 791 792 // Transform each post card 793 const postCols = postsRow.querySelectorAll(":scope > .col-md-6"); 794
795 postCols.forEach(function (col) { 796 // Remove col wrapper classes 797 col.className = ""; 798 799 const card = col.querySelector(".card.post-preview"); 800 if (!card) return; 801 802 card.classList.remove("post-preview"); 803 card.classList.add("post-enhanced"); 804 805 // Get post data 806 const cardImg = card.querySelector(".card-img-top"); 807 const cardBody = card.querySelector(".card-body"); 808 const cardFooter = card.querySelector(".card-footer"); 809 const title = cardBody?.querySelector(".card-title"); 810 const titleLink = title?.querySelector("a"); 811 const description = cardBody?.querySelector(".card-text"); 812 const readBtn = cardBody?.querySelector(".btn"); 813 const footerText = cardFooter?.textContent.trim() || ""; 814 815 // Extract slug from the link to find matching post data 816 let postData = null; 817 if (titleLink) { 818 const href = titleLink.getAttribute("href"); 819 const slugMatch = href.match(/\/news\/([^\/]+)/); 820 if (slugMatch) { 821 postData = postsMap[slugMatch[1]]; 822 } 823 } 824 825 // Extract date from API data 826 let day = "--"; 827 let monthYear = "--/--"; 828 829 if (postData && postData.published_at) { 830 const date = new Date(postData.published_at); 831 day = date.getDate().toString().padStart(2, "0");
832 const month = (date.getMonth() + 1).toString().padStart(2, "0"); 833 const year = date.getFullYear().toString().slice(-2); 834 monthYear = `${month}/${year}`; 835 } 836 837 // Rebuild card structure 838 card.innerHTML = ""; 839 840 // Image wrapper with date badge 841 const imageWrapper = document.createElement("div"); 842 imageWrapper.className = "post-image-wrapper"; 843 844 // Date badge 845 const dateBadge = document.createElement("div"); 846 dateBadge.className = "post-date"; 847 dateBadge.innerHTML = ` 848 <span class="post-date-day">${day}</span> 849 <span class="post-date-month-year">${monthYear}</span> 850 `; 851 imageWrapper.appendChild(dateBadge); 852 853 // Image 854 if (cardImg) { 855 cardImg.className = "post-image"; 856 imageWrapper.appendChild(cardImg); 857 } else { 858 // Placeholder image 859 const placeholder = document.createElement("div"); 860 placeholder.className = "post-image"; 861 placeholder.style.cssText = 862 "background: linear-gradient(135deg, var(--color5) 0%, var(--color6) 100%);"; 863 imageWrapper.appendChild(placeholder); 864 } 865 866 card.appendChild(imageWrapper); 867 868 // Content area 869 const content = document.createElement("div"); 870 content.className = "post-content"; 871 872 // Title 873 if (title) { 874 title.className = "post-title"; 875 content.appendChild(title); 876 } 877 878 // Description 879 if (description) { 880 description.className = "post-description"; 881 content.appendChild(description); 882 } 883 884 // Footer with button and author 885 const footer = document.createElement("div"); 886 footer.className = "post-footer"; 887 888 if (readBtn) { 889 readBtn.innerHTML = readBtn.textContent.trim(); 890 footer.appendChild(readBtn); 891 } 892 893 // Author info - use API data if available 894 let authorName = null; 895 if (postData && postData.author && postData.author.name) { 896 authorName = postData.author.name; 897 } else { 898 // Fallback: extract from footer text 899 const authorMatch = footerText.match(/(?:par|by)\s+(.+)$/i); 900 if (authorMatch) { 901 authorName = authorMatch[1]; 902 } 903 } 904 905 if (authorName) { 906 const stats = document.createElement("div"); 907 stats.className = "post-stats"; 908 stats.innerHTML = ` 909 <span class="post-stat-item"> 910 <i class="bi bi-person"></i> ${authorName} 911 </span> 912 `; 913 footer.appendChild(stats); 914 } 915 916 content.appendChild(footer); 917 card.appendChild(content); 918 919 // Move card out of col wrapper 920 postsRow.appendChild(card); 921 }); 922 923 // Remove empty col wrappers 924 postCols.forEach(function (col) { 925 if (!col.children.length) { 926 col.remove(); 927 } 928 }); 929 } catch (error) { 930 console.warn("Posts Enhancer - Failed to fetch posts from API:", error); 931 // Fallback: still transform layout but without dates 932 transformPostsIndexFallback(postsRow); 933 } 934 } 935 936 /** 937 * Fallback function to transform index page without API data 938 */ 939 function transformPostsIndexFallback(postsRow) { 940 // Transform row to enhanced grid 941 postsRow.classList.remove("row", "gy-3"); 942 postsRow.classList.add("posts-enhanced-grid"); 943 944 // Transform each post card 945 const postCols = postsRow.querySelectorAll(":scope > .col-md-6"); 946 947 postCols.forEach(function (col) { 948 col.className = ""; 949 950 const card = col.querySelector(".card.post-preview"); 951 if (!card) return; 952 953 card.classList.remove("post-preview"); 954 card.classList.add("post-enhanced"); 955 956 const cardImg = card.querySelector(".card-img-top"); 957 const cardBody = card.querySelector(".card-body"); 958 const cardFooter = card.querySelector(".card-footer"); 959 const title = cardBody?.querySelector(".card-title"); 960 const description = cardBody?.querySelector(".card-text"); 961 const readBtn = cardBody?.querySelector(".btn"); 962 const footerText = cardFooter?.textContent.trim() || ""; 963 964 card.innerHTML = ""; 965 966 const imageWrapper = document.createElement("div"); 967 imageWrapper.className = "post-image-wrapper"; 968 969 const dateBadge = document.createElement("div"); 970 dateBadge.className = "post-date"; 971 dateBadge.innerHTML = `
972 <span class="post-date-day">--</span> 973 <span class="post-date-month-year">--/--</span> 974 `; 975 imageWrapper.appendChild(dateBadge); 976 977 if (cardImg) { 978 cardImg.className = "post-image"; 979 imageWrapper.appendChild(cardImg); 980 } else { 981 const placeholder = document.createElement("div"); 982 placeholder.className = "post-image"; 983 placeholder.style.cssText = 984 "background: linear-gradient(135deg, var(--color5) 0%, var(--color6) 100%);"; 985 imageWrapper.appendChild(placeholder); 986 } 987 988 card.appendChild(imageWrapper); 989 990 const content = document.createElement("div"); 991 content.className = "post-content"; 992 993 if (title) { 994 title.className = "post-title"; 995 content.appendChild(title); 996 } 997 998 if (description) { 999 description.className = "post-description"; 1000 content.appendChild(description); 1001 } 1002 1003 const footer = document.createElement("div"); 1004 footer.className = "post-footer"; 1005 1006 if (readBtn) { 1007 readBtn.innerHTML = readBtn.textContent.trim(); 1008 footer.appendChild(readBtn); 1009 } 1010 1011 const authorMatch = footerText.match(/(?:par|by)\s+(.+)$/i); 1012 if (authorMatch) { 1013 const stats = document.createElement("div"); 1014 stats.className = "post-stats"; 1015 stats.innerHTML = ` 1016 <span class="post-stat-item"> 1017 <i class="bi bi-person"></i> ${authorMatch[1]} 1018 </span> 1019 `; 1020 footer.appendChild(stats); 1021 } 1022 1023 content.appendChild(footer); 1024 card.appendChild(content); 1025 1026 postsRow.appendChild(card); 1027 }); 1028 1029 postCols.forEach(function (col) { 1030 if (!col.children.length) { 1031 col.remove(); 1032 } 1033 }); 1034 } 1035 1036 /** 1037 * Fetch recent posts from API and render them 1038 */ 1039 async function fetchRecentPosts(container) { 1040 try { 1041 const response = await fetch("/api/posts"); 1042 if (!response.ok) 1043 throw new Error("API request failed: " + response.status); 1044 1045 const data = await response.json(); 1046 1047 // Laravel Resource Collection wraps data in { data: [...] } 1048 const posts = data.data || data; 1049 1050 console.log("Posts Enhancer - Fetched posts:", posts); 1051 1052 if (!posts || !Array.isArray(posts) || posts.length === 0) { 1053 container.innerHTML = 1054 '<p class="text-muted">' + 1055 _postsLang("no_recent", "No recent posts.") + 1056 "</p>"; 1057 return; 1058 } 1059 1060 // Get current post slug to filter it out 1061 const currentSlug = window.location.pathname 1062 .split("/news/")[1] 1063 ?.replace(/\/$/, ""); 1064 1065 // Filter out current post and take 3 1066 const recentPosts = posts 1067 .filter((post) => post.slug !== currentSlug) 1068 .slice(0, 3); 1069 1070 if (recentPosts.length === 0) { 1071 container.innerHTML = 1072 '<p class="text-muted">' + 1073 _postsLang("no_other", "No other posts.") + 1074 "</p>"; 1075 return; 1076 } 1077 1078 container.innerHTML = recentPosts 1079 .map((post) => { 1080 const date = new Date(post.published_at); 1081 const formattedDate = date.toLocaleDateString( 1082 document.documentElement.lang || "en", 1083 { 1084 day: "numeric", 1085 month: "short", 1086 }, 1087 ); 1088 1089 // Use the URL from API if available, otherwise build it 1090 const postUrl = post.url || `/news/${post.slug}`; 1091 1092 return ` 1093 <div class="card posts-recent-card"> 1094 <div class="card-body"> 1095 ${ 1096 post.image 1097 ? ` 1098 <div class="posts-recent-image-wrapper"> 1099 <img src="${post.image}" class="posts-recent-image" alt="${post.title}"> 1100 </div> 1101 ` 1102 : "" 1103 } 1104 <div class="posts-recent-content"> 1105 <h6 class="posts-recent-title"> 1106 <a href="${postUrl}">${post.title}</a> 1107 </h6>
1108 <span class="posts-recent-date">${formattedDate}</span> 1109 </div> 1110 </div> 1111 </div> 1112 `; 1113 }) 1114 .join(""); 1115 } catch (error) { 1116 console.warn("Posts Enhancer - Failed to fetch recent posts:", error); 1117 container.innerHTML = 1118 '<p class="text-muted opacity-50">' + 1119 _postsLang("load_failed", "Unable to load recent posts.") + 1120 "</p>"; 1121 } 1122 } 1123 1124 console.log("Posts Enhancer loaded successfully"); 1125}); 1126
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.