diff --git a/frontend/src/components/bets/TradingPanel.tsx b/frontend/src/components/bets/TradingPanel.tsx
index 9f80eb3..ac946c4 100644
--- a/frontend/src/components/bets/TradingPanel.tsx
+++ b/frontend/src/components/bets/TradingPanel.tsx
@@ -6,17 +6,13 @@ 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,
+ TrendingUp,
+ TrendingDown,
} from 'lucide-react'
import toast from 'react-hot-toast'
@@ -45,107 +41,6 @@ function formatTimeUntil(gameTime: string): { text: string; urgent: boolean } {
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 (
-
- )
-}
-
-// 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 (
-
-
- {isHome ? homeTeam : awayTeam} {bet.spread > 0 ? '+' : ''}{bet.spread}
-
- ${bet.stake.toFixed(0)}
-
- {bet.status}
-
-
- )
-}
-
export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelProps) => {
const { isAuthenticated } = useAuthStore()
const queryClient = useQueryClient()
@@ -153,7 +48,6 @@ export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelPr
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(() => {
@@ -193,32 +87,69 @@ export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelPr
.sort((a, b) => b - a) // High to low
}, [event.spread_grid])
- // Get max volume for visualization
- const maxVolume = useMemo(() => {
+ // Split spreads at official line for order book
+ const { aboveLine, belowLine, maxVolume } = useMemo(() => {
+ const above: number[] = []
+ const below: number[] = []
let max = 0
- Object.values(event.spread_grid).forEach(bets => {
+
+ sortedSpreads.forEach(spread => {
+ const bets = event.spread_grid[spread.toString()] || []
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) })
+ if (spread > event.official_spread) above.push(spread)
+ else if (spread < event.official_spread) below.push(spread)
+ })
+
+ // Add official line to both for display
+ return {
+ aboveLine: above.sort((a, b) => a - b), // Low to high (closest to line at bottom)
+ belowLine: below.sort((a, b) => b - a), // High to low (closest to line at top)
+ maxVolume: max
+ }
+ }, [sortedSpreads, event.spread_grid, event.official_spread])
+
+ // Get all bets for chart and recent activity (including matched)
+ const allBetsWithSpread = useMemo(() => {
+ const bets: (SpreadGridBet & { spread: number })[] = []
+ Object.entries(event.spread_grid).forEach(([spread, spreadBets]) => {
+ spreadBets.forEach(bet => {
+ bets.push({ ...bet, spread: Number(spread) })
})
})
- return allBets.slice(0, 10)
+ return bets
}, [event.spread_grid])
- // Get available bets to take at selected spread
+ // Recent activity - all bets sorted by recency (we don't have timestamps, so just show all)
+ const recentActivity = useMemo(() => {
+ return allBetsWithSpread.slice(0, 15) // Show last 15
+ }, [allBetsWithSpread])
+
+ // Chart data - volume per spread
+ const chartData = useMemo(() => {
+ const data: { spread: number; homeVolume: number; awayVolume: number; total: number }[] = []
+
+ // Get all possible spreads in range
+ for (let s = event.min_spread; s <= event.max_spread; s += 0.5) {
+ const bets = event.spread_grid[s.toString()] || []
+ const homeVol = bets.filter(b => b.team === 'home').reduce((sum, b) => sum + b.stake, 0)
+ const awayVol = bets.filter(b => b.team === 'away').reduce((sum, b) => sum + b.stake, 0)
+ data.push({ spread: s, homeVolume: homeVol, awayVolume: awayVol, total: homeVol + awayVol })
+ }
+
+ return data
+ }, [event.spread_grid, event.min_spread, event.max_spread])
+
+ const chartMaxVolume = useMemo(() => {
+ return Math.max(...chartData.map(d => d.total), 1)
+ }, [chartData])
+
+ // Available bets to take
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])
+ return bets.filter(b => b.status === 'open' && b.can_take)
+ }, [event.spread_grid, selectedSpread])
// Create bet mutation
const createBetMutation = useMutation({
@@ -240,7 +171,6 @@ export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelPr
mutationFn: (betId: number) => spreadBetsApi.takeBet(betId),
onSuccess: () => {
toast.success('Bet taken successfully!')
- setStakeAmount('')
queryClient.invalidateQueries({ queryKey: ['sport-event'] })
onBetTaken?.()
},
@@ -249,7 +179,7 @@ export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelPr
},
})
- const handleSubmit = () => {
+ const handleCreateBet = () => {
if (!isAuthenticated) {
toast.error('Please log in to place a bet')
return
@@ -261,78 +191,124 @@ export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelPr
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,
- })
- }
+ createBetMutation.mutate({
+ event_id: event.id,
+ spread: selectedSpread,
+ team: selectedSide === 'home' ? TeamSide.HOME : TeamSide.AWAY,
+ stake_amount: amount,
+ })
}
- const handleSpreadSelect = (spread: number) => {
- setSelectedSpread(spread)
+ const handleTakeBet = (betId: number) => {
+ if (!isAuthenticated) {
+ toast.error('Please log in to take a bet')
+ return
+ }
+ takeBetMutation.mutate(betId)
}
// Quick stake buttons
- const quickStakes = [25, 50, 100, 250, 500, 1000]
+ const quickStakes = [25, 50, 100, 250, 500]
+
+ // Order book row renderer
+ const renderOrderRow = (spread: number) => {
+ const bets = event.spread_grid[spread.toString()] || []
+ const homeBets = bets.filter(b => b.team === 'home')
+ const awayBets = bets.filter(b => b.team === 'away')
+ const homeVolume = homeBets.reduce((sum, b) => sum + b.stake, 0)
+ const awayVolume = awayBets.reduce((sum, b) => sum + b.stake, 0)
+ const homeOpen = homeBets.filter(b => b.status === 'open' && b.can_take).length
+ const awayOpen = awayBets.filter(b => b.status === 'open' && b.can_take).length
+
+ return (
+