diff --git a/components/NewsTab.tsx b/components/NewsTab.tsx index c825992..a858e9e 100644 --- a/components/NewsTab.tsx +++ b/components/NewsTab.tsx @@ -16,22 +16,22 @@ const CCY_FLAGS: Record = { }; 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" }, + "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" }, }; const PRIORITY_CATS = [ @@ -41,6 +41,11 @@ const PRIORITY_CATS = [ "Énergie", "Commodités", "Chine", ]; +// 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", "Reuters": "bg-orange-500/20 text-orange-400", @@ -52,13 +57,25 @@ const SOURCE_COLORS: Record = { 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`; + 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`; + if (hrs < 24) return `il y a ${hrs}h`; return `il y a ${Math.floor(hrs / 24)}j`; } +// ── Tri ─────────────────────────────────────────────────────────────────────── +// Score plus élevé = apparaît en premier dans la liste. +// Ordre : récence (primaire) → haute priorité (+3h) → article catégorisé (+1h) +// Les articles sans catégorie ni impact (bruit boursier Yahoo etc.) restent en bas. + +function scoreItem(item: NewsItem): number { + const t = new Date(item.publishedAt).getTime(); + const isPrio = item.categories.some(c => HIGH_PRIO.has(c)); + const hasCat = item.categories.length > 0 || item.impacts.length > 0; + return t + (isPrio ? 3 * 3_600_000 : hasCat ? 1 * 3_600_000 : 0); +} + // ── Composant principal ─────────────────────────────────────────────────────── interface Props { @@ -68,39 +85,54 @@ interface Props { } export default function NewsTab({ items, loading, onRefresh }: Props) { - const [filterCat, setFilterCat] = useState("ALL"); - const [filterDir, setFilterDir] = useState<"all" | "bullish" | "bearish">("all"); - const [priorityOnly, setPriorityOnly] = useState(false); - const [autoRefresh, setAutoRefresh] = useState(true); + // Multi-sélection : Set vide = "Toutes" (aucun filtre actif) + const [selectedCats, setSelectedCats] = useState>(new Set()); + 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 1 minute quand actif 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 isPriorityItem = (item: NewsItem) => - item.categories.some(c => ["Discours BC", "Décision Taux", "Crise", "Guerre", "Chef d'État", "Probabilités Taux"].includes(c)); + // Bascule une catégorie dans la sélection (OR logic — plusieurs peuvent être actives) + const toggleCat = (cat: string) => { + setSelectedCats(prev => { + const next = new Set(prev); + if (next.has(cat)) next.delete(cat); + else next.add(cat); + return next; + }); + }; - const filtered = useMemo(() => items.filter(item => { - if (priorityOnly && !isPriorityItem(item)) return false; - if (filterCat !== "ALL" && !item.categories.includes(filterCat)) return false; - if (filterDir !== "all" && !item.impacts.some(i => i.direction === filterDir)) return false; - return true; - // eslint-disable-next-line react-hooks/exhaustive-deps - }), [items, filterCat, filterDir, priorityOnly]); + // Filtre + tri client + const filtered = useMemo(() => { + const result = items.filter(item => { + // Filtre "Prioritaire" + if (priorityOnly && !item.categories.some(c => HIGH_PRIO.has(c))) return false; + // Filtre catégories multi-select (OR) : l'article doit matcher AU MOINS UNE cat sélectionnée + if (selectedCats.size > 0 && !item.categories.some(c => selectedCats.has(c))) return false; + // Filtre direction + if (filterDir !== "all" && !item.impacts.some(i => i.direction === filterDir)) return false; + return true; + }); + // Tri côté client : récence → priorité → catégorie présente + return result.sort((a, b) => scoreItem(b) - scoreItem(a)); + }, [items, selectedCats, filterDir, priorityOnly]); + // Catégories présentes dans les articles actuels (pour afficher les boutons pertinents) 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]); - const anyFilter = filterCat !== "ALL" || filterDir !== "all" || priorityOnly; - const clearFilters = () => { setFilterCat("ALL"); setFilterDir("all"); setPriorityOnly(false); }; + const anyFilter = selectedCats.size > 0 || filterDir !== "all" || priorityOnly; + const clearFilters = () => { setSelectedCats(new Set()); setFilterDir("all"); setPriorityOnly(false); }; return (
@@ -131,12 +163,10 @@ export default function NewsTab({ items, loading, onRefresh }: Props) {
- {/* Label Direction */} Direction - {/* Boutons direction */} {(["all", "bullish", "bearish"] as const).map(d => ( - {/* Compteur + effacer */}
{loading ? "…" : `${filtered.length} article${filtered.length !== 1 ? "s" : ""}`} @@ -176,25 +204,26 @@ export default function NewsTab({ items, loading, onRefresh }: Props) {
- {/* Ligne 2 : catégories */} + {/* Ligne 2 : catégories multi-select */} {activeCats.length > 0 && (
Catégorie - {activeCats.map(cat => { - const meta = CATEGORY_META[cat]; - const isActive = filterCat === cat; + const meta = CATEGORY_META[cat]; + const isActive = selectedCats.has(cat); return ( -