fix: TvMiniChart utilise embed-widget-mini-symbol-overview.js (pas tv.js)

MiniSymbolOverview n'existe pas dans tv.js → try/catch avalait l'erreur silencieusement
→ graphiques vides. Correction : embed script dédié avec config inline (script.text).
Structure DOM correcte : .tradingview-widget-container__widget + script sibling.
TvAdvancedChart (tv.js + TradingView.widget) inchangé.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
caty21
2026-06-27 22:36:27 +02:00
parent c82dbb397b
commit 1e1b95fdc0
+131 -95
View File
@@ -1,19 +1,18 @@
"use client";
import { useEffect, useRef, useId } from "react";
import { useEffect, useRef } from "react";
// Déclaration globale TradingView (chargé via script)
declare global {
interface Window {
TradingView?: {
MiniSymbolOverview: new (config: Record<string, unknown>) => void;
widget: new (config: Record<string, unknown>) => void;
};
}
}
// ── TvMiniChart ───────────────────────────────────────────────────────────────
// Utilise le script embed TradingView dédié (embed-widget-mini-symbol-overview.js)
// et NON pas tv.js (qui n'expose pas MiniSymbolOverview).
// La structure DOM attendue par TradingView :
// <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 {
symbol: string; // ex: "FX:EURUSD", "TVC:DXY", "SP:SPX"
symbol: string; // ex: "SP:SPX", "TVC:DXY", "FX:EURUSD"
label?: string; // titre affiché au-dessus
interval?: "W" | "D" | "M";
dateRange?: string; // "1D","5D","1M","3M","6M","12M","60M","ALL","YTD"
@@ -21,89 +20,122 @@ interface TvMiniChartProps {
showInfo?: boolean; // afficher nom + prix sous le graphique
}
// Script TradingView chargé une seule fois
let scriptLoaded = false;
let scriptLoading = false;
const onLoadCallbacks: (() => void)[] = [];
export function TvMiniChart({
symbol,
label,
dateRange = "1M",
height = 180,
showInfo = true,
}: TvMiniChartProps) {
const wrapperRef = useRef<HTMLDivElement>(null);
const initialized = useRef(false);
useEffect(() => {
if (initialized.current || !wrapperRef.current) return;
initialized.current = true;
const wrapper = wrapperRef.current;
wrapper.innerHTML = "";
// Div cible du widget
const widgetDiv = document.createElement("div");
widgetDiv.className = "tradingview-widget-container__widget";
wrapper.appendChild(widgetDiv);
// Script embed avec config inline (textContent lu par le script au chargement)
const script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
script.src = "https://s3.tradingview.com/external-embedding/embed-widget-mini-symbol-overview.js";
script.text = JSON.stringify({
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,
});
wrapper.appendChild(script);
return () => {
wrapper.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>
);
}
// ── TvAdvancedChart ───────────────────────────────────────────────────────────
// Graphique avancé plein format via tv.js (TradingView.widget).
// Note : certains symboles affichent une popup "disponible uniquement sur TradingView".
declare global {
interface Window {
TradingView?: {
widget: new (config: Record<string, unknown>) => void;
};
}
}
let tvScriptLoaded = false;
let tvScriptLoading = false;
const tvCallbacks: (() => void)[] = [];
function loadTvScript(cb: () => void) {
if (scriptLoaded) { cb(); return; }
onLoadCallbacks.push(cb);
if (scriptLoading) return;
scriptLoading = true;
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 = () => {
scriptLoaded = true;
onLoadCallbacks.forEach(f => f());
onLoadCallbacks.length = 0;
tvScriptLoaded = true;
tvCallbacks.forEach(f => f());
tvCallbacks.length = 0;
};
document.head.appendChild(s);
}
export function TvMiniChart({ symbol, label, dateRange = "1M", height = 180, showInfo = true }: TvMiniChartProps) {
const uid = useId().replace(/:/g, "_");
const id = `tv_mini_${uid}`;
interface TvAdvancedChartProps {
symbol: string;
label?: string;
interval?: string;
height?: number;
}
export function TvAdvancedChart({
symbol,
label,
interval = "W",
height = 250,
}: TvAdvancedChartProps) {
const uid = useRef(`tv_adv_${Math.random().toString(36).slice(2)}`);
const ref = useRef<HTMLDivElement>(null);
const init = useRef(false);
useEffect(() => {
if (init.current) return;
init.current = true;
loadTvScript(() => {
if (!window.TradingView || !ref.current) return;
try {
new window.TradingView.MiniSymbolOverview({
symbol,
container_id: id,
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,
});
} catch { /* TradingView indisponible */ }
});
}, []); // 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={ref}
id={id}
className="rounded-lg overflow-hidden bg-[#0f1623]"
style={{ height }}
/>
</div>
);
}
// ── Vue avancée plein format (pour la page graphiques) ──────────────────────
interface TvAdvancedChartProps {
symbol: string;
label?: string;
interval?: string;
height?: number;
}
export function TvAdvancedChart({ symbol, label, interval = "W", height = 250 }: TvAdvancedChartProps) {
const uid = useId().replace(/:/g, "_");
const id = `tv_adv_${uid}`;
const ref = useRef<HTMLDivElement>(null);
const init = useRef(false);
useEffect(() => {
if (init.current) return;
init.current = true;
@@ -112,25 +144,25 @@ export function TvAdvancedChart({ symbol, label, interval = "W", height = 250 }:
if (!window.TradingView || !ref.current) return;
try {
new window.TradingView.widget({
autosize: false,
width: "100%",
autosize: false,
width: "100%",
height,
symbol,
interval,
timezone: "Europe/Paris",
theme: "dark",
style: "1",
locale: "fr",
toolbar_bg: "#0f1623",
timezone: "Europe/Paris",
theme: "dark",
style: "1",
locale: "fr",
toolbar_bg: "#0f1623",
enable_publishing: false,
hide_top_toolbar: true,
hide_legend: false,
save_image: false,
container_id: id,
backgroundColor: "rgba(8,12,20,0)",
gridColor: "rgba(30,45,61,0.5)",
hide_volume: false,
studies: [],
container_id: uid.current,
backgroundColor: "rgba(8,12,20,0)",
gridColor: "rgba(30,45,61,0.5)",
hide_volume: false,
studies: [],
});
} catch { /* TradingView indisponible */ }
});
@@ -138,10 +170,14 @@ export function TvAdvancedChart({ symbol, label, interval = "W", height = 250 }:
return (
<div className="flex flex-col gap-1">
{label && <p className="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">{label}</p>}
{label && (
<p className="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">
{label}
</p>
)}
<div
ref={ref}
id={id}
id={uid.current}
className="rounded-lg overflow-hidden bg-[#0f1623] border border-white/[0.05]"
style={{ height }}
/>