- Add 'You're paying' and 'You're receiving' sections with proper token dropdowns - Implement balance display and MAX button functionality - Add automatic receiving amount calculation based on paying amount - Enhance token selector with icons, names, and balance information - Improve leverage position value calculations and risk warnings - Update trade execution to use new paying/receiving token structure - Maintain all existing functionality including stop loss, take profit, and position management This creates a more intuitive and professional trading interface that matches Jupiter's UX patterns.
290 lines
9.1 KiB
TypeScript
290 lines
9.1 KiB
TypeScript
'use client'
|
|
import React, { useRef, useEffect, useState } from 'react'
|
|
|
|
interface Position {
|
|
id: string
|
|
symbol: string
|
|
side: 'BUY' | 'SELL'
|
|
amount: number
|
|
entryPrice: number
|
|
stopLoss: number
|
|
takeProfit: number
|
|
currentPrice: number
|
|
unrealizedPnl: number
|
|
leverage: number
|
|
}
|
|
|
|
interface SimpleChartProps {
|
|
symbol?: string
|
|
positions?: Position[]
|
|
}
|
|
|
|
interface CandleData {
|
|
open: number
|
|
high: number
|
|
low: number
|
|
close: number
|
|
time: number
|
|
}
|
|
|
|
export default function SimpleChart({ symbol = 'SOL/USDC', positions = [] }: SimpleChartProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
const [candleData, setCandleData] = useState<CandleData[]>([])
|
|
const [timeframe, setTimeframe] = useState('5m')
|
|
|
|
const timeframes = ['1m', '5m', '15m', '1h', '4h', '1d']
|
|
|
|
// Generate realistic candlestick data
|
|
const generateCandleData = React.useCallback(() => {
|
|
const data: CandleData[] = []
|
|
const basePrice = symbol === 'SOL' ? 166.5 : symbol === 'BTC' ? 42150 : 2580
|
|
let currentPrice = basePrice
|
|
const now = Date.now()
|
|
|
|
for (let i = 60; i >= 0; i--) {
|
|
const timeOffset = i * 5 * 60 * 1000 // 5-minute intervals
|
|
const time = now - timeOffset
|
|
|
|
const volatility = basePrice * 0.002 // 0.2% volatility
|
|
const open = currentPrice
|
|
const change = (Math.random() - 0.5) * volatility * 2
|
|
const close = open + change
|
|
const high = Math.max(open, close) + Math.random() * volatility
|
|
const low = Math.min(open, close) - Math.random() * volatility
|
|
|
|
data.push({ open, high, low, close, time })
|
|
currentPrice = close
|
|
}
|
|
|
|
return data
|
|
}, [symbol])
|
|
|
|
useEffect(() => {
|
|
setCandleData(generateCandleData())
|
|
}, [symbol, timeframe, generateCandleData])
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current
|
|
if (!canvas || candleData.length === 0) return
|
|
|
|
const ctx = canvas.getContext('2d')
|
|
if (!ctx) return
|
|
|
|
// Set canvas size for high DPI displays
|
|
const rect = canvas.getBoundingClientRect()
|
|
const dpr = window.devicePixelRatio || 1
|
|
canvas.width = rect.width * dpr
|
|
canvas.height = rect.height * dpr
|
|
ctx.scale(dpr, dpr)
|
|
|
|
const width = rect.width
|
|
const height = rect.height
|
|
|
|
// Clear canvas with dark background
|
|
ctx.fillStyle = '#0f0f0f'
|
|
ctx.fillRect(0, 0, width, height)
|
|
|
|
// Calculate price range
|
|
const prices = candleData.flatMap(d => [d.high, d.low])
|
|
const maxPrice = Math.max(...prices)
|
|
const minPrice = Math.min(...prices)
|
|
const priceRange = maxPrice - minPrice
|
|
const padding = priceRange * 0.1
|
|
|
|
// Chart dimensions
|
|
const chartLeft = 60
|
|
const chartRight = width - 20
|
|
const chartTop = 40
|
|
const chartBottom = height - 60
|
|
const chartWidth = chartRight - chartLeft
|
|
const chartHeight = chartBottom - chartTop
|
|
|
|
// Draw grid
|
|
ctx.strokeStyle = '#1a1a1a'
|
|
ctx.lineWidth = 1
|
|
|
|
// Horizontal grid lines (price levels)
|
|
const priceStep = (maxPrice - minPrice + padding * 2) / 8
|
|
for (let i = 0; i <= 8; i++) {
|
|
const price = minPrice - padding + i * priceStep
|
|
const y = chartBottom - ((price - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
|
|
|
ctx.beginPath()
|
|
ctx.moveTo(chartLeft, y)
|
|
ctx.lineTo(chartRight, y)
|
|
ctx.stroke()
|
|
|
|
// Price labels
|
|
ctx.fillStyle = '#666'
|
|
ctx.font = '11px Arial'
|
|
ctx.textAlign = 'right'
|
|
ctx.fillText(price.toFixed(2), chartLeft - 5, y + 4)
|
|
}
|
|
|
|
// Vertical grid lines (time)
|
|
const timeStep = chartWidth / 12
|
|
for (let i = 0; i <= 12; i++) {
|
|
const x = chartLeft + i * timeStep
|
|
|
|
ctx.beginPath()
|
|
ctx.moveTo(x, chartTop)
|
|
ctx.lineTo(x, chartBottom)
|
|
ctx.stroke()
|
|
}
|
|
|
|
// Draw candlesticks
|
|
const candleWidth = Math.max(2, chartWidth / candleData.length - 2)
|
|
|
|
candleData.forEach((candle, index) => {
|
|
const x = chartLeft + (index / (candleData.length - 1)) * chartWidth
|
|
|
|
const openY = chartBottom - ((candle.open - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
|
const closeY = chartBottom - ((candle.close - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
|
const highY = chartBottom - ((candle.high - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
|
const lowY = chartBottom - ((candle.low - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
|
|
|
const isGreen = candle.close > candle.open
|
|
const color = isGreen ? '#26a69a' : '#ef5350'
|
|
|
|
ctx.strokeStyle = color
|
|
ctx.fillStyle = color
|
|
ctx.lineWidth = 1
|
|
|
|
// Draw wick (high-low line)
|
|
ctx.beginPath()
|
|
ctx.moveTo(x, highY)
|
|
ctx.lineTo(x, lowY)
|
|
ctx.stroke()
|
|
|
|
// Draw candle body
|
|
const bodyTop = Math.min(openY, closeY)
|
|
const bodyHeight = Math.abs(closeY - openY)
|
|
|
|
if (isGreen) {
|
|
ctx.strokeRect(x - candleWidth / 2, bodyTop, candleWidth, Math.max(bodyHeight, 1))
|
|
} else {
|
|
ctx.fillRect(x - candleWidth / 2, bodyTop, candleWidth, Math.max(bodyHeight, 1))
|
|
}
|
|
})
|
|
|
|
// Draw position overlays
|
|
positions.forEach((position) => {
|
|
if (!position.symbol.includes(symbol.replace('/USDC', ''))) return
|
|
|
|
const entryY = chartBottom - ((position.entryPrice - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
|
const stopLossY = chartBottom - ((position.stopLoss - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
|
const takeProfitY = chartBottom - ((position.takeProfit - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
|
|
|
// Entry price line
|
|
ctx.strokeStyle = position.side === 'BUY' ? '#26a69a' : '#ef5350'
|
|
ctx.lineWidth = 2
|
|
ctx.setLineDash([5, 5])
|
|
ctx.beginPath()
|
|
ctx.moveTo(chartLeft, entryY)
|
|
ctx.lineTo(chartRight, entryY)
|
|
ctx.stroke()
|
|
|
|
// Stop loss line
|
|
ctx.strokeStyle = '#ef5350'
|
|
ctx.lineWidth = 1
|
|
ctx.setLineDash([3, 3])
|
|
ctx.beginPath()
|
|
ctx.moveTo(chartLeft, stopLossY)
|
|
ctx.lineTo(chartRight, stopLossY)
|
|
ctx.stroke()
|
|
|
|
// Take profit line
|
|
ctx.strokeStyle = '#26a69a'
|
|
ctx.lineWidth = 1
|
|
ctx.setLineDash([3, 3])
|
|
ctx.beginPath()
|
|
ctx.moveTo(chartLeft, takeProfitY)
|
|
ctx.lineTo(chartRight, takeProfitY)
|
|
ctx.stroke()
|
|
|
|
ctx.setLineDash([]) // Reset line dash
|
|
})
|
|
|
|
// Draw current price line
|
|
const currentPrice = candleData[candleData.length - 1]?.close || 0
|
|
const currentPriceY = chartBottom - ((currentPrice - (minPrice - padding)) / (maxPrice - minPrice + padding * 2)) * chartHeight
|
|
|
|
ctx.strokeStyle = '#ffa726'
|
|
ctx.lineWidth = 2
|
|
ctx.setLineDash([])
|
|
ctx.beginPath()
|
|
ctx.moveTo(chartLeft, currentPriceY)
|
|
ctx.lineTo(chartRight, currentPriceY)
|
|
ctx.stroke()
|
|
|
|
// Current price label
|
|
ctx.fillStyle = '#ffa726'
|
|
ctx.fillRect(chartRight - 60, currentPriceY - 10, 60, 20)
|
|
ctx.fillStyle = '#000'
|
|
ctx.font = '12px Arial'
|
|
ctx.textAlign = 'center'
|
|
ctx.fillText(currentPrice.toFixed(2), chartRight - 30, currentPriceY + 4)
|
|
|
|
// Chart title
|
|
ctx.fillStyle = '#ffffff'
|
|
ctx.font = 'bold 16px Arial'
|
|
ctx.textAlign = 'left'
|
|
ctx.fillText(`${symbol} - ${timeframe}`, 20, 25)
|
|
|
|
}, [symbol, positions, candleData, timeframe])
|
|
|
|
return (
|
|
<div className="w-full bg-gray-900 rounded-lg border border-gray-700">
|
|
{/* Chart Controls */}
|
|
<div className="flex items-center justify-between p-3 border-b border-gray-700">
|
|
<div className="flex items-center space-x-4">
|
|
<h3 className="text-white font-medium">{symbol}</h3>
|
|
<div className="flex space-x-1">
|
|
{timeframes.map(tf => (
|
|
<button
|
|
key={tf}
|
|
onClick={() => setTimeframe(tf)}
|
|
className={`px-2 py-1 text-xs rounded transition-all ${
|
|
timeframe === tf
|
|
? 'bg-blue-600 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
{tf}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-sm text-gray-400">
|
|
{candleData.length > 0 && (
|
|
<span>
|
|
Last: ${candleData[candleData.length - 1]?.close.toFixed(2)} •
|
|
24h Vol: ${(Math.random() * 1000000).toFixed(0)}M
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Chart Canvas */}
|
|
<div className="relative">
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="w-full"
|
|
style={{ height: '400px', display: 'block' }}
|
|
/>
|
|
|
|
{/* Legend */}
|
|
{positions.length > 0 && (
|
|
<div className="absolute top-2 right-2 bg-gray-800/90 rounded p-2 text-xs space-y-1">
|
|
<div className="text-yellow-400">— Current Price</div>
|
|
<div className="text-green-400">⋯ Take Profit</div>
|
|
<div className="text-red-400">⋯ Stop Loss</div>
|
|
<div className="text-blue-400">⋯ Entry Price</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|