Event order book started. Needs work still.

This commit is contained in:
2026-01-09 17:15:16 -06:00
parent adb6a42039
commit 0dd77eee90
4 changed files with 883 additions and 7 deletions

174
backend/add_bets.py Normal file
View File

@ -0,0 +1,174 @@
"""
Add 100 spread bets to the Wake Forest vs MS State game
"""
import asyncio
import random
from datetime import datetime
from decimal import Decimal
from sqlalchemy import select
from app.database import async_session
from app.models import User, Wallet, SportEvent, SpreadBet
from app.models.spread_bet import TeamSide, SpreadBetStatus
from app.utils.security import get_password_hash
# Fake names for generating users
FIRST_NAMES = [
"James", "Emma", "Liam", "Olivia", "Noah", "Ava", "Oliver", "Sophia",
"Elijah", "Isabella", "Lucas", "Mia", "Mason", "Charlotte", "Ethan",
"Amelia", "Logan", "Harper", "Aiden", "Evelyn", "Jackson", "Luna",
"Sebastian", "Camila", "Henry", "Gianna", "Alexander", "Abigail"
]
LAST_NAMES = [
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller",
"Davis", "Rodriguez", "Martinez", "Wilson", "Anderson", "Taylor",
"Thomas", "Moore", "Jackson", "Martin", "Lee", "Thompson", "White"
]
async def create_users_if_needed(db, count: int) -> list[User]:
"""Create additional test users if needed"""
# Get existing users
result = await db.execute(select(User).where(User.is_admin == False))
existing_users = list(result.scalars().all())
if len(existing_users) >= count:
return existing_users[:count]
# Create more users
users_needed = count - len(existing_users)
print(f"Creating {users_needed} additional test users...")
new_users = []
for i in range(users_needed):
first = random.choice(FIRST_NAMES)
last = random.choice(LAST_NAMES)
username = f"{first.lower()}{last.lower()}{random.randint(1, 999)}"
email = f"{username}@example.com"
# Check if user exists
result = await db.execute(select(User).where(User.email == email))
if result.scalar_one_or_none():
continue
user = User(
email=email,
username=username,
password_hash=get_password_hash("password123"),
display_name=f"{first} {last}"
)
db.add(user)
await db.flush()
# Create wallet with random balance
wallet = Wallet(
user_id=user.id,
balance=Decimal(str(random.randint(500, 5000))),
escrow=Decimal("0.00")
)
db.add(wallet)
new_users.append(user)
await db.commit()
# Re-fetch all users
result = await db.execute(select(User).where(User.is_admin == False))
return list(result.scalars().all())
async def add_bets():
"""Add 100 spread bets to Wake Forest vs MS State game"""
async with async_session() as db:
# Find the Wake Forest vs MS State event
result = await db.execute(
select(SportEvent).where(
SportEvent.home_team == "Wake Forest",
SportEvent.away_team == "MS State"
)
)
event = result.scalar_one_or_none()
if not event:
print("Error: Wake Forest vs MS State event not found!")
print("Please run init_spread_betting.py first")
return
print(f"Found event: {event.home_team} vs {event.away_team} (ID: {event.id})")
print(f"Official spread: {event.official_spread}")
print(f"Spread range: {event.min_spread} to {event.max_spread}")
# Create/get test users (need at least 20 for variety)
users = await create_users_if_needed(db, 20)
print(f"Using {len(users)} users to create bets")
# Generate 100 bets
print("\nCreating 100 spread bets...")
# Spread range from -10 to +10 with 0.5 increments
spreads = [x / 2 for x in range(-20, 21)] # -10.0 to +10.0
# Stake amounts - realistic distribution
stakes = [25, 50, 75, 100, 150, 200, 250, 300, 400, 500, 750, 1000]
bets_created = 0
for i in range(100):
creator = random.choice(users)
# Generate spread - cluster around the official spread with some outliers
if random.random() < 0.7:
# 70% of bets cluster around official spread (+/- 3 points)
spread = event.official_spread + random.choice([-3, -2.5, -2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2, 2.5, 3])
else:
# 30% are spread across the full range
spread = random.choice(spreads)
# Clamp to valid range
spread = max(event.min_spread, min(event.max_spread, spread))
# Random team side
team = random.choice([TeamSide.HOME, TeamSide.AWAY])
# Random stake - weighted toward smaller amounts
if random.random() < 0.6:
stake = random.choice(stakes[:6]) # 60% small bets
elif random.random() < 0.85:
stake = random.choice(stakes[6:10]) # 25% medium bets
else:
stake = random.choice(stakes[10:]) # 15% large bets
bet = SpreadBet(
event_id=event.id,
spread=spread,
team=team,
creator_id=creator.id,
stake_amount=Decimal(str(stake)),
house_commission_percent=Decimal("10.00"),
status=SpreadBetStatus.OPEN,
created_at=datetime.utcnow()
)
db.add(bet)
bets_created += 1
if (i + 1) % 20 == 0:
print(f" Created {i + 1} bets...")
await db.commit()
print(f"\n{'='*60}")
print(f"Successfully created {bets_created} spread bets!")
print(f"{'='*60}")
print(f"\nBet distribution:")
print(f" Event: {event.home_team} vs {event.away_team}")
print(f" Spreads: clustered around {event.official_spread}")
print(f" Stakes: $25 - $1000")
print(f" Teams: mixed HOME and AWAY")
async def main():
await add_bets()
if __name__ == "__main__":
asyncio.run(main())

BIN
binance-trade.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

View 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>
)
}

View File

@ -3,6 +3,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useAuthStore } from '@/store'
import { sportEventsApi } from '@/api/sport-events'
import { SpreadGrid } from '@/components/bets/SpreadGrid'
import { TradingPanel } from '@/components/bets/TradingPanel'
import { Button } from '@/components/common/Button'
import { Loading } from '@/components/common/Loading'
import { Header } from '@/components/layout/Header'
@ -37,7 +38,7 @@ export const EventDetail = () => {
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-gray-900">
<Header />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Link to="/">
@ -56,7 +57,7 @@ export const EventDetail = () => {
if (error || !event) {
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-gray-900">
<Header />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Link to="/">
@ -65,8 +66,8 @@ export const EventDetail = () => {
Back to Events
</Button>
</Link>
<div className="mt-8 text-center py-12 bg-white rounded-lg shadow">
<p className="text-gray-600">Event not found</p>
<div className="mt-8 text-center py-12 bg-gray-800 rounded-lg shadow">
<p className="text-gray-400">Event not found</p>
</div>
</div>
</div>
@ -74,16 +75,28 @@ export const EventDetail = () => {
}
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-gray-900">
<Header />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Link to="/">
<Button variant="secondary">
<Button variant="secondary" className="mb-6">
<ChevronLeft size={20} className="mr-2" />
Back to Events
</Button>
</Link>
<div className="mt-6">
{/* Trading Panel - Binance-style interface */}
<div className="mb-8">
<TradingPanel
event={event}
onBetCreated={handleBetCreated}
onBetTaken={handleBetTaken}
/>
</div>
{/* Spread Grid - Visual betting grid */}
<div className="bg-gray-800 rounded-xl p-6">
<h2 className="text-xl font-bold text-white mb-4">Spread Grid</h2>
<SpreadGrid
event={event}
onBetCreated={handleBetCreated}