mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-21 08:28:06 +00:00
Forex Dashboard v8.0
This commit is contained in:
+148
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { RefreshCw, TrendingUp, AlertTriangle, Zap } from "lucide-react";
|
||||
import { CURRENCIES, CURRENCY_META } from "@/lib/constants";
|
||||
import type { Currency, DriverData } from "@/lib/types";
|
||||
import CurrencyCard from "@/components/CurrencyCard";
|
||||
import DriversBar from "@/components/DriversBar";
|
||||
|
||||
const REFRESH_MS = parseInt(process.env.NEXT_PUBLIC_REFRESH_INTERVAL_MS ?? "3600000");
|
||||
|
||||
export default function Dashboard() {
|
||||
const [drivers, setDrivers] = useState<DriverData | null>(null);
|
||||
const [expectations, setExpectations] = useState<Record<string, unknown> | null>(null);
|
||||
const [yields, setYields] = useState<{ yields: Record<string, number | null>; spreads: Record<string, number | null> } | null>(null);
|
||||
const [lastRefresh, setLastRefresh] = useState<Date>(new Date());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeDivergences, setActiveDivergences] = useState<{ currency: Currency; score: number }[]>([]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [driversRes, expectRes, yieldsRes, fxRes] = await Promise.allSettled([
|
||||
fetch("/api/drivers").then((r) => r.json()),
|
||||
fetch("/api/expectations").then((r) => r.json()),
|
||||
fetch("/api/yields").then((r) => r.json()),
|
||||
fetch("/api/fx").then((r) => r.json()),
|
||||
]);
|
||||
|
||||
if (driversRes.status === "fulfilled") {
|
||||
const driversData = driversRes.value;
|
||||
// Merge DXY from /api/fx into drivers
|
||||
if (fxRes.status === "fulfilled" && fxRes.value?.dxy != null) {
|
||||
driversData.dxy = fxRes.value.dxy;
|
||||
}
|
||||
setDrivers(driversData);
|
||||
}
|
||||
if (expectRes.status === "fulfilled") setExpectations(expectRes.value);
|
||||
if (yieldsRes.status === "fulfilled") setYields(yieldsRes.value);
|
||||
setLastRefresh(new Date());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
const id = setInterval(refresh, REFRESH_MS);
|
||||
return () => clearInterval(id);
|
||||
}, [refresh]);
|
||||
|
||||
const handleDivergenceUpdate = useCallback((currency: Currency, score: number) => {
|
||||
setActiveDivergences((prev) => {
|
||||
const filtered = prev.filter((d) => d.currency !== currency);
|
||||
if (Math.abs(score) >= 2) return [...filtered, { currency, score }];
|
||||
return filtered;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const divergenceCount = activeDivergences.filter((d) => Math.abs(d.score) >= 2).length;
|
||||
|
||||
return (
|
||||
<div className="max-w-[1600px] mx-auto px-4 py-4">
|
||||
{/* Header */}
|
||||
<header className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
Forex Macro Dashboard
|
||||
</h1>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
USD · EUR · GBP · JPY · CHF · CAD · AUD · NZD — v8.0
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{divergenceCount > 0 && (
|
||||
<div className="flex items-center gap-1.5 bg-amber-50 border border-amber-200 rounded-full px-3 py-1.5">
|
||||
<Zap size={13} className="text-amber-600" />
|
||||
<span className="text-xs font-medium text-amber-700">
|
||||
{divergenceCount} divergence{divergenceCount > 1 ? "s" : ""} active{divergenceCount > 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-gray-400">
|
||||
{lastRefresh.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={refresh}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 text-xs text-gray-600 hover:text-gray-900 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw size={13} className={loading ? "animate-spin" : ""} />
|
||||
{loading ? "Chargement…" : "Rafraîchir"}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Global drivers bar */}
|
||||
{drivers && <DriversBar drivers={drivers} />}
|
||||
|
||||
{/* Active divergences summary */}
|
||||
{activeDivergences.length > 0 && (
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
{activeDivergences
|
||||
.sort((a, b) => Math.abs(b.score) - Math.abs(a.score))
|
||||
.map(({ currency, score }) => (
|
||||
<div
|
||||
key={currency}
|
||||
className={`flex items-center gap-1 text-xs px-2 py-1 rounded-full border font-medium ${
|
||||
score < 0
|
||||
? "bg-red-50 border-red-200 text-red-700"
|
||||
: "bg-green-50 border-green-200 text-green-700"
|
||||
}`}
|
||||
>
|
||||
<Zap size={10} />
|
||||
{CURRENCY_META[currency].flag} {currency} SD:{score > 0 ? "+" : ""}{score}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Currency cards grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{CURRENCIES.map((currency) => (
|
||||
<CurrencyCard
|
||||
key={currency}
|
||||
currency={currency}
|
||||
expectations={expectations}
|
||||
yields={yields}
|
||||
onDivergenceUpdate={handleDivergenceUpdate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-6 text-center text-xs text-gray-400 space-y-1">
|
||||
<p>
|
||||
Sources: FRED · ECB · BoE · BoC · CFTC · Frankfurter · OANDA · investinglive.com
|
||||
</p>
|
||||
<p>
|
||||
LLM: Bytez (Llama 3.1) · Données à titre informatif uniquement — pas de conseil financier
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user