fix: TvChart embed-widget-advanced-chart (candlestick, no symbol restriction), unique instance IDs, CHF/SNB trial, NZD candlestick in ReportTab

This commit is contained in:
caty21
2026-06-27 23:09:02 +02:00
parent 204e62cb6c
commit cb52cfad1a
3 changed files with 126 additions and 127 deletions
+1
View File
@@ -10,6 +10,7 @@ const CB_KEYS = [
["CAD", "boc"], ["CAD", "boc"],
["AUD", "rba"], ["AUD", "rba"],
["NZD", "rbnz"], ["NZD", "rbnz"],
["CHF", "snb"], // BNS — essai, null si l'endpoint n'existe pas sur rateprobability.com
]; ];
const HEADERS = { const HEADERS = {
+2 -2
View File
@@ -523,7 +523,7 @@ export default function ReportTab({ calEvents, drivers, cotHistory }: Props) {
<div className="h-px flex-1 bg-sky-500/30" /> <div className="h-px flex-1 bg-sky-500/30" />
</div> </div>
{/* 8 currency mini charts */} {/* 8 currency charts — bougie hebdomadaire */}
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
{[ {[
{ sym: "TVC:DXY", label: "🇺🇸 USD · DXY" }, { sym: "TVC:DXY", label: "🇺🇸 USD · DXY" },
@@ -535,7 +535,7 @@ export default function ReportTab({ calEvents, drivers, cotHistory }: Props) {
{ sym: "FX:AUDUSD", label: "🇦🇺 AUD/USD" }, { sym: "FX:AUDUSD", label: "🇦🇺 AUD/USD" },
{ sym: "FX:NZDUSD", label: "🇳🇿 NZD/USD" }, { sym: "FX:NZDUSD", label: "🇳🇿 NZD/USD" },
].map(({ sym, label }) => ( ].map(({ sym, label }) => (
<TvMiniChart key={sym} symbol={sym} label={label} height={160} showInfo={false} /> <TvAdvancedChart key={sym} symbol={sym} label={label} interval="W" height={160} />
))} ))}
</div> </div>
+123 -125
View File
@@ -2,71 +2,96 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
// ── TvMiniChart ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────────
// Utilise le script embed TradingView dédié (embed-widget-mini-symbol-overview.js) // Chaque instance obtient un identifiant unique injecté dans l'URL du script
// et NON pas tv.js (qui n'expose pas MiniSymbolOverview). // pour forcer le navigateur à ré-exécuter le script même si l'URL est en cache.
// La structure DOM attendue par TradingView : // Sans ça, les 4 charts simultanés du tab Marchés ne s'initialisent pas tous.
// <div class="tradingview-widget-container"> ← wrapper
// <div class="tradingview-widget-container__widget"></div>
// <script src="embed-widget-mini-symbol-overview.js">{config}</script>
// </div>
interface TvMiniChartProps { function mkInstanceId() {
symbol: string; // ex: "SP:SPX", "TVC:DXY", "FX:EURUSD" return Math.random().toString(36).slice(2);
label?: string; // titre affiché au-dessus
interval?: "W" | "D" | "M";
dateRange?: string; // "1D","5D","1M","3M","6M","12M","60M","ALL","YTD"
height?: number;
showInfo?: boolean; // afficher nom + prix sous le graphique
} }
export function TvMiniChart({ // ── 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`;
wrapper.appendChild(widgetDiv);
const script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
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, symbol,
label, label,
dateRange = "1M", interval = "D",
height = 180, height = 250,
showInfo = true, }: TvAdvancedChartProps) {
}: TvMiniChartProps) {
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const initialized = useRef(false); const initialized = useRef(false);
const instanceId = useRef(mkInstanceId());
useEffect(() => { useEffect(() => {
if (initialized.current || !wrapperRef.current) return; if (initialized.current || !wrapperRef.current) return;
initialized.current = true; initialized.current = true;
const wrapper = wrapperRef.current; mountEmbedWidget(
wrapper.innerHTML = ""; wrapperRef.current,
`https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js?t=${instanceId.current}`,
// Div cible du widget {
const widgetDiv = document.createElement("div"); autosize: false,
widgetDiv.className = "tradingview-widget-container__widget"; width: "100%",
wrapper.appendChild(widgetDiv); height,
symbol,
// Script embed avec config inline (textContent lu par le script au chargement) interval,
const script = document.createElement("script"); timezone: "Europe/Paris",
script.type = "text/javascript"; theme: "dark",
script.async = true; style: "1", // bougies japonaises
script.src = "https://s3.tradingview.com/external-embedding/embed-widget-mini-symbol-overview.js"; locale: "fr",
script.text = JSON.stringify({ backgroundColor: "rgba(8,12,20,0)",
symbol, gridColor: "rgba(30,45,61,0.5)",
width: "100%", hide_top_toolbar: true,
height, hide_legend: false,
locale: "fr", allow_symbol_change: false,
dateRange, calendar: false,
colorTheme: "dark", hide_volume: true,
trendLineColor: "#38bdf8", isTransparent: true,
underLineColor: "rgba(56,189,248,0.08)", },
underLineBottomColor: "rgba(56,189,248,0)", height
isTransparent: true, );
autosize: false,
largeChartUrl: "",
noTimeScale: false,
chartOnly: !showInfo,
});
wrapper.appendChild(script);
return () => { return () => {
wrapper.innerHTML = ""; if (wrapperRef.current) wrapperRef.current.innerHTML = "";
initialized.current = false; initialized.current = false;
}; };
}, []); // eslint-disable-line }, []); // eslint-disable-line
@@ -87,85 +112,59 @@ export function TvMiniChart({
); );
} }
// ── TvAdvancedChart ─────────────────────────────────────────────────────────── // ── TvMiniChart ───────────────────────────────────────────────────────────────
// Graphique avancé plein format via tv.js (TradingView.widget). // Graphique léger (ligne/area) via embed-widget-mini-symbol-overview.js.
// Note : certains symboles affichent une popup "disponible uniquement sur TradingView". // Idéal pour les aperçus compacts (CurrencyCard, sidebar, etc.).
declare global { interface TvMiniChartProps {
interface Window { symbol: string;
TradingView?: { label?: string;
widget: new (config: Record<string, unknown>) => void; dateRange?: string; // "1D","5D","1M","3M","6M","12M","60M","ALL","YTD"
}; height?: number;
} showInfo?: boolean;
} }
let tvScriptLoaded = false; export function TvMiniChart({
let tvScriptLoading = false;
const tvCallbacks: (() => void)[] = [];
function loadTvScript(cb: () => void) {
if (tvScriptLoaded) { cb(); return; }
tvCallbacks.push(cb);
if (tvScriptLoading) return;
tvScriptLoading = true;
const s = document.createElement("script");
s.src = "https://s3.tradingview.com/tv.js";
s.async = true;
s.onload = () => {
tvScriptLoaded = true;
tvCallbacks.forEach(f => f());
tvCallbacks.length = 0;
};
document.head.appendChild(s);
}
interface TvAdvancedChartProps {
symbol: string;
label?: string;
interval?: string;
height?: number;
}
export function TvAdvancedChart({
symbol, symbol,
label, label,
interval = "W", dateRange = "1M",
height = 250, height = 180,
}: TvAdvancedChartProps) { showInfo = true,
const uid = useRef(`tv_adv_${Math.random().toString(36).slice(2)}`); }: TvMiniChartProps) {
const ref = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const init = useRef(false); const initialized = useRef(false);
const instanceId = useRef(mkInstanceId());
useEffect(() => { useEffect(() => {
if (init.current) return; if (initialized.current || !wrapperRef.current) return;
init.current = true; initialized.current = true;
loadTvScript(() => { mountEmbedWidget(
if (!window.TradingView || !ref.current) return; wrapperRef.current,
try { `https://s3.tradingview.com/external-embedding/embed-widget-mini-symbol-overview.js?t=${instanceId.current}`,
new window.TradingView.widget({ {
autosize: false, symbol,
width: "100%", width: "100%",
height, height,
symbol, locale: "fr",
interval, dateRange,
timezone: "Europe/Paris", colorTheme: "dark",
theme: "dark", trendLineColor: "#38bdf8",
style: "1", underLineColor: "rgba(56,189,248,0.08)",
locale: "fr", underLineBottomColor: "rgba(56,189,248,0)",
toolbar_bg: "#0f1623", isTransparent: true,
enable_publishing: false, autosize: false,
hide_top_toolbar: true, largeChartUrl: "",
hide_legend: false, noTimeScale: false,
save_image: false, chartOnly: !showInfo,
container_id: uid.current, },
backgroundColor: "rgba(8,12,20,0)", height
gridColor: "rgba(30,45,61,0.5)", );
hide_volume: false,
studies: [], return () => {
}); if (wrapperRef.current) wrapperRef.current.innerHTML = "";
} catch { /* TradingView indisponible */ } initialized.current = false;
}); };
}, []); // eslint-disable-line }, []); // eslint-disable-line
return ( return (
@@ -176,9 +175,8 @@ export function TvAdvancedChart({
</p> </p>
)} )}
<div <div
ref={ref} ref={wrapperRef}
id={uid.current} className="tradingview-widget-container rounded-lg overflow-hidden bg-[#0f1623]"
className="rounded-lg overflow-hidden bg-[#0f1623] border border-white/[0.05]"
style={{ height }} style={{ height }}
/> />
</div> </div>