Event order book started. Needs work still.
This commit is contained in:
689
frontend/src/components/bets/TradingPanel.tsx
Normal file
689
frontend/src/components/bets/TradingPanel.tsx
Normal file
@ -0,0 +1,689 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { spreadBetsApi } from '@/api/spread-bets'
|
||||
import type { SportEventWithBets, SpreadGridBet } from '@/types/sport-event'
|
||||
import { TeamSide } from '@/types/spread-bet'
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Clock,
|
||||
Activity,
|
||||
DollarSign,
|
||||
Users,
|
||||
Zap,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
Target,
|
||||
BarChart3,
|
||||
} from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
interface TradingPanelProps {
|
||||
event: SportEventWithBets
|
||||
onBetCreated?: () => void
|
||||
onBetTaken?: () => void
|
||||
}
|
||||
|
||||
// Helper to format time until game
|
||||
function formatTimeUntil(gameTime: string): { text: string; urgent: boolean } {
|
||||
const now = new Date()
|
||||
const game = new Date(gameTime)
|
||||
const diff = game.getTime() - now.getTime()
|
||||
|
||||
if (diff <= 0) return { text: 'LIVE', urgent: true }
|
||||
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000)
|
||||
|
||||
if (days > 0) return { text: `${days}d ${hours}h`, urgent: false }
|
||||
if (hours > 0) return { text: `${hours}h ${minutes}m`, urgent: hours < 2 }
|
||||
if (minutes > 0) return { text: `${minutes}m ${seconds}s`, urgent: true }
|
||||
return { text: `${seconds}s`, urgent: true }
|
||||
}
|
||||
|
||||
// Order book row component
|
||||
interface OrderRowProps {
|
||||
spread: number
|
||||
bets: SpreadGridBet[]
|
||||
side: 'home' | 'away'
|
||||
officialSpread: number
|
||||
maxVolume: number
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const OrderRow = ({ spread, bets, side, officialSpread, maxVolume, onClick }: OrderRowProps) => {
|
||||
const totalVolume = bets.reduce((sum, b) => sum + b.stake, 0)
|
||||
const betCount = bets.length
|
||||
const openBets = bets.filter(b => b.status === 'open')
|
||||
const takeableBets = openBets.filter(b => b.can_take)
|
||||
const volumePercent = maxVolume > 0 ? (totalVolume / maxVolume) * 100 : 0
|
||||
const isOfficial = spread === officialSpread
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`
|
||||
w-full grid grid-cols-4 gap-2 py-1.5 px-2 text-sm transition-colors relative overflow-hidden
|
||||
${side === 'home'
|
||||
? 'hover:bg-green-900/30 text-green-400'
|
||||
: 'hover:bg-red-900/30 text-red-400'
|
||||
}
|
||||
${isOfficial ? 'bg-yellow-900/20' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Volume bar background */}
|
||||
<div
|
||||
className={`absolute inset-y-0 ${side === 'home' ? 'right-0' : 'left-0'} opacity-20 ${
|
||||
side === 'home' ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}
|
||||
style={{ width: `${volumePercent}%` }}
|
||||
/>
|
||||
|
||||
{side === 'home' ? (
|
||||
<>
|
||||
<span className="text-left relative z-10 text-gray-400">{betCount || '-'}</span>
|
||||
<span className="text-right relative z-10 font-mono">
|
||||
{totalVolume > 0 ? `$${totalVolume.toLocaleString()}` : '-'}
|
||||
</span>
|
||||
<span className={`text-right relative z-10 font-bold ${isOfficial ? 'text-yellow-400' : ''}`}>
|
||||
{spread > 0 ? '+' : ''}{spread}
|
||||
{isOfficial && <span className="ml-1 text-yellow-500">★</span>}
|
||||
</span>
|
||||
<span className="text-right relative z-10">
|
||||
{takeableBets.length > 0 && (
|
||||
<span className="px-1.5 py-0.5 bg-green-600 text-white text-xs rounded">
|
||||
{takeableBets.length}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-left relative z-10">
|
||||
{takeableBets.length > 0 && (
|
||||
<span className="px-1.5 py-0.5 bg-red-600 text-white text-xs rounded">
|
||||
{takeableBets.length}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={`text-left relative z-10 font-bold ${isOfficial ? 'text-yellow-400' : ''}`}>
|
||||
{spread > 0 ? '+' : ''}{spread}
|
||||
{isOfficial && <span className="ml-1 text-yellow-500">★</span>}
|
||||
</span>
|
||||
<span className="text-left relative z-10 font-mono">
|
||||
{totalVolume > 0 ? `$${totalVolume.toLocaleString()}` : '-'}
|
||||
</span>
|
||||
<span className="text-right relative z-10 text-gray-400">{betCount || '-'}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// Recent trade item
|
||||
interface RecentTradeProps {
|
||||
bet: SpreadGridBet & { spread: number }
|
||||
homeTeam: string
|
||||
awayTeam: string
|
||||
}
|
||||
|
||||
const RecentTrade = ({ bet, homeTeam, awayTeam }: RecentTradeProps) => {
|
||||
const isHome = bet.team === 'home'
|
||||
return (
|
||||
<div className="flex items-center justify-between py-1 text-xs">
|
||||
<span className={isHome ? 'text-green-400' : 'text-red-400'}>
|
||||
{isHome ? homeTeam : awayTeam} {bet.spread > 0 ? '+' : ''}{bet.spread}
|
||||
</span>
|
||||
<span className="text-gray-400 font-mono">${bet.stake.toFixed(0)}</span>
|
||||
<span className={`${bet.status === 'open' ? 'text-blue-400' : 'text-yellow-400'}`}>
|
||||
{bet.status}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelProps) => {
|
||||
const { isAuthenticated } = useAuthStore()
|
||||
const queryClient = useQueryClient()
|
||||
const [timeUntil, setTimeUntil] = useState(formatTimeUntil(event.game_time))
|
||||
const [selectedSide, setSelectedSide] = useState<'home' | 'away'>('home')
|
||||
const [selectedSpread, setSelectedSpread] = useState(event.official_spread)
|
||||
const [stakeAmount, setStakeAmount] = useState('')
|
||||
const [orderType, setOrderType] = useState<'create' | 'take'>('create')
|
||||
|
||||
// Update countdown every second
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTimeUntil(formatTimeUntil(event.game_time))
|
||||
}, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [event.game_time])
|
||||
|
||||
// Calculate market stats
|
||||
const marketStats = useMemo(() => {
|
||||
let totalVolume = 0
|
||||
let totalBets = 0
|
||||
let openBets = 0
|
||||
let matchedBets = 0
|
||||
let homeVolume = 0
|
||||
let awayVolume = 0
|
||||
|
||||
Object.entries(event.spread_grid).forEach(([, bets]) => {
|
||||
bets.forEach(bet => {
|
||||
totalVolume += bet.stake
|
||||
totalBets++
|
||||
if (bet.status === 'open') openBets++
|
||||
if (bet.status === 'matched') matchedBets++
|
||||
if (bet.team === 'home') homeVolume += bet.stake
|
||||
else awayVolume += bet.stake
|
||||
})
|
||||
})
|
||||
|
||||
return { totalVolume, totalBets, openBets, matchedBets, homeVolume, awayVolume }
|
||||
}, [event.spread_grid])
|
||||
|
||||
// Get sorted spreads for order book
|
||||
const sortedSpreads = useMemo(() => {
|
||||
return Object.keys(event.spread_grid)
|
||||
.map(Number)
|
||||
.sort((a, b) => b - a) // High to low
|
||||
}, [event.spread_grid])
|
||||
|
||||
// Get max volume for visualization
|
||||
const maxVolume = useMemo(() => {
|
||||
let max = 0
|
||||
Object.values(event.spread_grid).forEach(bets => {
|
||||
const vol = bets.reduce((sum, b) => sum + b.stake, 0)
|
||||
if (vol > max) max = vol
|
||||
})
|
||||
return max
|
||||
}, [event.spread_grid])
|
||||
|
||||
// Get recent bets
|
||||
const recentBets = useMemo(() => {
|
||||
const allBets: (SpreadGridBet & { spread: number })[] = []
|
||||
Object.entries(event.spread_grid).forEach(([spread, bets]) => {
|
||||
bets.forEach(bet => {
|
||||
allBets.push({ ...bet, spread: Number(spread) })
|
||||
})
|
||||
})
|
||||
return allBets.slice(0, 10)
|
||||
}, [event.spread_grid])
|
||||
|
||||
// Get available bets to take at selected spread
|
||||
const availableBets = useMemo(() => {
|
||||
const bets = event.spread_grid[selectedSpread.toString()] || []
|
||||
return bets.filter(b => b.status === 'open' && b.can_take && b.team !== selectedSide)
|
||||
}, [event.spread_grid, selectedSpread, selectedSide])
|
||||
|
||||
// Create bet mutation
|
||||
const createBetMutation = useMutation({
|
||||
mutationFn: (data: { event_id: number; spread: number; team: TeamSide; stake_amount: number }) =>
|
||||
spreadBetsApi.createBet(data),
|
||||
onSuccess: () => {
|
||||
toast.success('Bet created successfully!')
|
||||
setStakeAmount('')
|
||||
queryClient.invalidateQueries({ queryKey: ['sport-event'] })
|
||||
onBetCreated?.()
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || 'Failed to create bet')
|
||||
},
|
||||
})
|
||||
|
||||
// Take bet mutation
|
||||
const takeBetMutation = useMutation({
|
||||
mutationFn: (betId: number) => spreadBetsApi.takeBet(betId),
|
||||
onSuccess: () => {
|
||||
toast.success('Bet taken successfully!')
|
||||
setStakeAmount('')
|
||||
queryClient.invalidateQueries({ queryKey: ['sport-event'] })
|
||||
onBetTaken?.()
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || 'Failed to take bet')
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!isAuthenticated) {
|
||||
toast.error('Please log in to place a bet')
|
||||
return
|
||||
}
|
||||
|
||||
const amount = parseFloat(stakeAmount)
|
||||
if (isNaN(amount) || amount < event.min_bet_amount || amount > event.max_bet_amount) {
|
||||
toast.error(`Stake must be between $${event.min_bet_amount} and $${event.max_bet_amount}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (orderType === 'take' && availableBets.length > 0) {
|
||||
// Take the first available bet that matches the stake
|
||||
const matchingBet = availableBets.find(b => b.stake === amount) || availableBets[0]
|
||||
takeBetMutation.mutate(matchingBet.bet_id)
|
||||
} else {
|
||||
createBetMutation.mutate({
|
||||
event_id: event.id,
|
||||
spread: selectedSpread,
|
||||
team: selectedSide === 'home' ? TeamSide.HOME : TeamSide.AWAY,
|
||||
stake_amount: amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSpreadSelect = (spread: number) => {
|
||||
setSelectedSpread(spread)
|
||||
}
|
||||
|
||||
// Quick stake buttons
|
||||
const quickStakes = [25, 50, 100, 250, 500, 1000]
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-xl overflow-hidden shadow-2xl">
|
||||
{/* Header - Event Info */}
|
||||
<div className="bg-gradient-to-r from-gray-800 to-gray-900 p-4 border-b border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Home Team */}
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-green-900/50 rounded-full flex items-center justify-center mb-1">
|
||||
<span className="text-2xl font-bold text-green-400">
|
||||
{event.home_team.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-white font-semibold text-sm">{event.home_team}</p>
|
||||
<p className="text-green-400 text-xs">HOME</p>
|
||||
</div>
|
||||
|
||||
{/* VS / Spread */}
|
||||
<div className="text-center px-4">
|
||||
<div className="text-gray-400 text-xs mb-1">SPREAD</div>
|
||||
<div className="text-2xl font-bold text-yellow-400">
|
||||
{event.official_spread > 0 ? '+' : ''}{event.official_spread}
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs">Official Line</div>
|
||||
</div>
|
||||
|
||||
{/* Away Team */}
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-red-900/50 rounded-full flex items-center justify-center mb-1">
|
||||
<span className="text-2xl font-bold text-red-400">
|
||||
{event.away_team.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-white font-semibold text-sm">{event.away_team}</p>
|
||||
<p className="text-red-400 text-xs">AWAY</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Game Time & Status */}
|
||||
<div className="text-right">
|
||||
<div className={`
|
||||
inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm font-semibold
|
||||
${timeUntil.urgent
|
||||
? 'bg-red-900/50 text-red-400 animate-pulse'
|
||||
: 'bg-gray-800 text-gray-300'
|
||||
}
|
||||
`}>
|
||||
<Clock size={14} />
|
||||
{timeUntil.text}
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
{new Date(event.game_time).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</p>
|
||||
{event.league && (
|
||||
<p className="text-gray-600 text-xs mt-0.5">{event.league}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Market Stats Ticker */}
|
||||
<div className="bg-gray-800/50 px-4 py-2 flex items-center justify-between text-xs border-b border-gray-700 overflow-x-auto">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<DollarSign size={12} className="text-gray-500" />
|
||||
<span className="text-gray-400">Volume</span>
|
||||
<span className="text-white font-semibold">${marketStats.totalVolume.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Activity size={12} className="text-gray-500" />
|
||||
<span className="text-gray-400">Bets</span>
|
||||
<span className="text-white font-semibold">{marketStats.totalBets}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Zap size={12} className="text-green-500" />
|
||||
<span className="text-gray-400">Open</span>
|
||||
<span className="text-green-400 font-semibold">{marketStats.openBets}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Users size={12} className="text-yellow-500" />
|
||||
<span className="text-gray-400">Matched</span>
|
||||
<span className="text-yellow-400 font-semibold">{marketStats.matchedBets}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TrendingUp size={12} className="text-green-500" />
|
||||
<span className="text-green-400">${marketStats.homeVolume.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TrendingDown size={12} className="text-red-500" />
|
||||
<span className="text-red-400">${marketStats.awayVolume.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Trading Area */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-0 lg:divide-x lg:divide-gray-700">
|
||||
{/* Order Book */}
|
||||
<div className="lg:col-span-2 p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-white font-semibold flex items-center gap-2">
|
||||
<BarChart3 size={16} className="text-gray-400" />
|
||||
Order Book
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-green-400">{event.home_team}</span>
|
||||
<span className="text-gray-500">/</span>
|
||||
<span className="text-red-400">{event.away_team}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* Home Side (Buy/Green) */}
|
||||
<div className="bg-gray-800/30 rounded-lg overflow-hidden">
|
||||
<div className="grid grid-cols-4 gap-2 py-2 px-2 text-xs text-gray-500 border-b border-gray-700">
|
||||
<span>Count</span>
|
||||
<span className="text-right">Volume</span>
|
||||
<span className="text-right">Spread</span>
|
||||
<span className="text-right">Open</span>
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
{sortedSpreads.map(spread => {
|
||||
const bets = event.spread_grid[spread.toString()] || []
|
||||
const homeBets = bets.filter(b => b.team === 'home')
|
||||
if (homeBets.length === 0 && spread !== event.official_spread) return null
|
||||
return (
|
||||
<OrderRow
|
||||
key={`home-${spread}`}
|
||||
spread={spread}
|
||||
bets={homeBets}
|
||||
side="home"
|
||||
officialSpread={event.official_spread}
|
||||
maxVolume={maxVolume}
|
||||
onClick={() => {
|
||||
setSelectedSide('home')
|
||||
handleSpreadSelect(spread)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Away Side (Sell/Red) */}
|
||||
<div className="bg-gray-800/30 rounded-lg overflow-hidden">
|
||||
<div className="grid grid-cols-4 gap-2 py-2 px-2 text-xs text-gray-500 border-b border-gray-700">
|
||||
<span>Open</span>
|
||||
<span>Spread</span>
|
||||
<span>Volume</span>
|
||||
<span className="text-right">Count</span>
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
{sortedSpreads.map(spread => {
|
||||
const bets = event.spread_grid[spread.toString()] || []
|
||||
const awayBets = bets.filter(b => b.team === 'away')
|
||||
if (awayBets.length === 0 && spread !== event.official_spread) return null
|
||||
return (
|
||||
<OrderRow
|
||||
key={`away-${spread}`}
|
||||
spread={-spread} // Show opposite spread for away
|
||||
bets={awayBets}
|
||||
side="away"
|
||||
officialSpread={-event.official_spread}
|
||||
maxVolume={maxVolume}
|
||||
onClick={() => {
|
||||
setSelectedSide('away')
|
||||
handleSpreadSelect(spread)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="mt-4">
|
||||
<h4 className="text-gray-400 text-xs font-semibold mb-2 flex items-center gap-1">
|
||||
<Activity size={12} />
|
||||
Recent Activity
|
||||
</h4>
|
||||
<div className="bg-gray-800/30 rounded-lg p-2 max-h-32 overflow-y-auto">
|
||||
{recentBets.length > 0 ? (
|
||||
recentBets.map((bet, i) => (
|
||||
<RecentTrade
|
||||
key={`${bet.bet_id}-${i}`}
|
||||
bet={bet}
|
||||
homeTeam={event.home_team}
|
||||
awayTeam={event.away_team}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-500 text-xs text-center py-2">No bets yet</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Trade Panel */}
|
||||
<div className="p-4 bg-gray-800/30">
|
||||
<h3 className="text-white font-semibold mb-4 flex items-center gap-2">
|
||||
<Target size={16} className="text-gray-400" />
|
||||
Place Bet
|
||||
</h3>
|
||||
|
||||
{!isAuthenticated ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-gray-400 mb-4">Log in to place bets</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Link
|
||||
to="/login"
|
||||
className="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-colors text-sm"
|
||||
>
|
||||
Log In
|
||||
</Link>
|
||||
<Link
|
||||
to="/register"
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-500 transition-colors text-sm"
|
||||
>
|
||||
Sign Up
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Side Selection */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-4">
|
||||
<button
|
||||
onClick={() => setSelectedSide('home')}
|
||||
className={`
|
||||
py-3 rounded-lg font-semibold transition-all
|
||||
${selectedSide === 'home'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-700 text-gray-400 hover:bg-gray-600'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<ChevronUp size={16} className="inline mr-1" />
|
||||
{event.home_team}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedSide('away')}
|
||||
className={`
|
||||
py-3 rounded-lg font-semibold transition-all
|
||||
${selectedSide === 'away'
|
||||
? 'bg-red-600 text-white'
|
||||
: 'bg-gray-700 text-gray-400 hover:bg-gray-600'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<ChevronDown size={16} className="inline mr-1" />
|
||||
{event.away_team}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Spread Selection */}
|
||||
<div className="mb-4">
|
||||
<label className="text-gray-400 text-xs block mb-1">Spread</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedSpread(s => Math.max(event.min_spread, s - 0.5))}
|
||||
className="px-3 py-2 bg-gray-700 text-white rounded hover:bg-gray-600"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<div className="flex-1 text-center">
|
||||
<span className="text-2xl font-bold text-white">
|
||||
{selectedSpread > 0 ? '+' : ''}{selectedSpread}
|
||||
</span>
|
||||
{selectedSpread === event.official_spread && (
|
||||
<span className="ml-2 text-yellow-400 text-xs">Official</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedSpread(s => Math.min(event.max_spread, s + 0.5))}
|
||||
className="px-3 py-2 bg-gray-700 text-white rounded hover:bg-gray-600"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Type */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-4">
|
||||
<button
|
||||
onClick={() => setOrderType('create')}
|
||||
className={`
|
||||
py-2 text-sm rounded-lg font-medium transition-all
|
||||
${orderType === 'create'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 text-gray-400 hover:bg-gray-600'
|
||||
}
|
||||
`}
|
||||
>
|
||||
Create Bet
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setOrderType('take')}
|
||||
disabled={availableBets.length === 0}
|
||||
className={`
|
||||
py-2 text-sm rounded-lg font-medium transition-all
|
||||
${orderType === 'take'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-gray-700 text-gray-400 hover:bg-gray-600'
|
||||
}
|
||||
${availableBets.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
`}
|
||||
>
|
||||
Take Bet {availableBets.length > 0 && `(${availableBets.length})`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stake Amount */}
|
||||
<div className="mb-4">
|
||||
<label className="text-gray-400 text-xs block mb-1">Stake Amount</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">$</span>
|
||||
<input
|
||||
type="number"
|
||||
value={stakeAmount}
|
||||
onChange={(e) => setStakeAmount(e.target.value)}
|
||||
placeholder={`${event.min_bet_amount} - ${event.max_bet_amount}`}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg py-3 pl-8 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Stakes */}
|
||||
<div className="grid grid-cols-3 gap-2 mb-4">
|
||||
{quickStakes
|
||||
.filter(s => s >= event.min_bet_amount && s <= event.max_bet_amount)
|
||||
.map(stake => (
|
||||
<button
|
||||
key={stake}
|
||||
onClick={() => setStakeAmount(stake.toString())}
|
||||
className="py-2 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
${stake}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={createBetMutation.isPending || takeBetMutation.isPending || !stakeAmount}
|
||||
className={`
|
||||
w-full py-4 rounded-lg font-bold text-white transition-all
|
||||
${selectedSide === 'home'
|
||||
? 'bg-green-600 hover:bg-green-500'
|
||||
: 'bg-red-600 hover:bg-red-500'
|
||||
}
|
||||
${(createBetMutation.isPending || takeBetMutation.isPending || !stakeAmount)
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: ''
|
||||
}
|
||||
`}
|
||||
>
|
||||
{createBetMutation.isPending || takeBetMutation.isPending
|
||||
? 'Processing...'
|
||||
: orderType === 'take'
|
||||
? `Take ${selectedSide === 'home' ? event.away_team : event.home_team} Bet`
|
||||
: `Bet on ${selectedSide === 'home' ? event.home_team : event.away_team}`
|
||||
}
|
||||
</button>
|
||||
|
||||
{/* Bet Summary */}
|
||||
{stakeAmount && (
|
||||
<div className="mt-4 p-3 bg-gray-700/50 rounded-lg text-xs">
|
||||
<div className="flex justify-between text-gray-400 mb-1">
|
||||
<span>Your Position</span>
|
||||
<span className={selectedSide === 'home' ? 'text-green-400' : 'text-red-400'}>
|
||||
{selectedSide === 'home' ? event.home_team : event.away_team} {selectedSpread > 0 ? '+' : ''}{selectedSide === 'home' ? selectedSpread : -selectedSpread}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-400 mb-1">
|
||||
<span>Stake</span>
|
||||
<span className="text-white">${parseFloat(stakeAmount).toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-400 mb-1">
|
||||
<span>Potential Win</span>
|
||||
<span className="text-green-400">
|
||||
${(parseFloat(stakeAmount) * 0.9 * 2).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-400">
|
||||
<span>House Fee (10%)</span>
|
||||
<span className="text-yellow-400">${(parseFloat(stakeAmount) * 0.1).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user