mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-25 18:38:07 +00:00
feat: banques centrales (vote+dot plot+previsions), calendrier elargi, fix InvestingLive, M3 auto
Central bank governance (nouvel onglet Banques centrales) : - lib/centralBankGovernance.ts + app/api/central-bank-sources : scraping live du vote de la derniere reunion, dot plot Fed (SEP), et desormais les previsions macro (PIB + inflation) publiees par chaque BC elle-meme (Fed SEP, Eurosystem staff projections, BoJ Outlook Report PDF, SNB conditional forecast, BoC MPR, RBA SMP). GBP/NZD laisses honnetement vides quand aucune source chiffree fiable n'est accessible (RBNZ bloque par Cloudflare). - components/CentralBankSourcesTab.tsx : nouvel onglet avec cards par banque. Taux directeurs + Money Supply M3 : - data/rate_decisions.json corrige (JPY, EUR, NZD etc. etaient perimes d'1-2 decisions) et desormais auto-maintenu : .github/workflows/update-rate-decisions.yml (horaire) detecte les changements de taux via Trading Economics et fait glisser current -> prev sans perte de donnee. - data/money-supply-m3.json (nouveau) + .github/workflows/fetch-money-supply.yml (hebdo) : M3 par devise (proxy M2 pour l'USD, la Fed ne publiant plus M3 depuis 2006), affiche dans CurrencyCard. Calendrier economique elargi : - lib/calendar-countries.ts, lib/calendar-taxonomy.ts, lib/fxstreetCalendar.ts : couverture pays elargie + classification des evenements + source FXStreet. Fix InvestingLive : - lib/investinglive.ts : l'ancienne API WordPress (wp-json) renvoyait 404 depuis leur migration Nuxt.js -> reecrit vers api.investinglive.com/api/homepage/articles, + fix crash silencieux (Tldr pas toujours un tableau). Divers : - .vercel/ ajoute au .gitignore (ne doit jamais etre commite, cf. son propre README). - scripts/ (lancement PWA, push env Vercel), captures d'ecran, cache InvestingLive. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
82a732fbdf
commit
0657b50c07
+61
-12
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useMemo } from "react";
|
||||
import React, { useState, useMemo, useRef } from "react";
|
||||
import { ChevronDown, ChevronRight, Loader2, Calendar } from "lucide-react";
|
||||
import { CURRENCIES, CURRENCY_META } from "@/lib/constants";
|
||||
import type { Currency } from "@/lib/types";
|
||||
import { CURRENCY_META } from "@/lib/constants";
|
||||
import type { CalendarEvent } from "@/app/api/calendar/route";
|
||||
|
||||
interface Props {
|
||||
@@ -21,9 +20,19 @@ const CATEGORY_LABELS: Record<string, string> = {
|
||||
gdp: "PIB",
|
||||
retail_sales: "Ventes détail",
|
||||
trade_balance: "Balance comm.",
|
||||
sentiment: "Confiance",
|
||||
housing: "Immobilier",
|
||||
money_supply: "Masse monétaire",
|
||||
trade_detail: "Commerce (détail)",
|
||||
regional_fed: "Fed régionale",
|
||||
portfolio_flows:"Flux portefeuille",
|
||||
public_finance: "Finances publiques",
|
||||
holiday: "Jour férié",
|
||||
other: "Autre",
|
||||
};
|
||||
|
||||
// ── Currency → ISO alpha-2 (pour flagcdn.com) ─────────────────────────────────
|
||||
// Univers élargi (45 pays côté calendrier, au-delà des 8 devises majeures tradées).
|
||||
|
||||
const CCY_ISO: Record<string, string> = {
|
||||
USD: "us", EUR: "eu", GBP: "gb", JPY: "jp",
|
||||
@@ -31,8 +40,15 @@ const CCY_ISO: Record<string, string> = {
|
||||
CNY: "cn", SEK: "se", NOK: "no", DKK: "dk",
|
||||
SGD: "sg", HKD: "hk", MXN: "mx", BRL: "br",
|
||||
ZAR: "za", INR: "in", KRW: "kr", TRY: "tr",
|
||||
ARS: "ar", CLP: "cl", COP: "co", CZK: "cz",
|
||||
HUF: "hu", ISK: "is", IDR: "id", ILS: "il",
|
||||
KWD: "kw", PLN: "pl", RON: "ro", RUB: "ru", VND: "vn",
|
||||
};
|
||||
|
||||
// Ordre d'affichage préféré des chips devise : majeures d'abord, puis le reste
|
||||
// trié alphabétiquement (calculé dynamiquement depuis les events reçus).
|
||||
const MAJOR_ORDER = ["USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD", "NZD"];
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function isoToLocalDate(iso: string): string {
|
||||
@@ -100,7 +116,6 @@ function EventRow({ ev, isChild, expanded, onToggle }: {
|
||||
ev: CalendarEvent; isChild: boolean; expanded: boolean; onToggle: () => void;
|
||||
}) {
|
||||
const { day, time } = fmtDate(ev.date);
|
||||
const meta = CURRENCY_META[ev.currency];
|
||||
|
||||
const borderCls = !ev.isPublished && ev.impact === "high" ? "border-l-2 border-l-red-500"
|
||||
: !ev.isPublished && ev.impact === "medium" ? "border-l-2 border-l-amber-400"
|
||||
@@ -177,11 +192,12 @@ function EventRow({ ev, isChild, expanded, onToggle }: {
|
||||
type WeekTab = "prev" | "current" | "next" | "next2" | "all";
|
||||
|
||||
export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
||||
const [filterCcy, setFilterCcy] = useState<Currency | "ALL">("ALL");
|
||||
const [filterCcy, setFilterCcy] = useState<string>("ALL");
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
||||
const [showLow, setShowLow] = useState(false);
|
||||
const [weekTab, setWeekTab] = useState<WeekTab>("all");
|
||||
const [fromDate, setFromDate] = useState<string>(todayIso());
|
||||
const fromDateRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { prevWeekLabel, currentWeekLabel, nextWeekLabel, next2StartLabel, nextMondayIso } = useMemo(getWeekBounds, []);
|
||||
void nextMondayIso;
|
||||
@@ -206,6 +222,19 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
||||
return true;
|
||||
}), [events, filterCcy, showLow, expanded, weekTab, fromDate]);
|
||||
|
||||
// Chips devise : toutes les devises présentes dans les events reçus,
|
||||
// majeures d'abord (ordre trading), puis le reste par ordre alphabétique.
|
||||
const availableCurrencies = useMemo(() => {
|
||||
const set = new Set(events.map(ev => ev.currency));
|
||||
return Array.from(set).sort((a, b) => {
|
||||
const ia = MAJOR_ORDER.indexOf(a), ib = MAJOR_ORDER.indexOf(b);
|
||||
if (ia !== -1 && ib !== -1) return ia - ib;
|
||||
if (ia !== -1) return -1;
|
||||
if (ib !== -1) return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}, [events]);
|
||||
|
||||
const days: string[] = [];
|
||||
const dayMap: Record<string, CalendarEvent[]> = {};
|
||||
for (const ev of filtered) {
|
||||
@@ -228,7 +257,7 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-slate-200">Calendrier économique</h2>
|
||||
<p className="text-[10px] text-slate-600 mt-0.5">Sources : ForexFactory · FRED · Banques centrales</p>
|
||||
<p className="text-[10px] text-slate-600 mt-0.5">Sources : Trading Economics · investingLive · Banques centrales</p>
|
||||
</div>
|
||||
<label className="flex items-center gap-1.5 text-[10px] text-slate-500 cursor-pointer">
|
||||
<input
|
||||
@@ -290,14 +319,27 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<Calendar size={11} className="text-slate-600" />
|
||||
<span className="text-[10px] text-slate-500">Depuis</span>
|
||||
{/* defaultValue (pas value) : un input date="" contrôlé re-render à chaque
|
||||
frappe et casse la saisie clavier native (les segments jour/mois/année
|
||||
se mélangent, y compris si le re-render est déclenché indirectement via
|
||||
une key). Non-contrôlé après le mount ; le bouton "Aujourd'hui" resynchronise
|
||||
l'affichage à la main via la ref, sans jamais re-render l'input lui-même. */}
|
||||
<input
|
||||
ref={fromDateRef}
|
||||
type="date"
|
||||
value={fromDate}
|
||||
onChange={e => setFromDate(e.target.value)}
|
||||
defaultValue={fromDate}
|
||||
onChange={e => {
|
||||
const v = e.target.value;
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) setFromDate(v);
|
||||
}}
|
||||
className="text-[10px] bg-slate-800 border border-slate-700 rounded px-1.5 py-0.5 text-slate-300 focus:outline-none focus:border-amber-500/50"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setFromDate(todayIso())}
|
||||
onClick={() => {
|
||||
const today = todayIso();
|
||||
setFromDate(today);
|
||||
if (fromDateRef.current) fromDateRef.current.value = today;
|
||||
}}
|
||||
className="text-[9px] text-amber-500 hover:text-amber-400 underline"
|
||||
>
|
||||
Aujourd'hui
|
||||
@@ -317,7 +359,7 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
||||
>
|
||||
Tout
|
||||
</button>
|
||||
{CURRENCIES.map(ccy => (
|
||||
{availableCurrencies.map(ccy => (
|
||||
<button
|
||||
key={ccy}
|
||||
onClick={() => setFilterCcy(ccy === filterCcy ? "ALL" : ccy)}
|
||||
@@ -327,7 +369,7 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
||||
: "bg-slate-800 text-slate-400 hover:bg-slate-700 hover:text-slate-200"
|
||||
}`}
|
||||
>
|
||||
{CURRENCY_META[ccy].flag} {ccy}
|
||||
{CURRENCY_META[ccy as keyof typeof CURRENCY_META]?.flag ?? ""} {ccy}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -348,7 +390,14 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
||||
</p>
|
||||
)}
|
||||
{fromDate > todayIso() && (
|
||||
<button onClick={() => setFromDate(todayIso())} className="mt-2 text-[10px] text-amber-500 underline">
|
||||
<button
|
||||
onClick={() => {
|
||||
const today = todayIso();
|
||||
setFromDate(today);
|
||||
if (fromDateRef.current) fromDateRef.current.value = today;
|
||||
}}
|
||||
className="mt-2 text-[10px] text-amber-500 underline"
|
||||
>
|
||||
Revenir à aujourd'hui
|
||||
</button>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user