blog_tech/src/components/RSSFeedWidget/index.tsx
Tellsanguis df63713055 Ajout page Veille avec agrégation RSS automatique
- Création d'un plugin Docusaurus pour agréger les flux RSS au build
  * Récupère 37 flux RSS depuis le fichier OPML
  * Filtre les articles des dernières 24h
  * Génère un fichier JSON statique pour chargement instantané

- Page Veille avec composant React
  * Affichage des articles groupés par catégorie
  * Menus dépliables (repliés par défaut)
  * Chargement ultra-rapide depuis JSON pré-généré
  * Support bilingue FR/EN

- GitHub Actions pour rebuild automatique quotidien
  * Workflow déclenché tous les jours à 6h UTC
  * Met à jour les flux RSS via l'API Cloudflare Pages
  * Déclenchement manuel possible

- Configuration Webpack pour compatibilité navigateur
  * Désactivation des polyfills Node.js côté client
  * Correction du warning onBrokenMarkdownLinks

- Icône RSS dans la navbar
  * Lien vers le flux Atom du blog
  * Style cohérent avec les autres icônes

125 articles trouvés dans les dernières 24h lors du dernier build.
2025-12-06 09:33:43 +01:00

165 lines
5 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useLocation } from '@docusaurus/router';
import styles from './styles.module.css';
interface FeedItem {
title: string;
link: string;
pubDate: string;
source: string;
category: string;
}
interface CategoryGroup {
category: string;
items: FeedItem[];
}
interface RSSCacheData {
groups: CategoryGroup[];
generatedAt: string;
totalArticles: number;
}
const RSSFeedWidget: React.FC = () => {
const location = useLocation();
const isEnglish = location.pathname.startsWith('/en');
const [categoryGroups, setCategoryGroups] = useState<CategoryGroup[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [generatedAt, setGeneratedAt] = useState<string>('');
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
const t = {
loading: isEnglish ? 'Loading RSS feeds...' : 'Chargement des flux RSS...',
error: isEnglish ? 'Error loading RSS feeds' : 'Erreur lors du chargement des flux RSS',
noArticles: isEnglish ? 'No articles published in the last 24 hours in monitored RSS feeds.' : 'Aucun article publié dans les dernières 24h dans les flux RSS suivis.',
comeBack: isEnglish ? 'Come back later for new updates!' : 'Revenez plus tard pour de nouvelles actualités !',
articlesCount: (count: number) => isEnglish
? `${count} article${count > 1 ? 's' : ''} in the last 24 hours`
: `${count} article${count > 1 ? 's' : ''} publié${count > 1 ? 's' : ''} dans les dernières 24h`,
};
useEffect(() => {
const fetchFeeds = async () => {
try {
setLoading(true);
// Chargement du fichier JSON pré-généré au build
const response = await fetch('/rss-feed-cache.json');
const data: RSSCacheData = await response.json();
setCategoryGroups(data.groups);
setGeneratedAt(data.generatedAt);
setLoading(false);
} catch (err) {
console.error('Erreur lors du chargement des flux RSS:', err);
setError('Erreur lors du chargement des flux RSS');
setLoading(false);
}
};
fetchFeeds();
}, []);
const formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
return date.toLocaleTimeString(isEnglish ? 'en-US' : 'fr-FR', {
hour: '2-digit',
minute: '2-digit'
});
} catch {
return dateString;
}
};
const getTotalArticles = () => {
return categoryGroups.reduce((total, group) => total + group.items.length, 0);
};
const toggleCategory = (category: string) => {
setExpandedCategories((prev) => {
const newSet = new Set(prev);
if (newSet.has(category)) {
newSet.delete(category);
} else {
newSet.add(category);
}
return newSet;
});
};
if (loading) {
return (
<div className={styles.loading}>
<div className={styles.spinner}></div>
<p>{t.loading}</p>
</div>
);
}
if (error) {
return (
<div className={styles.error}>
<p>{t.error}</p>
</div>
);
}
if (categoryGroups.length === 0) {
return (
<div className={styles.noArticles}>
<p>{t.noArticles}</p>
<p>{t.comeBack}</p>
</div>
);
}
return (
<div className={styles.container}>
<div className={styles.summary}>
<p>{t.articlesCount(getTotalArticles())}</p>
</div>
{categoryGroups.map((group) => {
const isExpanded = expandedCategories.has(group.category);
return (
<div key={group.category} className={styles.categorySection}>
<h2
className={styles.categoryTitle}
onClick={() => toggleCategory(group.category)}
style={{ cursor: 'pointer', userSelect: 'none' }}
>
<span className={styles.categoryTitleContent}>
<span className={styles.expandIcon}>{isExpanded ? '▼' : '▶'}</span>
{group.category}
</span>
<span className={styles.categoryCount}>{group.items.length}</span>
</h2>
{isExpanded && (
<div className={styles.feedList}>
{group.items.map((item, index) => (
<article key={`${item.link}-${index}`} className={styles.feedItem}>
<div className={styles.feedHeader}>
<span className={styles.source}>{item.source}</span>
<time className={styles.date}>{formatDate(item.pubDate)}</time>
</div>
<h3 className={styles.title}>
<a href={item.link} target="_blank" rel="noopener noreferrer">
{item.title}
</a>
</h3>
</article>
))}
</div>
)}
</div>
);
})}
</div>
);
};
export default RSSFeedWidget;