feat: Implement Jupiter-style trading interface with token selection
- 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.
This commit is contained in:
@@ -1,100 +1,289 @@
|
||||
'use client'
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
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?: any[]
|
||||
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) return
|
||||
if (!canvas || candleData.length === 0) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
// Set canvas size
|
||||
canvas.width = 800
|
||||
canvas.height = 400
|
||||
// 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)
|
||||
|
||||
// Clear canvas
|
||||
ctx.fillStyle = '#1a1a1a'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
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 = '#333'
|
||||
ctx.strokeStyle = '#1a1a1a'
|
||||
ctx.lineWidth = 1
|
||||
|
||||
// Vertical lines
|
||||
for (let x = 0; x < canvas.width; x += 40) {
|
||||
// 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(x, 0)
|
||||
ctx.lineTo(x, canvas.height)
|
||||
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()
|
||||
}
|
||||
|
||||
// Horizontal lines
|
||||
for (let y = 0; y < canvas.height; y += 40) {
|
||||
// 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(0, y)
|
||||
ctx.lineTo(canvas.width, y)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Draw sample candlesticks
|
||||
const candleWidth = 20
|
||||
const basePrice = 166.5
|
||||
const priceScale = 2
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const x = 40 + i * 25
|
||||
const open = basePrice + (Math.random() - 0.5) * 10
|
||||
const close = open + (Math.random() - 0.5) * 5
|
||||
const high = Math.max(open, close) + Math.random() * 3
|
||||
const low = Math.min(open, close) - Math.random() * 3
|
||||
|
||||
const openY = canvas.height - (open - 150) * priceScale
|
||||
const closeY = canvas.height - (close - 150) * priceScale
|
||||
const highY = canvas.height - (high - 150) * priceScale
|
||||
const lowY = canvas.height - (low - 150) * priceScale
|
||||
|
||||
// Determine color
|
||||
const isGreen = close > open
|
||||
ctx.fillStyle = isGreen ? '#26a69a' : '#ef5350'
|
||||
ctx.strokeStyle = isGreen ? '#26a69a' : '#ef5350'
|
||||
|
||||
// Draw wick
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + candleWidth / 2, highY)
|
||||
ctx.lineTo(x + candleWidth / 2, lowY)
|
||||
ctx.moveTo(x, highY)
|
||||
ctx.lineTo(x, lowY)
|
||||
ctx.stroke()
|
||||
|
||||
// Draw body
|
||||
// Draw candle body
|
||||
const bodyTop = Math.min(openY, closeY)
|
||||
const bodyHeight = Math.abs(closeY - openY)
|
||||
ctx.fillRect(x, bodyTop, candleWidth, Math.max(bodyHeight, 2))
|
||||
}
|
||||
|
||||
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 price labels
|
||||
ctx.fillStyle = '#ffffff'
|
||||
// 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.fillText(`${symbol} - $${basePrice.toFixed(2)}`, 10, 30)
|
||||
ctx.fillStyle = '#26a69a'
|
||||
ctx.fillText('+2.45%', 10, 50)
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText(currentPrice.toFixed(2), chartRight - 30, currentPriceY + 4)
|
||||
|
||||
}, [symbol, positions])
|
||||
// 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">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-96 bg-gray-800 rounded border border-gray-600"
|
||||
style={{ maxWidth: '100%', height: '400px' }}
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user