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.
This commit is contained in:
parent
aaf03916d4
commit
df63713055
16 changed files with 1148 additions and 5 deletions
165
src/components/RSSFeedWidget/index.tsx
Normal file
165
src/components/RSSFeedWidget/index.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
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;
|
||||
212
src/components/RSSFeedWidget/styles.module.css
Normal file
212
src/components/RSSFeedWidget/styles.module.css
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
/* Conteneur principal */
|
||||
.container {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Résumé du jour */
|
||||
.summary {
|
||||
padding: 1rem 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
background: linear-gradient(135deg, var(--ifm-color-primary-lightest) 0%, var(--ifm-color-primary-lighter) 100%);
|
||||
border-left: 4px solid var(--ifm-color-primary);
|
||||
border-radius: 8px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ifm-color-primary-darker);
|
||||
}
|
||||
|
||||
.summary p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Section par catégorie */
|
||||
.categorySection {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.categoryTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--ifm-background-surface-color);
|
||||
border: 2px solid var(--ifm-color-primary-lighter);
|
||||
border-radius: 8px;
|
||||
font-size: 1.5rem;
|
||||
color: var(--ifm-color-content);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.categoryTitle:hover {
|
||||
background-color: var(--ifm-color-primary-lightest);
|
||||
border-color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
.categoryTitleContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
color: var(--ifm-color-primary);
|
||||
font-size: 0.9rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.categoryCount {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0 0.75rem;
|
||||
background-color: var(--ifm-color-primary);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Liste des flux */
|
||||
.feedList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Article individuel */
|
||||
.feedItem {
|
||||
padding: 1.5rem;
|
||||
background-color: var(--ifm-background-surface-color);
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.feedItem:hover {
|
||||
border-color: var(--ifm-color-primary);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* En-tête de l'article */
|
||||
.feedHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.source {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background-color: var(--ifm-color-emphasis-100);
|
||||
color: var(--ifm-color-content-secondary);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Titre de l'article */
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.title a {
|
||||
color: var(--ifm-color-content);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.title a:hover {
|
||||
color: var(--ifm-color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* État de chargement */
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid var(--ifm-color-emphasis-300);
|
||||
border-top-color: var(--ifm-color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* État d'erreur */
|
||||
.error {
|
||||
padding: 2rem;
|
||||
background-color: var(--ifm-color-danger-contrast-background);
|
||||
border: 1px solid var(--ifm-color-danger);
|
||||
border-radius: 8px;
|
||||
color: var(--ifm-color-danger-darker);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Aucun article */
|
||||
.noArticles {
|
||||
padding: 3rem 2rem;
|
||||
background-color: var(--ifm-background-surface-color);
|
||||
border: 2px dashed var(--ifm-color-emphasis-300);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
}
|
||||
|
||||
.noArticles p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.categoryTitle {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.feedItem {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.summary {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue