PageSourceSearch

https://accf-comores.org/js/facebook-feed.js?v=20260524

js accf-comores.org collected 2026-09-30 21:37:50 UTC 12,877 bytes, 366 lines download raw bytes

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">&lt;</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">&gt;</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.