pummmp.fun/components/trading-chart.tsx
2026-07-04 12:49:09 -07:00

247 lines
8.3 KiB
TypeScript

'use client'
// gpt idk how to do ts
import { useEffect, useRef, useState, useMemo } from 'react'
import { createChart, ColorType, IChartApi, Time } from 'lightweight-charts'
import { PricePoint } from '@/types'
interface TradingChartProps {
priceHistory: PricePoint[]
currentPrice: number
solPrice?: number
}
function aggregateToCandles(data: { time: number; value: number }[], periodSeconds = 60 * 5) {
if (data.length === 0) return []
const candles = []
let currentCandleStartTime = Math.floor(data[0].time / periodSeconds) * periodSeconds
let currentOpen = data[0].value
let currentHigh = data[0].value
let currentLow = data[0].value
let currentClose = data[0].value
for (let i = 0; i < data.length; i++) {
const point = data[i]
if (point.time < currentCandleStartTime + periodSeconds) {
currentHigh = Math.max(currentHigh, point.value)
currentLow = Math.min(currentLow, point.value)
currentClose = point.value
} else {
candles.push({
time: currentCandleStartTime as Time,
open: currentOpen,
high: currentHigh,
low: currentLow,
close: currentClose,
})
currentOpen = currentClose
currentCandleStartTime = Math.floor(point.time / periodSeconds) * periodSeconds
currentHigh = point.value
currentLow = point.value
currentClose = point.value
}
}
candles.push({
time: currentCandleStartTime as Time,
open: currentOpen,
high: currentHigh,
low: currentLow,
close: currentClose,
})
return candles
}
export function TradingChart({ priceHistory, currentPrice, solPrice = 0 }: TradingChartProps) {
const chartContainerRef = useRef<HTMLDivElement>(null)
const chartRef = useRef<IChartApi | null>(null)
const [showUsd, setShowUsd] = useState(true)
const baseData = useMemo(() => {
return priceHistory.map(point => ({
time: (new Date(point.timestamp).getTime() / 1000),
value: showUsd ? point.price * (solPrice || 1) : point.price
})).sort((a, b) => (a.time as number) - (b.time as number))
}, [priceHistory, showUsd, solPrice])
const displayData = useMemo(() => {
const data = [...baseData]
if (data.length > 0) {
const lastTime = data[data.length - 1].time
const now = Math.floor(Date.now() / 1000)
const currentVal = showUsd ? currentPrice * (solPrice || 1) : currentPrice
if (now > lastTime) {
data.push({ time: now, value: currentVal })
} else {
data[data.length - 1].value = currentVal
}
} else {
const now = Math.floor(Date.now() / 1000)
const currentVal = showUsd ? currentPrice * (solPrice || 1) : currentPrice
data.push({ time: now, value: currentVal })
}
return data
}, [baseData, currentPrice, solPrice, showUsd])
const startPrice = displayData[0]?.value || 0
const endPrice = displayData[displayData.length - 1]?.value || 0
const priceChange = startPrice !== 0 ? ((endPrice - startPrice) / startPrice) * 100 : 0
const isPositive = priceChange >= 0
const displayPrice = endPrice
const formatPrice = (price: number) => {
if (price === 0) return '0.00'
if (price < 0.00000001) return price.toFixed(12)
if (price < 0.000001) return price.toFixed(9)
if (price < 0.001) return price.toFixed(7)
if (price < 1) return price.toFixed(5)
return price.toFixed(3)
}
useEffect(() => {
if (!chartContainerRef.current) return
const handleResize = () => {
chartRef.current?.applyOptions({ width: chartContainerRef.current!.clientWidth })
}
const chart = createChart(chartContainerRef.current, {
layout: {
background: { type: ColorType.Solid, color: 'transparent' },
textColor: '#9ca3af',
},
width: chartContainerRef.current.clientWidth,
height: 500,
grid: {
vertLines: { color: 'rgba(42, 46, 57, 0.1)' },
horzLines: { color: 'rgba(42, 46, 57, 0.1)' },
},
rightPriceScale: {
borderColor: 'rgba(197, 203, 206, 0.1)',
},
timeScale: {
borderColor: 'rgba(197, 203, 206, 0.1)',
timeVisible: true,
secondsVisible: false,
rightOffset: 12,
barSpacing: 6,
},
crosshair: {
mode: 1, // @type CrosshairMode.Normal
}
})
// Create Candle Series
let series: any;
try {
// Standard v3/v4 way
series = (chart as any).addCandlestickSeries({
upColor: '#10b981',
downColor: '#ef4444',
borderVisible: false,
wickUpColor: '#10b981',
wickDownColor: '#ef4444',
priceFormat: {
type: 'custom',
formatter: (price: number) => {
if (price < 0.000001) return price.toFixed(9)
if (price < 0.001) return price.toFixed(6)
if (price < 1) return price.toFixed(4)
return price.toFixed(2)
},
minMove: 0.000000001,
}
})
// Aggregate data into candles (5 min intervals? or appropriate based on history length)
// If history is short (new coin), use small interval (1 min)
// If long, use longer.
const duration = displayData.length > 0 ? displayData[displayData.length - 1].time - displayData[0].time : 0
const interval = duration < 3600 ? 60 : 300 // 1 min vs 5 min
const candleData = aggregateToCandles(displayData, interval)
series.setData(candleData)
// Removed fitContent to avoid "zoomed all the way in" look on load
// chart.timeScale().fitContent()
chartRef.current = chart
} catch (e) {
console.error("Failed to chart series:", e)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
chart.remove()
}
}, [displayData, isPositive])
return (
<div className="rounded-2xl border border-border/50 bg-card/30 p-6 backdrop-blur-sm">
<div className="mb-6 flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<h2 className="text-sm text-muted-foreground font-mono">
{priceHistory.length > 0 ? new Date(priceHistory[0].timestamp).toLocaleDateString() : 'Today'}
{' '}-{' '}
Live
</h2>
<button
onClick={() => setShowUsd(!showUsd)}
className="rounded bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground hover:bg-muted/80 hover:text-foreground"
>
Switch to {showUsd ? 'SOL' : 'USD'}
</button>
</div>
<div className="mt-1 flex items-baseline gap-3">
<span className={`font-mono text-3xl font-semibold ${isPositive ? 'text-[#10b981]' : 'text-[#ef4444]'}`}>
{showUsd ? (
'$'
) : (
<div
className="inline-block h-6 w-6 mr-3 mt-1 bg-current"
style={{
mask: 'url(/solana.svg) center / contain no-repeat',
WebkitMask: 'url(/solana.svg) center / contain no-repeat',
}}
/>
)}
{formatPrice(displayPrice)}
</span>
<span className={`flex items-center gap-1 text-sm font-medium ${
isPositive ? 'text-[#10b981]' : 'text-[#ef4444]'
}`}>
{isPositive ? '↑' : '↓'}{Math.abs(priceChange).toFixed(2)}%
</span>
</div>
<div className="text-xs text-muted-foreground font-mono mt-1">
MCAP: {showUsd ? '$' : (
<div
className="inline-block h-3 w-3 mr-2 opacity-50 bg-current"
style={{
mask: 'url(/solana.svg) center / contain no-repeat',
WebkitMask: 'url(/solana.svg) center / contain no-repeat',
}}
/>
)}
{formatPrice(displayPrice * 1_000_000_000)} {showUsd ? 'USD' : 'SOL'}
{/* !! note: this is a rough estimation based on 1b supply for display only! */}
</div>
</div>
</div>
<div ref={chartContainerRef} className="w-full" />
</div>
)
}