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:
caty21
2026-06-28 14:13:17 +02:00
parent 2673686743
commit 05803c8547
3 changed files with 79 additions and 81 deletions
+22 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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