1/** 2 * Facebook Graph API Integration 3 * 4 * This script fetches posts from a Facebook page using the Graph API 5 * and displays them in the #facebook-posts-container element. 6 * 7 * IMPORTANT: The Page Access Token is stored in a separate .env file 8 * that should NEVER be committed to version control. 9 */ 10 11// Configuration locale pour l'affichage 12const fbConfig = { 13 limit: 4 14}; 15const fbSiteErrors = window.ACCFErrors; 16 17// Function to format date in a readable format 18function formatDate(dateString) { 19 const date = new Date(dateString); 20 return date.toLocaleDateString('fr-FR', { 21 year: 'numeric', 22 month: 'long', 23 day: 'numeric' 24 }); 25} 26 27// Function to create HTML for a Facebook post 28function createPostHTML(post) { 29 const hasImage = post.full_picture ? true : false; 30 const formattedDate = formatDate(post.created_time); 31 const message = post.message || ''; 32 33 // Extraire un titre à partir du début du message 34 let title = ''; 35 let contentMessage = message; 36 37 // Si le message contient des sauts de ligne, utiliser la première ligne comme titre 38 if (message.includes('\n')) { 39 const parts = message.split('\n'); 40 title = parts[0].trim(); 41 contentMessage = parts.slice(1).join('\n').trim(); 42 } else { 43 // Sinon, prendre les premiers mots (max 8) comme titre 44 const words = message.split(' '); 45 if (words.length > 8) { 46 title = words.slice(0, 8).join(' ') + '...'; 47 } else { 48 title = message; 49 contentMessage = ''; 50 } 51 } 52 53 // Limiter le message à 150 caractères avec ellipsis pour un meilleur aperçu 54 const isTruncated = contentMessage.length > 150; 55 const shortMessage = isTruncated ? contentMessage.substring(0, 150) + '...' : contentMessage; 56 57 let postHTML = ` 58 <div class="facebook-post-container"> 59 <div class="facebook-post card-style"> 60 <div class="post-author"> 61 <div class="author-avatar"> 62 <img src="images/Logo _ACCF_Cancer_du_Sein_blanc.png" alt="ACCF" width="120" height="120" loading="lazy" decoding="async" /> 63 </div> 64 <div class="author-info"> 65 <div class="author-name">ACCF</div> 66 <div class="post-date">${formattedDate}</div> 67 </div> 68 </div> 69 70 <div class="post-content"> 71 <h3 class="post-title">${title}</h3> 72 ${contentMessage ? `<div class="post-text">${shortMessage}</div>` : ''} 73 </div> 74 75 ${hasImage ? ` 76 <div class="post-image"> 77 <img src="${post.full_picture}" alt="Publication Facebook" width="600" height="400" loading="lazy" decoding="async" /> 78 </div> 79 ` : ''} 80 </div> 81 <div class="post-action"> 82 <a href="${post.permalink_url}" target="_blank" class="post-link">Lire la suite</a> 83 </div> 84 </div> 85 `; 86 87 return postHTML; 88} 89 90// Function to display a public unavailable state in the container 91function displayUnavailableState() { 92 const container = document.getElementById('facebook-posts-container'); 93 if (!container) return; 94 95 if (fbSiteErrors && typeof fbSiteErrors.renderState === 'function') { 96 fbSiteErrors.renderState(container, { 97 title: fbSiteErrors.messages.facebookTitle, 98 text: fbSiteErrors.messages.facebookText 99 }); 100 return; 101 } 102 103 container.innerHTML = ` 104 <div class="site-feedback" role="status" aria-live="polite"> 105 <h3 class="site-feedback__title">Publications temporairement indisponibles</h3> 106 <p class="site-feedback__text">Les dernières publications ne peuvent pas être affichées pour le moment.</p> 107 </div> 108 `; 109} 110 111// Main function to fetch and display Facebook posts 112async function loadFacebookPosts() { 113 const container = document.getElementById('facebook-posts-container'); 114 if (!container) return; 115 116 try { 117 // Show loading indicator 118 container.innerHTML = '<div class="text-center"><div class="spinner-border text-primary" role="status"><span class="sr-only">Chargement...</span></div></div>'; 119 // Appel du proxy serveur sécurisé (Netlify en prod, Express en dev) 120 const response = await fetch(`/api/facebook/posts?limit=${encodeURIComponent(fbConfig.limit)}
120`); 121 if (!response.ok) { 122 const errorData = await response.json().catch(() => ({})); 123 if (fbSiteErrors && typeof fbSiteErrors.log === 'function') { 124 fbSiteErrors.log('Réponse Facebook indisponible.', errorData); 125 } 126 throw new Error(`Ãchec de la récupération des publications: ${errorData.error || response.statusText}`); 127 } 128 129 const data = await response.json(); 130 if (!data || !data.data || data.data.length === 0) { 131 container.innerHTML = '<p>Aucune publication Facebook récente à afficher.</p>'; 132 return; 133 } 134 135 // Create a container with responsive grid layout and scroll buttons 136 let carouselHTML = ` 137 <div class="facebook-posts-carousel"> 138 <button class="scroll-btn prev" aria-label="Publications précédentes"><</button> 139 <div class="facebook-posts-container"> 140 ${data.data.map(post => createPostHTML(post)).join('')} 141 </div> 142 <button class="scroll-btn next" aria-label="Publications suivantes">></button> 143 </div> 144 `; 145 146 // Update the container with posts 147 container.innerHTML = carouselHTML; 148 149 // Apply responsive behavior (native scroll on mobile/tablet, arrows on desktop) 150 applyResponsiveCarouselMode(); 151 152 // Ajuster le mode d'affichage des images (contain/cover) selon leur ratio 153 applyImageFit(); 154 155 } catch (error) { 156 if (fbSiteErrors && typeof fbSiteErrors.log === 'function') { 157 fbSiteErrors.log('Chargement des publications Facebook échoué.', error); 158 } 159 displayUnavailableState(); 160 } 161} 162 163// La fonction de test a été supprimée car elle n'est plus nécessaire 164 165// Fonction pour initialiser les boutons de défilement 166function initScrollButtons() { 167 const prevBtn = document.querySelector('.scroll-btn.prev'); 168 const nextBtn = document.querySelector('.scroll-btn.next'); 169 const container = document.querySelector('.facebook-posts-container'); 170 171 if (!prevBtn || !nextBtn || !container) return; 172 173 // Nombre de cartes visibles à la fois (adapté selon la taille d'écran) 174 let visibleCards = window.innerWidth < 768 ? 2 : 3; 175 if (window.innerWidth < 576) visibleCards = 1; 176 177 // Index de la première carte visible 178 let currentIndex = 0; 179 180 // Nombre total de cartes (carte + bouton) 181 const totalCards = container.querySelectorAll('.facebook-post-container').length; 182 183 // Fonction pour mettre à jour la visibilité des cartes avec animation fluide 184 function updateVisibility(animate = true) { 185 const cards = container.querySelectorAll('.facebook-post-container'); 186 if (!cards.length) return; 187 188 // Utiliser les valeurs réelles de largeur et d'espacement 189 const styles = getComputedStyle(container); 190 const gap = parseFloat(styles.columnGap || styles.gap) || 20; 191 const cardWidth = cards[0].offsetWidth || (container.offsetWidth / visibleCards - gap); 192 const offscreenRight = visibleCards * (cardWidth + gap); 193 194 // Appliquer le défilement fluide 195 if (animate) {
196 Array.from(cards).forEach((card) => { 197 card.style.transition = 'transform 0.5s ease, opacity 0.5s ease'; 198 }); 199 } else { 200 Array.from(cards).forEach((card) => { 201 card.style.transition = 'none'; 202 }); 203 } 204 205 // Positionner les cartes 206 cards.forEach((card, index) => { 207 // Positionner toutes les cartes en absolu 208 const position = index - currentIndex; 209 210 if (position >= 0 && position < visibleCards) { 211 // Cartes visibles 212 card.style.transform = `translateX(${position * (cardWidth + gap)}px)`; 213 card.style.opacity = '1'; 214 card.style.zIndex = '1'; 215 } else if (position < 0) { 216 // Cartes à gauche (précédentes) 217 card.style.transform = `translateX(-${cardWidth + gap}px)`; 218 card.style.opacity = '0'; 219 card.style.zIndex = '0'; 220 } else { 221 // Cartes à droite (suivantes) 222 card.style.transform = `translateX(${offscreenRight}px)`; 223 card.style.opacity = '0'; 224 card.style.zIndex = '0'; 225 } 226 }); 227 228 // Mettre à jour l'état des boutons 229 prevBtn.disabled = currentIndex === 0; 230 nextBtn.disabled = currentIndex + visibleCards >= totalCards; 231 232 prevBtn.style.opacity = prevBtn.disabled ? '0.5' : '1'; 233 nextBtn.style.opacity = nextBtn.disabled ? '0.5' : '1'; 234 } 235 236 // Ãvénement pour le bouton précédent 237 prevBtn.addEventListener('click', () => { 238 if (currentIndex > 0) { 239 currentIndex--; 240 updateVisibility(true); 241 } 242 }); 243 244 // Ãvénement pour le bouton suivant 245 nextBtn.addEventListener('click', () => { 246 if (currentIndex + visibleCards < totalCards) { 247 currentIndex++; 248 updateVisibility(true); 249 } 250 }); 251 252 // Initialiser la visibilité sans animation au début 253 updateVisibility(false); 254 255 // Réajuster lors du redimensionnement de la fenêtre 256 window.addEventListener('resize', () => { 257 // Mettre à jour le nombre de cartes visibles selon la taille d'écran 258 const oldVisibleCards = visibleCards; 259 visibleCards = window.innerWidth < 768 ? 2 : 3; 260 if (window.innerWidth < 576) visibleCards = 1; 261 262 // Si le nombre de cartes visibles a changé, réinitialiser l'affichage 263 if (oldVisibleCards !== visibleCards) { 264 // Réinitialiser l'index si nécessaire 265 if (currentIndex + visibleCards > totalCards) { 266 currentIndex = Math.max(0, totalCards - visibleCards); 267 } 268 updateVisibility(false); 269 } 270 // Réappliquer le fit des images au resize 271 applyImageFit(); 272 }); 273} 274 275// Active un mode natif de défilement pour les petits écrans 276function applyResponsiveCarouselMode() { 277 const container = document.querySelector('.facebook-posts-container'); 278 const prevBtn = document.querySelector('.scroll-btn.prev'); 279 const nextBtn = document.querySelector('.scroll-btn.next'); 280 if (!container) return; 281 282 const useNative = window.innerWidth < 992; // tablette et mobile 283 if (useNative) { 284 container.classList.add('native-scroll'); 285 // Nettoyer les styles inline éventuels 286 container.querySelectorAll('.facebook-post-container').forEach((card) => { 287 card.style.transition = ''; 288 card.style.transform = ''; 289 card.style.opacity = ''; 290 card.style.zIndex = ''; 291 }); 292 if (prevBtn) prevBtn.style.display = 'none'; 293 if (nextBtn) nextBtn.style.display = 'none'; 294 } else { 295 container.classList.remove('native-scroll'); 296 if (prevBtn) prevBtn.style.display = 'flex'; 297 if (nextBtn) nextBtn.style.display = 'flex'; 298 initScrollButtons(); 299 } 300 301 // Re-évaluer au redimensionnement 302 const onResize = () => { 303 const nowUseNative = window.innerWidth < 992; 304 const isNative = container.classList.contains('native-scroll'); 305 if (nowUseNative && !isNative) { 306 container.classList.add('native-scroll'); 307 container.querySelectorAll('.facebook-post-container').forEach((card) => { 308 card.style.transition = ''; 309 card.style.transform = ''; 310 card.style.opacity = ''; 311 card.style.zIndex = ''; 312 }); 313 if (prevBtn) prevBtn.style.display = 'none'; 314 if (nextBtn) nextBtn.style.display = 'none'; 315 } else if (!nowUseNative && isNative) { 316 container.classList.remove('native-scroll'); 317 if (prevBtn) prevBtn.style.display = 'flex'; 318 if (nextBtn) nextBtn.style.display = 'flex'; 319 initScrollButtons(); 320 } 321 applyImageFit(); 322 }; 323 324 // Debounce resize handler to avoid flooding 325 let resizeTimer; 326 window.addEventListener('resize', () => { 327 clearTimeout(resizeTimer); 328 resizeTimer = setTimeout(onResize, 150); 329 }); 330} 331 332// Toujours utiliser cover pour les images comme dans l'exemple 333function applyImageFit() { 334 const images = document.querySelectorAll('.post-image img');
335 images.forEach((img) => { 336 const setFit = () => { 337 const wrapper = img.closest('.post-image'); 338 if (!wrapper) return; 339 340 // Pour le nouveau design, on utilise toujours cover 341 wrapper.classList.remove('is-contain'); 342 wrapper.classList.add('is-cover'); 343 img.style.objectFit = 'cover'; 344 img.style.objectPosition = 'center'; 345 346 // Ajuster les proportions pour les images dans le nouveau design 347 if (wrapper.closest('.card-style')) { 348 // S'assurer que l'image remplit bien son conteneur 349 img.style.width = '100%'; 350 img.style.height = '100%'; 351 } 352 }; 353 354 if (img.complete) { 355 setFit(); 356 } else { 357 img.addEventListener('load', setFit, { once: true }); 358 } 359 }); 360} 361 362// Load posts when the DOM is ready 363document.addEventListener('DOMContentLoaded', async () => { 364 // Charger les publications via le proxy sécurisé 365 await loadFacebookPosts(); 366});
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.