rerun complete
This commit is contained in:
parent
11c6c19628
commit
d15e542f20
17 changed files with 4694 additions and 146 deletions
53
apps/stock/web-app/src/components/ErrorBoundary.tsx
Normal file
53
apps/stock/web-app/src/components/ErrorBoundary.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import React, { Component, ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
this.props.fallback || (
|
||||
<div className="flex items-center justify-center h-full bg-background">
|
||||
<div className="text-center p-8">
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-2">
|
||||
Something went wrong
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary mb-4">
|
||||
{this.state.error?.message || 'An unexpected error occurred'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
|
||||
>
|
||||
Reload Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import * as LightweightCharts from 'lightweight-charts';
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { useChartManager } from '@/hooks/useChartManager';
|
||||
|
||||
export interface ChartData {
|
||||
time: number;
|
||||
|
|
@ -35,6 +36,7 @@ export interface ChartProps {
|
|||
}>;
|
||||
tradeMarkers?: TradeMarker[];
|
||||
className?: string;
|
||||
chartId?: string;
|
||||
}
|
||||
|
||||
export function Chart({
|
||||
|
|
@ -46,16 +48,32 @@ export function Chart({
|
|||
overlayData = [],
|
||||
tradeMarkers = [],
|
||||
className = '',
|
||||
chartId = 'default',
|
||||
}: ChartProps) {
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||
const chartRef = useRef<LightweightCharts.IChartApi | null>(null);
|
||||
const mainSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
|
||||
const volumeSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
|
||||
const overlaySeriesRef = useRef<Map<string, LightweightCharts.ISeriesApi<any>>>(new Map());
|
||||
const { createChart, getChart, setMainSeries, setVolumeSeries, addOverlaySeries, disposeChart } = useChartManager();
|
||||
|
||||
// Use a stable unique ID that changes only when chartId prop changes
|
||||
const uniqueChartId = useMemo(() => {
|
||||
return `${chartId}-${Date.now()}`;
|
||||
}, [chartId])
|
||||
const mountedRef = useRef(true);
|
||||
const lastChartIdRef = useRef(chartId);
|
||||
|
||||
// Track component mount state
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
// Cleanup on unmount
|
||||
disposeChart(uniqueChartId);
|
||||
};
|
||||
}, [disposeChart]);
|
||||
|
||||
// Reset zoom handler
|
||||
const resetZoom = useCallback(() => {
|
||||
if (chartRef.current && data.length > 0) {
|
||||
const chartInstance = getChart(uniqueChartId);
|
||||
if (chartInstance && !chartInstance.isDisposed && data.length > 0) {
|
||||
// Get the validated data to ensure we're using the correct time values
|
||||
const validateData = (rawData: any[]) => {
|
||||
const seen = new Set<number>();
|
||||
|
|
@ -84,23 +102,35 @@ export function Chart({
|
|||
const timeRange = lastTime - firstTime;
|
||||
const padding = timeRange * 0.05;
|
||||
|
||||
chartRef.current.timeScale().setVisibleRange({
|
||||
from: (firstTime - padding) as any,
|
||||
to: (lastTime + padding) as any,
|
||||
});
|
||||
try {
|
||||
chartInstance.chart.timeScale().setVisibleRange({
|
||||
from: (firstTime - padding) as any,
|
||||
to: (lastTime + padding) as any,
|
||||
});
|
||||
|
||||
chartInstance.chart.timeScale().fitContent();
|
||||
} catch (error) {
|
||||
console.error('Error resetting zoom:', error);
|
||||
}
|
||||
}
|
||||
|
||||
chartRef.current.timeScale().fitContent();
|
||||
}
|
||||
}, [data]);
|
||||
}, [data, getChart]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartContainerRef.current || !data || !data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let handleResize: (() => void) | undefined;
|
||||
let isCleanedUp = false;
|
||||
|
||||
// Use requestAnimationFrame to ensure DOM is ready and avoid conflicts
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
const initTimeout = setTimeout(() => {
|
||||
if (!chartContainerRef.current || isCleanedUp || !mountedRef.current) return;
|
||||
|
||||
// Create chart
|
||||
const chart = LightweightCharts.createChart(chartContainerRef.current, {
|
||||
// Create chart using the chart manager
|
||||
const chart = createChart(uniqueChartId, chartContainerRef.current, {
|
||||
width: chartContainerRef.current.clientWidth,
|
||||
height: height,
|
||||
layout: {
|
||||
|
|
@ -138,7 +168,10 @@ export function Chart({
|
|||
},
|
||||
});
|
||||
|
||||
chartRef.current = chart;
|
||||
if (!chart) {
|
||||
console.error('Failed to create chart');
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter, validate and sort data
|
||||
const validateAndFilterData = (rawData: any[]) => {
|
||||
|
|
@ -166,8 +199,10 @@ export function Chart({
|
|||
};
|
||||
|
||||
// Create main series
|
||||
let mainSeries: LightweightCharts.ISeriesApi<any> | null = null;
|
||||
|
||||
if (type === 'candlestick' && data[0].open !== undefined) {
|
||||
mainSeriesRef.current = chart.addCandlestickSeries({
|
||||
mainSeries = chart.addCandlestickSeries({
|
||||
upColor: '#10b981',
|
||||
downColor: '#ef4444',
|
||||
borderUpColor: '#10b981',
|
||||
|
|
@ -176,9 +211,9 @@ export function Chart({
|
|||
wickDownColor: '#ef4444',
|
||||
});
|
||||
const validData = validateAndFilterData(data);
|
||||
mainSeriesRef.current.setData(validData as LightweightCharts.CandlestickData[]);
|
||||
mainSeries.setData(validData as LightweightCharts.CandlestickData[]);
|
||||
} else if (type === 'line' || (type === 'candlestick' && data[0].value !== undefined)) {
|
||||
mainSeriesRef.current = chart.addLineSeries({
|
||||
mainSeries = chart.addLineSeries({
|
||||
color: '#3b82f6',
|
||||
lineWidth: 2,
|
||||
});
|
||||
|
|
@ -187,9 +222,9 @@ export function Chart({
|
|||
value: d.value ?? d.close ?? 0
|
||||
}));
|
||||
const validData = validateAndFilterData(lineData);
|
||||
mainSeriesRef.current.setData(validData);
|
||||
mainSeries.setData(validData);
|
||||
} else if (type === 'area') {
|
||||
mainSeriesRef.current = chart.addAreaSeries({
|
||||
mainSeries = chart.addAreaSeries({
|
||||
lineColor: '#3b82f6',
|
||||
topColor: '#3b82f6',
|
||||
bottomColor: 'rgba(59, 130, 246, 0.1)',
|
||||
|
|
@ -200,12 +235,16 @@ export function Chart({
|
|||
value: d.value ?? d.close ?? 0
|
||||
}));
|
||||
const validData = validateAndFilterData(areaData);
|
||||
mainSeriesRef.current.setData(validData);
|
||||
mainSeries.setData(validData);
|
||||
}
|
||||
|
||||
if (mainSeries) {
|
||||
setMainSeries(uniqueChartId, mainSeries);
|
||||
}
|
||||
|
||||
// Add volume if available
|
||||
if (showVolume && data.some(d => d.volume)) {
|
||||
volumeSeriesRef.current = chart.addHistogramSeries({
|
||||
const volumeSeries = chart.addHistogramSeries({
|
||||
color: '#3b82f680',
|
||||
priceFormat: {
|
||||
type: 'volume',
|
||||
|
|
@ -213,7 +252,7 @@ export function Chart({
|
|||
priceScaleId: 'volume',
|
||||
});
|
||||
|
||||
volumeSeriesRef.current.priceScale().applyOptions({
|
||||
volumeSeries.priceScale().applyOptions({
|
||||
scaleMargins: {
|
||||
top: 0.8,
|
||||
bottom: 0,
|
||||
|
|
@ -231,11 +270,11 @@ export function Chart({
|
|||
'#3b82f640',
|
||||
}))
|
||||
);
|
||||
volumeSeriesRef.current.setData(volumeData);
|
||||
volumeSeries.setData(volumeData);
|
||||
setVolumeSeries(uniqueChartId, volumeSeries);
|
||||
}
|
||||
|
||||
// Add overlay series
|
||||
overlaySeriesRef.current.clear();
|
||||
overlayData.forEach((overlay, index) => {
|
||||
const series = chart.addLineSeries({
|
||||
color: overlay.color || ['#ff9800', '#4caf50', '#9c27b0', '#f44336'][index % 4],
|
||||
|
|
@ -260,11 +299,11 @@ export function Chart({
|
|||
}));
|
||||
const validatedData = validateAndFilterData(overlayDataWithTime);
|
||||
series.setData(validatedData);
|
||||
overlaySeriesRef.current.set(overlay.name, series);
|
||||
addOverlaySeries(uniqueChartId, overlay.name, series);
|
||||
});
|
||||
|
||||
// Add trade markers
|
||||
if (tradeMarkers.length > 0 && mainSeriesRef.current) {
|
||||
if (tradeMarkers.length > 0 && mainSeries) {
|
||||
// Sort markers by time to ensure they're in ascending order
|
||||
const sortedMarkers = [...tradeMarkers].sort((a, b) => a.time - b.time);
|
||||
const markers: LightweightCharts.SeriesMarker<LightweightCharts.Time>[] = sortedMarkers.map(marker => ({
|
||||
|
|
@ -276,7 +315,7 @@ export function Chart({
|
|||
id: marker.id,
|
||||
size: 1
|
||||
}));
|
||||
mainSeriesRef.current.setMarkers(markers);
|
||||
mainSeries.setMarkers(markers);
|
||||
}
|
||||
|
||||
// Fit content with a slight delay to ensure all series are loaded
|
||||
|
|
@ -322,29 +361,39 @@ export function Chart({
|
|||
});
|
||||
|
||||
// Handle resize
|
||||
const handleResize = () => {
|
||||
if (chartContainerRef.current && chart) {
|
||||
chart.applyOptions({
|
||||
width: chartContainerRef.current.clientWidth,
|
||||
});
|
||||
handleResize = () => {
|
||||
if (chartContainerRef.current && !isCleanedUp) {
|
||||
const chartInstance = getChart(uniqueChartId);
|
||||
if (chartInstance && !chartInstance.isDisposed) {
|
||||
try {
|
||||
chartInstance.chart.applyOptions({
|
||||
width: chartContainerRef.current.clientWidth,
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore errors if the chart is being disposed
|
||||
if (!error?.message?.includes('disposed')) {
|
||||
console.error('Error resizing chart:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
}, 0); // End of setTimeout
|
||||
}); // End of requestAnimationFrame
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
// Clear all refs before removing the chart
|
||||
mainSeriesRef.current = null;
|
||||
volumeSeriesRef.current = null;
|
||||
overlaySeriesRef.current.clear();
|
||||
if (chartRef.current) {
|
||||
chartRef.current.remove();
|
||||
chartRef.current = null;
|
||||
isCleanedUp = true;
|
||||
cancelAnimationFrame(rafId);
|
||||
if (handleResize) {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
}
|
||||
// Dispose immediately - the chart manager handles cleanup safely
|
||||
disposeChart(uniqueChartId);
|
||||
};
|
||||
}, [data, height, type, showVolume, theme, overlayData, tradeMarkers]);
|
||||
}, [data, height, type, showVolume, theme, overlayData, tradeMarkers, createChart, getChart, setMainSeries, setVolumeSeries, addOverlaySeries, disposeChart]);
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue