PageSourceSearch

https://vanillasquad.com/assets/themes/apex/js/posts-enhancer.js?v1.2.13

js vanillasquad.com collected 2026-10-05 03:41:01 UTC 36,658 bytes, 1,126 lines download raw bytes

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.