"use client"; import { useState, useMemo, useEffect, useRef } from "react"; import { ExternalLink, RefreshCw, TrendingUp, TrendingDown, Minus, Loader2, Radio, AlertTriangle, Landmark, Globe, BarChart2, Zap, ChevronDown, ChevronUp, } from "lucide-react"; import type { NewsItem } from "@/app/api/news/route"; import type { Currency } from "@/lib/types"; // ── Constantes ──────────────────────────────────────────────────────────────── const CCY_FLAGS: Record = { USD: "🇺🇸", EUR: "🇪🇺", GBP: "🇬🇧", JPY: "🇯🇵", CHF: "🇨🇭", CAD: "🇨🇦", AUD: "🇦🇺", NZD: "🇳🇿", }; const CATEGORY_META: Record = { "Discours BC": { icon: , color: "bg-blue-500/20 text-blue-400 border-blue-500/30", label: "Discours BC" }, "Chef d'État": { icon: , color: "bg-violet-500/20 text-violet-400 border-violet-500/30", label: "Chef d'État" }, "Décision Taux": { icon: , color: "bg-amber-500/20 text-amber-400 border-amber-500/30", label: "Décision Taux" }, "Probabilités Taux": { icon: , color: "bg-sky-500/20 text-sky-400 border-sky-500/30", label: "OIS / Proba Taux" }, "Données Clés": { icon: , color: "bg-slate-400/20 text-slate-300 border-slate-500/30", label: "Données Clés" }, "Emploi": { icon: , color: "bg-slate-400/20 text-slate-300 border-slate-500/30", label: "Emploi" }, "Inflation": { icon: , color: "bg-orange-500/20 text-orange-400 border-orange-500/30", label: "Inflation" }, "Crise": { icon: , color: "bg-red-600/20 text-red-400 border-red-500/30", label: "Crise" }, "Guerre": { icon: , color: "bg-red-700/20 text-red-400 border-red-600/30", label: "Guerre" }, "Géopolitique": { icon: , color: "bg-purple-500/20 text-purple-400 border-purple-500/30", label: "Géopolitique" }, "Risk-Off": { icon: , color: "bg-red-500/20 text-red-400 border-red-500/30", label: "Risk-Off" }, "Risk-On": { icon: , color: "bg-emerald-500/20 text-emerald-400 border-emerald-500/30", label: "Risk-On" }, "Énergie": { icon: , color: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30", label: "Énergie" }, "Banque Centrale": { icon: , color: "bg-blue-500/20 text-blue-400 border-blue-500/30", label: "Banque Centrale" }, "Commodités": { icon: , color: "bg-orange-500/20 text-orange-400 border-orange-500/30", label: "Commodités" }, "Chine": { icon: , color: "bg-red-600/20 text-red-400 border-red-600/30", label: "Chine" }, }; // Catégories affichées comme boutons de filtre (ordre d'apparition) const PRIORITY_CATS = [ "Discours BC", "Décision Taux", "Probabilités Taux", "Chef d'État", "Données Clés", "Emploi", "Inflation", "Crise", "Guerre", "Géopolitique", "Risk-Off", "Risk-On", "Énergie", "Commodités", "Chine", ]; // Catégories toujours visibles dans la barre même sans articles correspondants const ALWAYS_VISIBLE = new Set(["Inflation", "Géopolitique", "Emploi", "Énergie"]); // Set pour la séparation top-news / autres const PRIORITY_CATS_SET = new Set(PRIORITY_CATS); // Catégories "haute priorité" : reçoivent un boost de tri const HIGH_PRIO = new Set([ "Discours BC", "Décision Taux", "Crise", "Guerre", "Chef d'État", "Probabilités Taux", ]); const SOURCE_COLORS: Record = { "InvestingLive": "bg-amber-500/20 text-amber-400", "CNBC": "bg-orange-500/20 text-orange-400", "Bloomberg Economics": "bg-sky-500/20 text-sky-400", "Bloomberg CB": "bg-sky-500/20 text-sky-400", "Bloomberg FX": "bg-sky-500/20 text-sky-400", }; function formatRelativeTime(isoDate: string): string { const diff = Date.now() - new Date(isoDate).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return "à l'instant"; if (mins < 60) return `il y a ${mins}min`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `il y a ${hrs}h`; return `il y a ${Math.floor(hrs / 24)}j`; } // ── Tri ─────────────────────────────────────────────────────────────────────── // Strictement chronologique (score = timestamp). Un bonus "+3h prioritaire / // +1h catégorisé" existait ici, mais en période de crise soutenue la quasi- // totalité des articles matche une catégorie prioritaire (Guerre/Géopolitique/ // Chef d'État) : le bonus s'applique alors presque partout et casse l'ordre // chronologique au lieu de l'affiner (des articles plus vieux mais // "prioritaires" passent devant des articles réellement plus récents). Le // badge visuel "⚡ Prioritaire" (NewsCard) signale déjà l'importance sans // trahir la fraîcheur réelle du flux. function scoreItem(item: NewsItem): number { return new Date(item.publishedAt).getTime(); } // ── Composant principal ─────────────────────────────────────────────────────── interface Props { items: NewsItem[]; loading: boolean; onRefresh: () => void; } export default function NewsTab({ items, loading, onRefresh }: Props) { const [selectedCats, setSelectedCats] = useState([]); const [filterDir, setFilterDir] = useState<"all" | "bullish" | "bearish">("all"); const [priorityOnly, setPriorityOnly] = useState(false); const [autoRefresh, setAutoRefresh] = useState(true); const [lastRefreshAt, setLastRefreshAt] = useState(null); const [othersOpen, setOthersOpen] = useState(false); const intervalRef = useRef | null>(null); useEffect(() => { if (!autoRefresh) { if (intervalRef.current) clearInterval(intervalRef.current); return; } intervalRef.current = setInterval(() => { onRefresh(); setLastRefreshAt(new Date()); }, 60_000); return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [autoRefresh, onRefresh]); const toggleCat = (cat: string) => { setSelectedCats(prev => prev.includes(cat) ? prev.filter(c => c !== cat) : [...prev, cat] ); }; // Filtre + tri — articles avec catégories macro et articles sans const { topFiltered, otherItems } = useMemo(() => { const hasActiveFilter = selectedCats.length > 0 || filterDir !== "all" || priorityOnly; // Articles filtrés (tous) const allFiltered = items .filter(item => { if (priorityOnly && !item.categories.some(c => HIGH_PRIO.has(c))) return false; if (selectedCats.length > 0 && !item.categories.some(c => selectedCats.includes(c))) return false; if (filterDir !== "all" && !item.impacts.some(i => i.direction === filterDir)) return false; return true; }) .sort((a, b) => scoreItem(b) - scoreItem(a)); // TOP NEWS : articles reconnus (≥1 catégorie macro) const top = allFiltered.filter(item => item.categories.some(c => PRIORITY_CATS_SET.has(c))); // AUTRES : articles sans étiquette macro — uniquement quand aucun filtre actif const others = hasActiveFilter ? [] : items .filter(item => !item.categories.some(c => PRIORITY_CATS_SET.has(c))) .sort((a, b) => scoreItem(b) - scoreItem(a)); return { topFiltered: top, otherItems: others }; }, [items, selectedCats, filterDir, priorityOnly]); // Boutons de catégories : dynamiques + catégories "toujours visibles" const activeCats = useMemo(() => { const seen = new Set(); for (const item of items) item.categories.forEach(c => seen.add(c)); ALWAYS_VISIBLE.forEach(c => seen.add(c)); return PRIORITY_CATS.filter(c => seen.has(c)); }, [items]); const anyFilter = selectedCats.length > 0 || filterDir !== "all" || priorityOnly; const clearFilters = () => { setSelectedCats([]); setFilterDir("all"); setPriorityOnly(false); }; return (
{/* ── Barre filtres ──────────────────────────────────────────────────── */}
{/* Ligne 1 : contrôles + direction + prioritaire */}
Direction {(["all", "bullish", "bearish"] as const).map(d => ( ))}
{loading ? "…" : `${topFiltered.length} article${topFiltered.length !== 1 ? "s" : ""}`} {anyFilter && ( )}
{/* Ligne 2 : catégories multi-select */} {activeCats.length > 0 && (
Catégorie {activeCats.map(cat => { const meta = CATEGORY_META[cat]; const isActive = selectedCats.includes(cat); return ( ); })}
)}
{/* ── TOP NEWS ────────────────────────────────────────────────────────── */} {loading && items.length === 0 ? (
Chargement des actualités… InvestingLive · Reuters · Bloomberg
) : topFiltered.length === 0 && otherItems.length === 0 ? (
Aucune actualité pour ce filtre.
) : ( <> {/* Articles macro / top news */} {topFiltered.length > 0 && (
{topFiltered.map(item => ( ))}
)} {/* Section "Autres actualités" — articles sans étiquette macro */} {otherItems.length > 0 && (
{othersOpen && (
{otherItems.map(item => ( ))}
)}
)} )}
); } // ── NewsCard ────────────────────────────────────────────────────────────────── function NewsCard({ item, secondary = false }: { item: NewsItem; secondary?: boolean }) { const [expanded, setExpanded] = useState(false); const overallDir = item.impacts.some(i => i.direction === "bullish") && item.impacts.some(i => i.direction === "bearish") ? "mixed" : item.impacts.some(i => i.direction === "bullish") ? "bullish" : item.impacts.some(i => i.direction === "bearish") ? "bearish" : "neutral"; const isPriority = !secondary && item.categories.some(c => ["Discours BC", "Décision Taux", "Crise", "Guerre", "Chef d'État"].includes(c) ); const borderCls = secondary ? "border-slate-800/50" : overallDir === "bullish" ? "border-emerald-500/25" : overallDir === "bearish" ? "border-red-500/25" : overallDir === "mixed" ? "border-amber-500/20" : "border-slate-700/30"; const bgCls = secondary ? "bg-slate-900/30" : overallDir === "bullish" ? "bg-emerald-500/5" : overallDir === "bearish" ? "bg-red-500/5" : "bg-slate-800/30"; const topCat = PRIORITY_CATS.find(p => item.categories.includes(p)); const topMeta = topCat ? CATEGORY_META[topCat] : null; return (
{/* Heure · Analyse · Source · Catégorie · Prioritaire */}
{formatRelativeTime(item.publishedAt)} {!secondary && ( )} {item.source} {topMeta && topCat && ( {topMeta.icon} {topMeta.label} )} {isPriority && ( ⚡ Prioritaire )}
{/* Titre seul */} {item.summary && !secondary && (

{item.summary}

)} {item.impacts.length > 0 && (
{item.impacts.map(imp => (
{CCY_FLAGS[imp.ccy]} {imp.ccy} {imp.direction === "bullish" ? : imp.direction === "bearish" ? : }
))}
)} {expanded && item.impacts.length > 0 && (
{item.impacts.map(imp => (
{CCY_FLAGS[imp.ccy]} {imp.ccy} {imp.direction === "bullish" ? "↑" : imp.direction === "bearish" ? "↓" : "→"} {imp.reason}
))}
)}
); }