From 60d124a6bf050bba14c65959e5d037f185f0ce95 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Sat, 28 Jun 2025 23:03:38 +0300 Subject: [PATCH] revert charts fix --- .../src/routes/coin/[coinSymbol]/+page.svelte | 359 ++++++++---------- 1 file changed, 155 insertions(+), 204 deletions(-) diff --git a/website/src/routes/coin/[coinSymbol]/+page.svelte b/website/src/routes/coin/[coinSymbol]/+page.svelte index 9b8f653..980159f 100644 --- a/website/src/routes/coin/[coinSymbol]/+page.svelte +++ b/website/src/routes/coin/[coinSymbol]/+page.svelte @@ -11,7 +11,14 @@ import CoinSkeleton from '$lib/components/self/skeletons/CoinSkeleton.svelte'; import TopHolders from '$lib/components/self/TopHolders.svelte'; import { TrendingUp, TrendingDown, DollarSign, Coins, ChartColumn } from 'lucide-svelte'; - import { onMount, onDestroy } from 'svelte'; + import { + createChart, + ColorType, + type IChartApi, + CandlestickSeries, + HistogramSeries + } from 'lightweight-charts'; + import { onMount } from 'svelte'; import { goto } from '$app/navigation'; import { toast } from 'svelte-sonner'; import CoinIcon from '$lib/components/self/CoinIcon.svelte'; @@ -21,7 +28,6 @@ import { websocketController, type PriceUpdate, isConnectedStore } from '$lib/stores/websocket'; import SEO from '$lib/components/self/SEO.svelte'; import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte'; - import { browser } from '$app/environment'; const { data } = $props(); let coinSymbol = $derived(data.coinSymbol); @@ -38,15 +44,6 @@ let previousCoinSymbol = $state(null); - // Chart-related variables - only used on client - let chartContainer = $state(); - let chart: any = null; - let candlestickSeries: any = null; - let volumeSeries: any = null; - let chartLibrary: any = null; - let chartInitialized = $state(false); - let resizeHandler: (() => void) | null = null; - const timeframeOptions = [ { value: '1m', label: '1 minute' }, { value: '5m', label: '5 minutes' }, @@ -57,59 +54,22 @@ ]; onMount(async () => { - // Only import chart library on client-side - if (browser) { - try { - const chartModule = await import('lightweight-charts'); - chartLibrary = { - createChart: chartModule.createChart, - ColorType: chartModule.ColorType, - CandlestickSeries: chartModule.CandlestickSeries, - HistogramSeries: chartModule.HistogramSeries - }; - console.log('Chart library loaded successfully'); - } catch (e) { - console.error('Failed to load chart library:', e); - } - } - await loadUserHolding(); - if (browser) { - websocketController.setCoin(coinSymbol.toUpperCase()); - websocketController.subscribeToPriceUpdates(coinSymbol.toUpperCase(), handlePriceUpdate); - } + websocketController.setCoin(coinSymbol.toUpperCase()); + websocketController.subscribeToPriceUpdates(coinSymbol.toUpperCase(), handlePriceUpdate); previousCoinSymbol = coinSymbol; }); - onDestroy(() => { - if (browser) { - cleanupChart(); + $effect(() => { + return () => { if (previousCoinSymbol) { websocketController.unsubscribeFromPriceUpdates(previousCoinSymbol.toUpperCase()); } - } + }; }); - function cleanupChart() { - if (!browser) return; - - if (resizeHandler) { - window.removeEventListener('resize', resizeHandler); - resizeHandler = null; - } - - if (chart) { - chart.remove(); - chart = null; - } - - candlestickSeries = null; - volumeSeries = null; - chartInitialized = false; - } - $effect(() => { if (coinSymbol && previousCoinSymbol && coinSymbol !== previousCoinSymbol) { loading = true; @@ -118,33 +78,148 @@ volumeData = []; userHolding = 0; - if (browser) { - websocketController.unsubscribeFromPriceUpdates(previousCoinSymbol.toUpperCase()); - } + websocketController.unsubscribeFromPriceUpdates(previousCoinSymbol.toUpperCase()); loadCoinData().then(() => { loadUserHolding(); - if (browser) { - websocketController.setCoin(coinSymbol.toUpperCase()); - websocketController.subscribeToPriceUpdates(coinSymbol.toUpperCase(), handlePriceUpdate); - } + websocketController.setCoin(coinSymbol.toUpperCase()); + websocketController.subscribeToPriceUpdates(coinSymbol.toUpperCase(), handlePriceUpdate); previousCoinSymbol = coinSymbol; }); } }); - // Chart initialization effect - $effect(() => { - if (!browser || !chartLibrary || !chartContainer || !chartData || chartData.length === 0) { - return; - } + async function loadCoinData() { + try { + loading = true; + const response = await fetch(`/api/coin/${coinSymbol}?timeframe=${selectedTimeframe}`); - // Clean up existing chart - cleanupChart(); + if (!response.ok) { + toast.error(response.status === 404 ? 'Coin not found' : 'Failed to load coin data'); + return; + } + + const result = await response.json(); + coin = result.coin; + chartData = result.candlestickData || []; + volumeData = result.volumeData || []; + } catch (e) { + console.error('Failed to fetch coin data:', e); + toast.error('Failed to load coin data'); + } finally { + loading = false; + } + } + + async function loadUserHolding() { + if (!$USER_DATA) return; try { - const { createChart, ColorType, CandlestickSeries, HistogramSeries } = chartLibrary; - + const response = await fetch('/api/portfolio/total'); + if (response.ok) { + const result = await response.json(); + const holding = result.coinHoldings.find((h: any) => h.symbol === coinSymbol.toUpperCase()); + userHolding = holding ? holding.quantity : 0; + } + } catch (e) { + console.error('Failed to load user holding:', e); + } + } + async function handleTradeSuccess() { + await Promise.all([loadCoinData(), loadUserHolding(), fetchPortfolioSummary()]); + } + function handlePriceUpdate(priceUpdate: PriceUpdate) { + if (coin && priceUpdate.coinSymbol === coinSymbol.toUpperCase()) { + // throttle updates to prevent excessive UI updates, 1s interval + const now = Date.now(); + if (now - lastPriceUpdateTime < 1000) { + return; + } + lastPriceUpdateTime = now; + + coin = { + ...coin, + currentPrice: priceUpdate.currentPrice, + marketCap: priceUpdate.marketCap, + change24h: priceUpdate.change24h, + volume24h: priceUpdate.volume24h, + ...(priceUpdate.poolCoinAmount !== undefined && { + poolCoinAmount: priceUpdate.poolCoinAmount + }), + ...(priceUpdate.poolBaseCurrencyAmount !== undefined && { + poolBaseCurrencyAmount: priceUpdate.poolBaseCurrencyAmount + }) + }; + + updateChartRealtime(priceUpdate.currentPrice); + } + } + + function updateChartRealtime(newPrice: number) { + if (!candlestickSeries || !chart || chartData.length === 0) return; + + const timeframeSeconds = getTimeframeInSeconds(selectedTimeframe); + const currentTime = Math.floor(Date.now() / 1000); + + const currentCandleTime = Math.floor(currentTime / timeframeSeconds) * timeframeSeconds; + const localCandleTime = timeToLocal(currentCandleTime); + + const lastCandle = chartData[chartData.length - 1]; + + if (lastCandle && lastCandle.time === localCandleTime) { + const updatedCandle = { + time: localCandleTime, + open: lastCandle.open, + high: Math.max(lastCandle.high, newPrice), + low: Math.min(lastCandle.low, newPrice), + close: newPrice + }; + + candlestickSeries.update(updatedCandle); + chartData[chartData.length - 1] = updatedCandle; + } else if (localCandleTime > (lastCandle?.time || 0)) { + const newCandle = { + time: localCandleTime, + open: newPrice, + high: newPrice, + low: newPrice, + close: newPrice + }; + + candlestickSeries.update(newCandle); + chartData.push(newCandle); + } + } + + async function handleTimeframeChange(timeframe: string) { + selectedTimeframe = timeframe; + loading = true; + + if (chart) { + chart.remove(); + chart = null; + } + + await loadCoinData(); + loading = false; + } + + let currentTimeframeLabel = $derived( + timeframeOptions.find((option) => option.value === selectedTimeframe)?.label || '1 minute' + ); + + let chartContainer = $state(); + let chart: IChartApi | null = null; + let candlestickSeries: any = null; + let volumeSeries: any = null; + + $effect(() => { + if (chart && chartData.length > 0) { + chart.remove(); + chart = null; + } + + if (chartContainer && chartData.length > 0) { chart = createChart(chartContainer, { layout: { textColor: '#666666', @@ -179,7 +254,6 @@ horzLine: { color: '#758696', width: 1, style: 2, visible: true, labelVisible: true } } }); - candlestickSeries = chart.addSeries(CandlestickSeries, { upColor: '#26a69a', downColor: '#ef5350', @@ -221,147 +295,24 @@ chart.timeScale().fitContent(); - // Setup resize handler - resizeHandler = () => { - if (chart && chartContainer) { - chart.applyOptions({ width: chartContainer.clientWidth }); - } - }; - window.addEventListener('resize', resizeHandler); - resizeHandler(); // Initial call + const handleResize = () => chart?.applyOptions({ width: chartContainer?.clientWidth }); + window.addEventListener('resize', handleResize); + handleResize(); candlestickSeries.priceScale().applyOptions({ borderColor: '#71649C' }); volumeSeries.priceScale().applyOptions({ borderColor: '#71649C' }); chart.timeScale().applyOptions({ borderColor: '#71649C' }); - chartInitialized = true; - console.log('Chart initialized successfully'); - } catch (error) { - console.error('Failed to initialize chart:', error); + return () => { + window.removeEventListener('resize', handleResize); + if (chart) { + chart.remove(); + chart = null; + } + }; } }); - async function loadCoinData() { - try { - loading = true; - const response = await fetch(`/api/coin/${coinSymbol}?timeframe=${selectedTimeframe}`); - - if (!response.ok) { - toast.error(response.status === 404 ? 'Coin not found' : 'Failed to load coin data'); - return; - } - - const result = await response.json(); - coin = result.coin; - chartData = result.candlestickData || []; - volumeData = result.volumeData || []; - } catch (e) { - console.error('Failed to fetch coin data:', e); - toast.error('Failed to load coin data'); - } finally { - loading = false; - } - } - - async function loadUserHolding() { - if (!$USER_DATA) return; - - try { - const response = await fetch('/api/portfolio/total'); - if (response.ok) { - const result = await response.json(); - const holding = result.coinHoldings.find((h: any) => h.symbol === coinSymbol.toUpperCase()); - userHolding = holding ? holding.quantity : 0; - } - } catch (e) { - console.error('Failed to load user holding:', e); - } - } - - async function handleTradeSuccess() { - await Promise.all([loadCoinData(), loadUserHolding(), fetchPortfolioSummary()]); - } - - function handlePriceUpdate(priceUpdate: PriceUpdate) { - if (!browser || !coin || priceUpdate.coinSymbol !== coinSymbol.toUpperCase()) { - return; - } - - // throttle updates to prevent excessive UI updates, 1s interval - const now = Date.now(); - if (now - lastPriceUpdateTime < 1000) { - return; - } - lastPriceUpdateTime = now; - - coin = { - ...coin, - currentPrice: priceUpdate.currentPrice, - marketCap: priceUpdate.marketCap, - change24h: priceUpdate.change24h, - volume24h: priceUpdate.volume24h, - ...(priceUpdate.poolCoinAmount !== undefined && { - poolCoinAmount: priceUpdate.poolCoinAmount - }), - ...(priceUpdate.poolBaseCurrencyAmount !== undefined && { - poolBaseCurrencyAmount: priceUpdate.poolBaseCurrencyAmount - }) - }; - - updateChartRealtime(priceUpdate.currentPrice); - } - - function updateChartRealtime(newPrice: number) { - if (!browser || !candlestickSeries || !chart || chartData.length === 0) return; - - const timeframeSeconds = getTimeframeInSeconds(selectedTimeframe); - const currentTime = Math.floor(Date.now() / 1000); - - const currentCandleTime = Math.floor(currentTime / timeframeSeconds) * timeframeSeconds; - const localCandleTime = timeToLocal(currentCandleTime); - - const lastCandle = chartData[chartData.length - 1]; - - if (lastCandle && lastCandle.time === localCandleTime) { - const updatedCandle = { - time: localCandleTime, - open: lastCandle.open, - high: Math.max(lastCandle.high, newPrice), - low: Math.min(lastCandle.low, newPrice), - close: newPrice - }; - - candlestickSeries.update(updatedCandle); - chartData[chartData.length - 1] = updatedCandle; - } else if (localCandleTime > (lastCandle?.time || 0)) { - const newCandle = { - time: localCandleTime, - open: newPrice, - high: newPrice, - low: newPrice, - close: newPrice - }; - - candlestickSeries.update(newCandle); - chartData.push(newCandle); - } - } - - async function handleTimeframeChange(timeframe: string) { - selectedTimeframe = timeframe; - loading = true; - - // Clean up chart before loading new data - cleanupChart(); - - await loadCoinData(); - loading = false; - } - - let currentTimeframeLabel = $derived( - timeframeOptions.find((option) => option.value === selectedTimeframe)?.label || '1 minute' - ); - function formatPrice(price: number): string { if (price < 0.000001) { return price.toFixed(8); @@ -730,4 +681,4 @@ {/if} - + \ No newline at end of file