mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-07-27 20:37:45 +00:00
feat: UI — timestamp/Analyse avant le titre (blanc), drapeaux ronds calendrier, carry dans header, 2 tableaux côte à côte
- NewsTab: timestamp (blanc) + bouton Analyse déplacés avant le titre - CalendarTab: drapeaux ronds via flagcdn.com (w5 h5 rounded-full) - YieldsTab: top carry affiché dans le header (★ LONG→SHORT +Xbp), tableau Top Carry supprimé, Classement + Movers côte à côte Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,16 @@ const CATEGORY_LABELS: Record<string, string> = {
|
||||
trade_balance: "Balance comm.",
|
||||
};
|
||||
|
||||
// ── Currency → ISO alpha-2 (pour flagcdn.com) ─────────────────────────────────
|
||||
|
||||
const CCY_ISO: Record<string, string> = {
|
||||
USD: "us", EUR: "eu", GBP: "gb", JPY: "jp",
|
||||
CAD: "ca", AUD: "au", NZD: "nz", CHF: "ch",
|
||||
CNY: "cn", SEK: "se", NOK: "no", DKK: "dk",
|
||||
SGD: "sg", HKD: "hk", MXN: "mx", BRL: "br",
|
||||
ZAR: "za", INR: "in", KRW: "kr", TRY: "tr",
|
||||
};
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function isoToLocalDate(iso: string): string {
|
||||
@@ -105,8 +115,18 @@ function EventRow({ ev, isChild, expanded, onToggle }: {
|
||||
</td>
|
||||
|
||||
<td className="py-2 px-2 whitespace-nowrap">
|
||||
<span className="text-sm">{meta?.flag}</span>
|
||||
<span className="ml-1 text-xs font-semibold text-slate-300">{ev.currency}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-5 h-5 rounded-full overflow-hidden shrink-0 bg-slate-700">
|
||||
<img
|
||||
src={`https://flagcdn.com/w40/${CCY_ISO[ev.currency] ?? ev.currency.slice(0,2).toLowerCase()}.png`}
|
||||
width={20} height={20}
|
||||
alt={ev.currency}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-slate-300">{ev.currency}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="py-2 px-3">
|
||||
|
||||
+10
-10
@@ -348,8 +348,17 @@ function NewsCard({ item, secondary = false }: { item: NewsItem; secondary?: boo
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Titre + heure + Analyse sur la même ligne */}
|
||||
{/* Heure + Analyse + Titre */}
|
||||
<div className="flex items-start gap-2 mb-1.5">
|
||||
<div className="flex items-center gap-1 shrink-0 mt-0.5">
|
||||
<span className="text-[9px] text-white whitespace-nowrap">{formatRelativeTime(item.publishedAt)}</span>
|
||||
{!secondary && (
|
||||
<button onClick={() => setExpanded(e => !e)}
|
||||
className="text-[9px] text-slate-400 hover:text-white px-1.5 py-0.5 rounded-full border border-slate-600/50 transition-colors whitespace-nowrap">
|
||||
{expanded ? "▲" : "▼ Analyse"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<a href={item.url} target="_blank" rel="noopener noreferrer"
|
||||
className="group flex items-start gap-1 flex-1 min-w-0">
|
||||
<span className={`text-[12px] leading-snug font-medium group-hover:text-white transition-colors ${secondary ? "text-slate-400" : "text-slate-200"}`}>
|
||||
@@ -357,15 +366,6 @@ function NewsCard({ item, secondary = false }: { item: NewsItem; secondary?: boo
|
||||
</span>
|
||||
<ExternalLink size={10} className="text-slate-600 group-hover:text-slate-400 shrink-0 mt-0.5" />
|
||||
</a>
|
||||
<div className="flex items-center gap-1 shrink-0 mt-0.5">
|
||||
<span className="text-[9px] text-slate-600 whitespace-nowrap">{formatRelativeTime(item.publishedAt)}</span>
|
||||
{!secondary && (
|
||||
<button onClick={() => setExpanded(e => !e)}
|
||||
className="text-[9px] text-slate-500 hover:text-slate-300 px-1.5 py-0.5 rounded-full border border-slate-700/30 transition-colors whitespace-nowrap">
|
||||
{expanded ? "▲" : "▼ Analyse"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{item.summary && !secondary && (
|
||||
|
||||
+47
-69
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { TrendingUp, TrendingDown, Minus, ArrowRight } from "lucide-react";
|
||||
import { TrendingUp, TrendingDown, Minus } from "lucide-react";
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -107,89 +107,67 @@ export default function YieldsTab({ yieldsData, fxDayPct }: Props) {
|
||||
<span className="text-emerald-400 font-semibold">{sorted[0][0]} {fmt(maxYield)} ↑ max</span>
|
||||
<span className="text-slate-700">|</span>
|
||||
<span className="text-sky-400 font-semibold">{sorted.at(-1)![0]} {fmt(sorted.at(-1)![1])} ↓ min</span>
|
||||
{topCarry[0] && (
|
||||
<>
|
||||
<span className="text-slate-700">|</span>
|
||||
<span className="text-amber-400 font-semibold">
|
||||
★ Top carry : {topCarry[0].long}→{topCarry[0].short} +{topCarry[0].bps}bp
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Classement Yield 10Y ────────────────────────────────────────────── */}
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4">
|
||||
<p className="text-[9px] font-semibold text-slate-500 uppercase tracking-widest mb-3">
|
||||
Classement Yield 10Y
|
||||
</p>
|
||||
<div className="space-y-2.5">
|
||||
{sorted.map(([ccy, yld], i) => {
|
||||
const pct = maxYield > 0 ? (yld / maxYield) * 100 : 0;
|
||||
const delta = dayDeltas[ccy] ?? null;
|
||||
const isMax = i === 0;
|
||||
const isMin = i === sorted.length - 1;
|
||||
const barColor = isMax
|
||||
? "bg-emerald-500"
|
||||
: isMin
|
||||
? "bg-sky-400"
|
||||
: yld > 3.5 ? "bg-green-500"
|
||||
: yld > 2 ? "bg-yellow-500"
|
||||
: "bg-sky-500";
|
||||
|
||||
return (
|
||||
<div key={ccy} className="flex items-center gap-3">
|
||||
<div className="w-24 flex items-center gap-1.5 shrink-0">
|
||||
<span className="text-sm">{CCY_FLAGS[ccy]}</span>
|
||||
<span className="text-[11px] font-semibold text-slate-300 w-7">{ccy}</span>
|
||||
{isMax && <span className="text-[8px] font-medium bg-emerald-500/20 text-emerald-400 rounded-full px-1.5">MAX</span>}
|
||||
{isMin && <span className="text-[8px] font-medium bg-sky-500/20 text-sky-400 rounded-full px-1.5">MIN</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 bg-slate-700/40 rounded-full h-2 overflow-hidden">
|
||||
<div className={`h-full rounded-full transition-all duration-500 ${barColor}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
|
||||
<div className="w-16 text-right text-[11px] font-mono font-semibold text-slate-200">
|
||||
{fmt(yld)}
|
||||
</div>
|
||||
|
||||
<div className={`w-16 text-right text-[10px] font-mono flex items-center justify-end gap-0.5 ${deltaColor(delta)}`}>
|
||||
{deltaIcon(delta)}
|
||||
{delta !== null ? (delta >= 0 ? "+" : "") + delta.toFixed(3) : "—"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Carry Trade + Movers ────────────────────────────────────────────── */}
|
||||
{/* ── Classement Yield 10Y + Movers côte à côte ──────────────────────── */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
{/* Top Carry — compact */}
|
||||
{/* Classement Yield 10Y */}
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4">
|
||||
<p className="text-[9px] font-semibold text-slate-500 uppercase tracking-widest mb-3">
|
||||
🚀 Top Carry Trade
|
||||
Classement Yield 10Y
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{topCarry.map(({ long, short, bps }, i) => {
|
||||
const { label, color } = carryStrength(bps);
|
||||
<div className="space-y-2.5">
|
||||
{sorted.map(([ccy, yld], i) => {
|
||||
const pct = maxYield > 0 ? (yld / maxYield) * 100 : 0;
|
||||
const delta = dayDeltas[ccy] ?? null;
|
||||
const isMax = i === 0;
|
||||
const isMin = i === sorted.length - 1;
|
||||
const barColor = isMax
|
||||
? "bg-emerald-500"
|
||||
: isMin
|
||||
? "bg-sky-400"
|
||||
: yld > 3.5 ? "bg-green-500"
|
||||
: yld > 2 ? "bg-yellow-500"
|
||||
: "bg-sky-500";
|
||||
|
||||
return (
|
||||
<div key={`${long}${short}`} className="flex items-center gap-2">
|
||||
{i === 0 && <span className="text-[8px] text-amber-400 font-bold w-3">★</span>}
|
||||
{i !== 0 && <span className="text-[8px] text-slate-700 w-3">{i + 1}</span>}
|
||||
<span className="text-[11px]">{CCY_FLAGS[long]}</span>
|
||||
<span className="text-[10px] font-semibold text-slate-300">{long}</span>
|
||||
<ArrowRight size={9} className="text-slate-600 shrink-0" />
|
||||
<span className="text-[11px]">{CCY_FLAGS[short]}</span>
|
||||
<span className="text-[10px] font-semibold text-slate-300">{short}</span>
|
||||
<span className="ml-auto text-[10px] font-mono font-bold text-slate-200">+{bps}bp</span>
|
||||
<span className={`text-[8px] font-medium border rounded-full px-1.5 py-0.5 shrink-0 ${color}`}>
|
||||
{label}
|
||||
</span>
|
||||
<div key={ccy} className="flex items-center gap-3">
|
||||
<div className="w-20 flex items-center gap-1.5 shrink-0">
|
||||
<span className="text-sm">{CCY_FLAGS[ccy]}</span>
|
||||
<span className="text-[11px] font-semibold text-slate-300 w-7">{ccy}</span>
|
||||
{isMax && <span className="text-[8px] font-medium bg-emerald-500/20 text-emerald-400 rounded-full px-1.5">MAX</span>}
|
||||
{isMin && <span className="text-[8px] font-medium bg-sky-500/20 text-sky-400 rounded-full px-1.5">MIN</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 bg-slate-700/40 rounded-full h-2 overflow-hidden">
|
||||
<div className={`h-full rounded-full transition-all duration-500 ${barColor}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
|
||||
<div className="w-14 text-right text-[11px] font-mono font-semibold text-slate-200">
|
||||
{fmt(yld)}
|
||||
</div>
|
||||
|
||||
<div className={`w-14 text-right text-[10px] font-mono flex items-center justify-end gap-0.5 ${deltaColor(delta)}`}>
|
||||
{deltaIcon(delta)}
|
||||
{delta !== null ? (delta >= 0 ? "+" : "") + delta.toFixed(3) : "—"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[8px] text-slate-700 mt-3 pt-2 border-t border-slate-800">
|
||||
Long haut yield · Short bas yield · Emprunter la devise bon marché
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Movers du jour — renforcé */}
|
||||
{/* Movers du jour */}
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4">
|
||||
<p className="text-[9px] font-semibold text-slate-500 uppercase tracking-widest mb-3">
|
||||
📊 Movers du Jour — Yield 10Y
|
||||
|
||||
Reference in New Issue
Block a user