mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-07-27 20:37:45 +00:00
d3c5340f9b
- charts Marchés : interval D/W → 4H (240), hauteur 220 → 420px, grid 2col - TvAdvancedChart : bouton "↗ TradingView" par chart (outils tracé complets) - cache fichier IL 2h (data/il-enrichment-cache.json) : API rate-proba 13s → <500ms - stale-while-revalidate macro : affiche cache immédiatement, refetch 10s background - seuil cache rate-proba : 48h → 7j pour dev local Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
210 lines
7.2 KiB
TypeScript
210 lines
7.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
// Chaque instance obtient un identifiant unique injecté dans l'URL du script
|
|
// pour forcer le navigateur à ré-exécuter le script même si l'URL est en cache.
|
|
// Sans ça, les 4 charts simultanés du tab Marchés ne s'initialisent pas tous.
|
|
|
|
function mkInstanceId() {
|
|
return Math.random().toString(36).slice(2);
|
|
}
|
|
|
|
// ── Structure DOM commune attendue par les widgets embed TradingView ──────────
|
|
// <div class="tradingview-widget-container"> ← wrapperRef
|
|
// <div class="tradingview-widget-container__widget"></div>
|
|
// <script src="embed-widget-*.js?t=INSTANCE_ID">{config JSON}</script>
|
|
// </div>
|
|
|
|
function mountEmbedWidget(
|
|
wrapper: HTMLDivElement,
|
|
scriptSrc: string,
|
|
config: Record<string, unknown>,
|
|
height: number
|
|
) {
|
|
wrapper.innerHTML = "";
|
|
|
|
const widgetDiv = document.createElement("div");
|
|
widgetDiv.className = "tradingview-widget-container__widget";
|
|
widgetDiv.style.width = "100%";
|
|
widgetDiv.style.height = `${height}px`;
|
|
// container_id permet au widget de trouver son propre conteneur parmi plusieurs instances
|
|
if (config.container_id) widgetDiv.id = String(config.container_id);
|
|
wrapper.appendChild(widgetDiv);
|
|
|
|
const script = document.createElement("script");
|
|
script.type = "text/javascript";
|
|
script.async = false; // false → document.currentScript défini lors de l'exécution
|
|
script.src = scriptSrc;
|
|
script.text = JSON.stringify(config);
|
|
wrapper.appendChild(script);
|
|
}
|
|
|
|
// ── TvAdvancedChart ───────────────────────────────────────────────────────────
|
|
// Graphique avancé (bougies) via embed-widget-advanced-chart.js.
|
|
// Utilise une iframe TradingView → accès aux mêmes symboles que le site web
|
|
// (SP:SPX, TVC:VIX, TVC:DXY, TVC:GOLD, etc.) sans restriction "abonnement".
|
|
|
|
interface TvAdvancedChartProps {
|
|
symbol: string; // ex: "SP:SPX", "TVC:DXY", "FX:EURUSD"
|
|
label?: string;
|
|
interval?: string; // "D", "W", "M", "60", etc.
|
|
height?: number;
|
|
}
|
|
|
|
export function TvAdvancedChart({
|
|
symbol,
|
|
label,
|
|
interval = "D",
|
|
height = 250,
|
|
}: TvAdvancedChartProps) {
|
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
const initialized = useRef(false);
|
|
const instanceId = useRef(mkInstanceId());
|
|
|
|
useEffect(() => {
|
|
if (initialized.current || !wrapperRef.current) return;
|
|
initialized.current = true;
|
|
|
|
const containerId = `tv_adv_${instanceId.current}`;
|
|
mountEmbedWidget(
|
|
wrapperRef.current,
|
|
`https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js?t=${instanceId.current}`,
|
|
{
|
|
autosize: false,
|
|
width: "100%",
|
|
height,
|
|
symbol,
|
|
interval,
|
|
timezone: "Europe/Paris",
|
|
theme: "dark",
|
|
style: "1",
|
|
locale: "fr",
|
|
backgroundColor: "rgba(8,12,20,0)",
|
|
gridColor: "rgba(30,45,61,0.5)",
|
|
hide_top_toolbar: false,
|
|
hide_side_toolbar: false,
|
|
hide_legend: false,
|
|
allow_symbol_change: false,
|
|
calendar: false,
|
|
hide_volume: false,
|
|
isTransparent: true,
|
|
save_image: true,
|
|
withdateranges: true,
|
|
drawings_access: { type: "all" },
|
|
support_host: "https://www.tradingview.com",
|
|
container_id: containerId,
|
|
},
|
|
height
|
|
);
|
|
|
|
return () => {
|
|
if (wrapperRef.current) wrapperRef.current.innerHTML = "";
|
|
initialized.current = false;
|
|
};
|
|
}, []); // eslint-disable-line
|
|
|
|
const tvUrl = `https://www.tradingview.com/chart/?symbol=${encodeURIComponent(symbol)}&interval=${interval}`;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1">
|
|
<div className="flex items-center justify-between">
|
|
{label && (
|
|
<p className="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">{label}</p>
|
|
)}
|
|
<a
|
|
href={tvUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-1 text-[9px] text-slate-600 hover:text-sky-400 transition-colors ml-auto"
|
|
title="Ouvrir dans TradingView (outils de tracé complets)"
|
|
>
|
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
<polyline points="15 3 21 3 21 9"/>
|
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
</svg>
|
|
TradingView
|
|
</a>
|
|
</div>
|
|
<div
|
|
ref={wrapperRef}
|
|
className="tradingview-widget-container rounded-lg overflow-hidden bg-[#0f1623]"
|
|
style={{ height }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── TvMiniChart ───────────────────────────────────────────────────────────────
|
|
// Graphique léger (ligne/area) via embed-widget-mini-symbol-overview.js.
|
|
// Idéal pour les aperçus compacts (CurrencyCard, sidebar, etc.).
|
|
|
|
interface TvMiniChartProps {
|
|
symbol: string;
|
|
label?: string;
|
|
dateRange?: string; // "1D","5D","1M","3M","6M","12M","60M","ALL","YTD"
|
|
height?: number;
|
|
showInfo?: boolean;
|
|
}
|
|
|
|
export function TvMiniChart({
|
|
symbol,
|
|
label,
|
|
dateRange = "1M",
|
|
height = 180,
|
|
showInfo = true,
|
|
}: TvMiniChartProps) {
|
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
const initialized = useRef(false);
|
|
const instanceId = useRef(mkInstanceId());
|
|
|
|
useEffect(() => {
|
|
if (initialized.current || !wrapperRef.current) return;
|
|
initialized.current = true;
|
|
|
|
mountEmbedWidget(
|
|
wrapperRef.current,
|
|
`https://s3.tradingview.com/external-embedding/embed-widget-mini-symbol-overview.js?t=${instanceId.current}`,
|
|
{
|
|
symbol,
|
|
width: "100%",
|
|
height,
|
|
locale: "fr",
|
|
dateRange,
|
|
colorTheme: "dark",
|
|
trendLineColor: "#38bdf8",
|
|
underLineColor: "rgba(56,189,248,0.08)",
|
|
underLineBottomColor: "rgba(56,189,248,0)",
|
|
isTransparent: true,
|
|
autosize: false,
|
|
largeChartUrl: "",
|
|
noTimeScale: false,
|
|
chartOnly: !showInfo,
|
|
},
|
|
height
|
|
);
|
|
|
|
return () => {
|
|
if (wrapperRef.current) wrapperRef.current.innerHTML = "";
|
|
initialized.current = false;
|
|
};
|
|
}, []); // eslint-disable-line
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1">
|
|
{label && (
|
|
<p className="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">
|
|
{label}
|
|
</p>
|
|
)}
|
|
<div
|
|
ref={wrapperRef}
|
|
className="tradingview-widget-container rounded-lg overflow-hidden bg-[#0f1623]"
|
|
style={{ height }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|