1var initBlogArticlesItem = async function() { 2 3 const list = document.getElementById("articles-list"); 4 const loadMoreBtn = document.getElementById("load-more"); 5 6 if (!list) return; 7 8 let page = parseInt(list.dataset.page, 10) || 1; 9 const limit = parseInt(list.dataset.limit, 10) || 21; 10 const lang = list.dataset.lang || "fr"; 11 const blogUrl = list.dataset.blogUrl || ""; 12 const mediaUrl = "https://api.pulse-cdn.com"; 13 const readLabel = list.dataset.readLabel || ""; 14 15 let currentSearch = ""; 16 17 const arrow = loadMoreBtn?.querySelector(".load-more-arrow"); 18 const spinner = loadMoreBtn?.querySelector(".load-more-spinner"); 19 20 const setLoading = (loading) => { 21 if (!loadMoreBtn) return; 22 loadMoreBtn.disabled = loading; 23 arrow?.classList.toggle("hidden", loading); 24 spinner?.classList.toggle("hidden", !loading); 25 }; 26 27 const fetchArticles = async (p, search, replace = false) => { 28 setLoading(true); 29 30 let url = `/res/php/blog-articles.php?page=${p}&limit=${limit}&lang=${lang}`; 31 32 if (search) { 33 url += `&search=${encodeURIComponent(search)}&fields=contenu`; 34 } 35 36 try { 37 const res = await fetch(url, { 38 headers: { "X-Requested-With": "XMLHttpRequest" } 39 }); 40 41 const data = await res.json(); 42 43 if (replace) { 44 list.innerHTML = ""; 45 } 46 47 if (!Array.isArray(data) || data.length === 0) { 48 if (loadMoreBtn) loadMoreBtn.style.display = "none"; 49 setLoading(false); 50 return; 51 } 52 53 data.forEach(article => list.appendChild(buildArticleEl(article))); 54 55 page = p; 56 list.dataset.page = p; 57 58 if (loadMoreBtn) { 59 loadMoreBtn.style.display = data.length < limit ? "none" : ""; 60 } 61 62 setLoading(false); 63 64 } catch (e) { 65 setLoading(false); 66 } 67 }; 68 69 const initSearch = () => { 70 const searchEl = document.getElementById("search-articles"); 71 const clearBtn = document.getElementById("clear-search-articles"); 72 const loupe = document.getElementById("loupe"); 73 74 if (!searchEl) return; 75 76 let debounceTimer; 77 78 const toggleClearBtn = () => { 79 const hasValue = searchEl.value.trim() !== ""; 80 81 clearBtn?.classList.toggle("hidden", !hasValue); 82 loupe?.classList.toggle("hidden", hasValue); 83 }; 84 85 const runSearch = () => { 86 currentSearch = searchEl.value.trim(); 87 88 if (currentSearch) { 89 sessionStorage.setItem("blogSearch", currentSearch); 90 } else { 91 sessionStorage.removeItem("blogSearch"); 92 } 93 94 toggleClearBtn(); 95 fetchArticles(1, currentSearch, true); 96 }; 97 98 const savedSearch = sessionStorage.getItem("blogSearch"); 99 100 if (savedSearch) { 101 searchEl.value = savedSearch; 102 currentSearch = savedSearch; 103 toggleClearBtn(); 104 fetchArticles(1, currentSearch, true); 105 } 106 107 searchEl.addEventListener("input", () => { 108 clearTimeout(debounceTimer); 109 toggleClearBtn(); 110 111 debounceTimer = setTimeout(() => { 112 runSearch(); 113 }, 400); 114 }); 115 116 clearBtn?.addEventListener("click", () => { 117 clearTimeout(debounceTimer); 118 119 searchEl.value = ""; 120 currentSearch = ""; 121 sessionStorage.removeItem("blogSearch"); 122 123 toggleClearBtn(); 124 fetchArticles(1, "", true); 125 126 searchEl.focus(); 127 }); 128 129 toggleClearBtn(); 130 }; 131 132 const dropDown = () => { 133 const dropDown = document.querySelector(".dropdown"); 134 const dropDownSvg = document.querySelector(".dropdown svg"); 135 const dropdownMenu = document.querySelector(".dropdown-menu"); 136 137 if (dropDown && dropdownMenu) { 138 dropDown.addEventListener("click", () => { 139 dropdownMenu.classList.toggle("open"); 140 dropDownSvg?.classList.toggle("rotate-180"); 141 }); 142 } 143 }; 144
145 const loadMore = () => { 146 if (!loadMoreBtn) return; 147 148 loadMoreBtn.addEventListener("click", () => { 149 fetchArticles(page + 1, currentSearch); 150 }); 151 }; 152 153 function buildArticleEl(article) { 154 const slugify = s => (s || "").toString() 155 .normalize("NFD").replace(/[\u0300-\u036f]/g, "") 156 .toLowerCase() 157 .replace(/[^a-z0-9]+/g, "-") 158 .replace(/-+/g, "-") 159 .replace(/^-+|-+$/g, ""); 160 161 const slug = article.url || (slugify(article["titre-seo"] || article.nom || "item") + "-" + article.id); 162 const itemUrl = `${blogUrl}/${slug}`; 163 164 const div = document.createElement("div"); 165 div.className = "article-item"; 166 div.dataset.title = (article.nom || "").toLowerCase(); 167 div.dataset.category = (article.categories?.items?.[0]?.nom || "").toLowerCase(); 168 div.dataset.content = (article.resume || "").replace(/<[^>]+>/g, "").toLowerCase(); 169 170 const cat = article.categories?.items?.[0] || {}; 171 const image = article.image || {}; 172 const alt = (image.alt || article.nom || "").replace(/"/g, """); 173 174 const resizeUrl = (w, h, mode) => { 175 const m = mode === "auto" ? "resize" : mode === "thumbnail" ? "crop" : mode; 176 177 if (image.api_id) { 178 return `${mediaUrl}/api/v1/images/${image.api_id}/resize?w=${w}&h=${h}&mode=${m}&q=90&f=webp`; 179 } 180 181 const src = image.src || image.url || ""; 182 183 if (!src || !mediaUrl) return src; 184 if (/\.svg(\?.*)?$/i.test(src)) return src; 185 186 const idM = src.match(/\/api\/v1\/images\/([^/]+)\/(original|resize)/); 187 if (idM) { 188 return `${mediaUrl}/api/v1/images/${idM[1]}/resize?w=${w}&h=${h}&mode=${m}&q=90&f=webp`; 189 } 190 191 const upM = src.match(/\/uploads\/(.+)/); 192 if (upM) { 193 return `${mediaUrl}/api/v1/resize/uploads/${upM[1]}-${m}-${w}-${h}-90-webp`; 194 } 195 196 return src; 197 }; 198 199 const imgMob = resizeUrl(290, 251, "auto"); 200 const imgMob2 = resizeUrl(580, 502, "auto"); 201 const imgCrop = resizeUrl(290, 251, "crop"); 202 const imgCrop2 = resizeUrl(580, 502, "crop"); 203 204 div.innerHTML = ` 205 <a href="${itemUrl}"> 206 <picture class="block"> 207 <source srcset="${imgMob}, ${imgMob2} 2x" media="(max-width: 767px)"> 208 <source srcset="${imgCrop}, ${imgCrop2} 2x" media="(min-width: 768px) and (max-width: 1024px)"> 209 <img src="${imgCrop}" srcset="${imgCrop}, ${imgCrop2} 2x" alt="${alt}" class="object-cover w-[286px] mx-auto lg:w-auto h-[251px] lg:w-auto border-white border-12 shadow-lg" loading="lazy"> 210 </picture> 211 </a> 212 213 <a href="${blogUrl}/${cat.url || ""}" class="block font-title mt-3 text-sm md:text-base"> 214 ${cat.nom || ""} 215 </a> 216 217 <time datetime="${article.publishedAt || ""}" itemprop="datePublished" class="mt-1 text-sm"> 218 ${article.publishedAt ? new Date(article.publishedAt).toLocaleDateString("fr-FR") : ""} 219 </time> 220 221 <div class="mt-3"> 222 <a href="${itemUrl}"> 223 <h2 class="text-base! font-semibold">${(article.nom || "").toUpperCase()}</h2> 224 </a> 225 </div> 226 227 <div class="mt-3 [&_h2]:text-xl"> 228 ${article.resume || ""} 229 </div> 230 231 <a href="${itemUrl}" class="btn-discover mt-6 text-sm p-3 lg:text-base"> 232 ${readLabel} 233 <svg class="ml-2" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> 234 <path d="M6.25 3.75V5H14.1187L3.75 15.3687L4.63125 16.25L15 5.88125V13.75H16.25V3.75H6.25Z" fill="#8EBEBB"/> 235 </svg> 236 </a> 237 `; 238 239 return div; 240 } 241 242 initSearch(); 243 dropDown(); 244 loadMore(); 245};
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.