mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-23 17:38:05 +00:00
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:
@@ -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 = {
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user