"use client"; import { useState, useMemo, useEffect, useRef } from "react"; import { ExternalLink, RefreshCw, TrendingUp, TrendingDown, Minus, Loader2, Radio, AlertTriangle, Landmark, Globe, BarChart2, Zap, } 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 CCY_LIST: Currency[] = ["USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD", "NZD"]; // Catégories prioritaires avec icône et couleur 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" }, }; // Ordre d'affichage des filtres catégorie (par priorité signal) 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", ]; const SOURCE_COLORS: Record = { "InvestingLive": "bg-amber-500/20 text-amber-400", "Reuters": "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`; } // ── Composant principal ─────────────────────────────────────────────────────── interface Props { items: NewsItem[]; loading: boolean; onRefresh: () => void; } export default function NewsTab({ items, loading, onRefresh }: Props) { const [filterCcy, setFilterCcy] = useState("ALL"); const [filterCat, setFilterCat] = useState("ALL"); const [filterDir, setFilterDir] = useState<"all" | "bullish" | "bearish">("all"); const [priorityOnly, setPriorityOnly] = useState(false); const [autoRefresh, setAutoRefresh] = useState(true); const [lastRefreshAt, setLastRefreshAt] = useState(null); const intervalRef = useRef | null>(null); // Auto-refresh toutes les 5 minutes useEffect(() => { if (!autoRefresh) { if (intervalRef.current) clearInterval(intervalRef.current); return; } intervalRef.current = setInterval(() => { onRefresh(); setLastRefreshAt(new Date()); }, 5 * 60_000); return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [autoRefresh, onRefresh]); const isPriorityItem = (item: NewsItem) => item.categories.some(c => ["Discours BC", "Décision Taux", "Crise", "Guerre", "Chef d'État", "Probabilités Taux"].includes(c)); const filtered = useMemo(() => items.filter(item => { if (priorityOnly && !isPriorityItem(item)) return false; if (filterCcy !== "ALL" && !item.impacts.some(i => i.ccy === filterCcy)) return false; if (filterCat !== "ALL" && !item.categories.includes(filterCat)) return false; if (filterDir !== "all" && filterCcy !== "ALL") { if (!item.impacts.some(i => i.ccy === filterCcy && i.direction === filterDir)) return false; } return true; // eslint-disable-next-line react-hooks/exhaustive-deps }), [items, filterCcy, filterCat, filterDir, priorityOnly]); // Catégories présentes dans le feed actuel const activeCats = useMemo(() => { const set = new Set(); for (const item of items) item.categories.forEach(c => set.add(c)); return PRIORITY_CATS.filter(c => set.has(c)); }, [items]); // Comptage par devise const ccyCount = useMemo(() => { const counts: Partial> = {}; for (const item of items) { for (const imp of item.impacts) { if (!counts[imp.ccy]) counts[imp.ccy] = { bull: 0, bear: 0, total: 0 }; counts[imp.ccy]!.total++; if (imp.direction === "bullish") counts[imp.ccy]!.bull++; if (imp.direction === "bearish") counts[imp.ccy]!.bear++; } } return counts; }, [items]); // Résumé par devise (pour headline) const ccySummary = useMemo(() => { const summary: Partial> = {}; for (const [ccy, cnt] of Object.entries(ccyCount) as [Currency, { bull: number; bear: number }][]) { if (cnt.bull > cnt.bear * 1.5) summary[ccy] = "bullish"; else if (cnt.bear > cnt.bull * 1.5) summary[ccy] = "bearish"; else if (cnt.bull > 0 && cnt.bear > 0) summary[ccy] = "mixed"; else summary[ccy] = "neutral"; } return summary; }, [ccyCount]); return (
{/* ── Headline résumé par devise ──────────────────────────────────────── */} {!loading && items.length > 0 && (
Biais actualités par devise
{/* Bouton Prioritaires */} {/* Auto-refresh */}
{CCY_LIST.map(ccy => { const cnt = ccyCount[ccy]; const summary = ccySummary[ccy] ?? "neutral"; const bgCls = summary === "bullish" ? "bg-emerald-500/10 border-emerald-500/20" : summary === "bearish" ? "bg-red-500/10 border-red-500/20" : summary === "mixed" ? "bg-amber-500/10 border-amber-500/20" : "bg-slate-800/40 border-slate-700/30"; const arrow = summary === "bullish" ? "↑" : summary === "bearish" ? "↓" : summary === "mixed" ? "↕" : "→"; const arrowCls = summary === "bullish" ? "text-emerald-400" : summary === "bearish" ? "text-red-400" : summary === "mixed" ? "text-amber-400" : "text-slate-600"; return ( ); })}
)} {/* ── Filtres ─────────────────────────────────────────────────────────── */}
{/* Direction */}
Direction
{(["all", "bullish", "bearish"] as const).map(d => ( ))}
{/* Catégories */} {activeCats.length > 0 && (
Catégorie
{activeCats.map(cat => { const meta = CATEGORY_META[cat]; const isActive = filterCat === cat; return ( ); })}
)}
{/* ── Compteur résultats ──────────────────────────────────────────────── */}
{loading ? "Chargement…" : `${filtered.length} article${filtered.length > 1 ? "s" : ""}`} {filterCcy !== "ALL" && ` · ${CCY_FLAGS[filterCcy]} ${filterCcy}`} {filterCat !== "ALL" && ` · ${filterCat}`} {(filterCcy !== "ALL" || filterCat !== "ALL" || filterDir !== "all") && ( )}
{/* ── Liste ───────────────────────────────────────────────────────────── */} {loading && items.length === 0 ? (
Chargement des actualités… InvestingLive · Reuters · Bloomberg
) : filtered.length === 0 ? (
Aucune actualité pour ce filtre.
) : (
{filtered.map(item => ( ))}
)}
); } // ── NewsCard ────────────────────────────────────────────────────────────────── function NewsCard({ item, activeCcy }: { item: NewsItem; activeCcy: Currency | null }) { const [expanded, setExpanded] = useState(false); const visibleImpacts = activeCcy ? item.impacts.filter(i => i.ccy === activeCcy) : item.impacts; const overallDir = visibleImpacts.some(i => i.direction === "bullish") && visibleImpacts.some(i => i.direction === "bearish") ? "mixed" : visibleImpacts.some(i => i.direction === "bullish") ? "bullish" : visibleImpacts.some(i => i.direction === "bearish") ? "bearish" : "neutral"; // Détecter les catégories prioritaires const isPriority = item.categories.some(c => ["Discours BC", "Décision Taux", "Crise", "Guerre", "Chef d'État"].includes(c) ); const borderCls = overallDir === "bullish" ? "border-emerald-500/25" : overallDir === "bearish" ? "border-red-500/25" : overallDir === "mixed" ? "border-amber-500/20" : "border-slate-700/30"; const bgCls = overallDir === "bullish" ? "bg-emerald-500/5" : overallDir === "bearish" ? "bg-red-500/5" : "bg-slate-800/30"; // Catégorie la plus prioritaire const topCat = PRIORITY_CATS.find(p => item.categories.includes(p)); const topMeta = topCat ? CATEGORY_META[topCat] : null; return (
{/* Header */}
{item.source} {topMeta && topCat && ( {topMeta.icon} {topMeta.label} )} {isPriority && ( ⚡ Prioritaire )} {formatRelativeTime(item.publishedAt)}
{/* Titre */} {item.title} {/* Résumé */} {item.summary && (

{item.summary}

)} {/* Impact badges + bouton détail */} {visibleImpacts.length > 0 && (
{visibleImpacts.map(imp => (
{CCY_FLAGS[imp.ccy]} {imp.ccy} {imp.direction === "bullish" ? : imp.direction === "bearish" ? : }
))}
)} {/* Détail des raisons */} {expanded && visibleImpacts.length > 0 && (
{visibleImpacts.map(imp => (
{CCY_FLAGS[imp.ccy]} {imp.ccy} {imp.direction === "bullish" ? "↑" : imp.direction === "bearish" ? "↓" : "→"} {imp.reason}
))}
)}
); }