diff --git a/frontend/src/components/bets/TradingPanel.tsx b/frontend/src/components/bets/TradingPanel.tsx index ac946c4..48a1fb9 100644 --- a/frontend/src/components/bets/TradingPanel.tsx +++ b/frontend/src/components/bets/TradingPanel.tsx @@ -13,6 +13,8 @@ import { Target, TrendingUp, TrendingDown, + BarChart3, + Grid3X3, } from 'lucide-react' import toast from 'react-hot-toast' @@ -48,6 +50,7 @@ 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 [activeTab, setActiveTab] = useState<'chart' | 'grid'>('chart') // Update countdown every second useEffect(() => { @@ -80,35 +83,32 @@ export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelPr 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]) - - // Split spreads at official line for order book - const { aboveLine, belowLine, maxVolume } = useMemo(() => { - const above: number[] = [] - const below: number[] = [] + // Calculate max volume across all spreads for order book bars + const maxVolume = useMemo(() => { let max = 0 - - sortedSpreads.forEach(spread => { - const bets = event.spread_grid[spread.toString()] || [] + Object.values(event.spread_grid).forEach(bets => { const vol = bets.reduce((sum, b) => sum + b.stake, 0) if (vol > max) max = vol - - if (spread > event.official_spread) above.push(spread) - else if (spread < event.official_spread) below.push(spread) }) + return max + }, [event.spread_grid]) - // 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 + // Generate ALL spreads from min to max, split at official line for order book + const { aboveLine, belowLine } = useMemo(() => { + const above: number[] = [] + const below: number[] = [] + + // Generate all possible spreads in range + for (let s = event.min_spread; s <= event.max_spread; s += 0.5) { + if (s > event.official_spread) above.push(s) + else if (s < event.official_spread) below.push(s) } - }, [sortedSpreads, event.spread_grid, event.official_spread]) + + return { + aboveLine: above.sort((a, b) => b - a), // High to low (highest at top) + belowLine: below.sort((a, b) => b - a), // High to low (closest to line at top) + } + }, [event.min_spread, event.max_spread, event.official_spread]) // Get all bets for chart and recent activity (including matched) const allBetsWithSpread = useMemo(() => { @@ -121,9 +121,9 @@ export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelPr return bets }, [event.spread_grid]) - // Recent activity - all bets sorted by recency (we don't have timestamps, so just show all) + // Recent activity - all bets const recentActivity = useMemo(() => { - return allBetsWithSpread.slice(0, 15) // Show last 15 + return allBetsWithSpread }, [allBetsWithSpread]) // Chart data - volume per spread @@ -265,6 +265,50 @@ export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelPr ) } + // Grid view - shows all spreads in a grid format + const renderGridView = () => { + const spreads: number[] = [] + for (let s = event.min_spread; s <= event.max_spread; s += 0.5) { + spreads.push(s) + } + + return ( +
+ {spreads.map(spread => { + 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 totalBets = bets.length + const isOfficial = spread === event.official_spread + + return ( + + ) + })} +
+ ) + } + return (
{/* Header - Event Info */} @@ -352,17 +396,19 @@ export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelPr
- {/* Main Content */} -
+ {/* Main Content - fixed height container */} +
{/* Left - Order Book */} -
-

- - Order Book -

+
+
+

+ + Order Book +

+
{/* Header */} -
+
{event.away_team.slice(0, 4)} Vol @@ -372,122 +418,187 @@ export const TradingPanel = ({ event, onBetCreated, onBetTaken }: TradingPanelPr {event.home_team.slice(0, 4)}
- {/* Above official line (away favored) */} -
- {aboveLine.map(spread => renderOrderRow(spread))} + {/* Above official line - fills available space */} +
+ {aboveLine.length > 0 ? ( + aboveLine.map(spread => renderOrderRow(spread)) + ) : ( +
+ No spreads above line +
+ )}
{/* Official Line */} -
+
{renderOrderRow(event.official_spread)}
- {/* Below official line (home favored) */} -
- {belowLine.map(spread => renderOrderRow(spread))} + {/* Below official line - fills available space */} +
+ {belowLine.length > 0 ? ( + belowLine.map(spread => renderOrderRow(spread)) + ) : ( +
+ No spreads below line +
+ )}
- {/* Center - Volume Chart */} -
-

Market Depth

- - {/* Chart */} -
- {/* Official line indicator */} -
+ {/* Tabs */} +
+
+ + Chart + + +
- {chartData.map((d) => ( -
setSelectedSpread(d.spread)} - > - {/* Away volume (red, on top) */} -
0 ? '2px' : '0' }} - /> - {/* Home volume (green, on bottom) */} -
0 ? '2px' : '0' }} - /> + {/* Tab Content */} +
+ {activeTab === 'chart' ? ( +
+ {/* Chart Section */} +
+

Market Depth

- {/* Tooltip on hover */} -
- {d.spread > 0 ? '+' : ''}{d.spread}: ${d.total.toLocaleString()} + {/* Chart container */} +
+ {/* Official line indicator */} +
+
+ {event.official_spread > 0 ? '+' : ''}{event.official_spread} +
+
+ + {chartData.map((d) => { + // Calculate heights as percentage of max, ensuring max bar fills 100% + const totalHeightPercent = (d.total / chartMaxVolume) * 100 + const homeHeightPercent = d.total > 0 ? (d.homeVolume / d.total) * totalHeightPercent : 0 + const awayHeightPercent = d.total > 0 ? (d.awayVolume / d.total) * totalHeightPercent : 0 + + return ( +
setSelectedSpread(d.spread)} + > + {/* Away volume (red, on top) */} +
0 ? '2px' : '0' }} + /> + {/* Home volume (green, on bottom) */} +
0 ? '2px' : '0' }} + /> + + {/* Tooltip on hover */} +
+ {d.spread > 0 ? '+' : ''}{d.spread}: ${d.total.toLocaleString()} +
+
+ ) + })} +
+ + {/* X-axis labels */} +
+ {event.min_spread} + Spread + +{event.max_spread} +
+ + {/* Legend */} +
+
+
+ {event.home_team} +
+
+
+ {event.away_team} +
+
+
+ Official Line +
+
+
+ + {/* Recent Activity Section */} +
+

+ Recent Activity ({recentActivity.length} bets) +

+
+ {recentActivity.length > 0 ? ( + recentActivity.map((bet, i) => ( +
+ + {bet.team === 'home' ? event.home_team : event.away_team} {bet.spread > 0 ? '+' : ''}{bet.spread} + + ${bet.stake.toFixed(0)} + + {bet.status} + + {bet.creator_username} +
+ )) + ) : ( +
+ No bets yet +
+ )} +
- ))} -
- - {/* X-axis labels */} -
- {event.min_spread} - Spread - +{event.max_spread} -
- - {/* Legend */} -
-
-
- {event.home_team} Volume -
-
-
- {event.away_team} Volume -
-
-
- Official Line -
-
- - {/* Recent Activity */} -
-

Recent Activity

-
- {recentActivity.length > 0 ? ( - recentActivity.map((bet, i) => ( -
- - {bet.team === 'home' ? event.home_team : event.away_team} {bet.spread > 0 ? '+' : ''}{bet.spread} - - ${bet.stake.toFixed(0)} - - {bet.status} - - {bet.creator_username} -
- )) - ) : ( -

No bets yet

- )} -
+ ) : ( + /* Grid View */ +
+

All Spreads

+ {renderGridView()} +
+ )}
{/* Right - Quick Trade Panel */} -
+

Place Bet diff --git a/frontend/test-results/event-detail.png b/frontend/test-results/event-detail.png new file mode 100644 index 0000000..28d01b7 Binary files /dev/null and b/frontend/test-results/event-detail.png differ diff --git a/frontend/tests/debug-trading-panel.spec.ts b/frontend/tests/debug-trading-panel.spec.ts new file mode 100644 index 0000000..3983c3a --- /dev/null +++ b/frontend/tests/debug-trading-panel.spec.ts @@ -0,0 +1,46 @@ +import { test } from '@playwright/test'; + +test('capture trading panel screenshot', async ({ page }) => { + // Navigate to event detail + await page.goto('/events/1', { waitUntil: 'networkidle' }); + await page.waitForTimeout(3000); + + // Take screenshot + await page.screenshot({ path: 'test-results/event-detail.png', fullPage: true }); + + // Check the spread values being used + const spreadInfo = await page.evaluate(() => { + // Look for the spread grid to understand the range + const spreadGridCells = document.querySelectorAll('.grid button'); + const spreads: string[] = []; + spreadGridCells.forEach(cell => { + const text = cell.textContent; + if (text && (text.includes('+') || text.includes('-') || text.match(/^\\d/))) { + spreads.push(text.substring(0, 10)); + } + }); + + // Check order book sections + const orderBookContainer = document.querySelector('.col-span-3'); + const aboveSection = orderBookContainer?.querySelectorAll('.flex-1.overflow-y-auto')[0]; + const belowSection = orderBookContainer?.querySelectorAll('.flex-1.overflow-y-auto')[1]; + + // Count rows in each section + const aboveRows = aboveSection?.querySelectorAll('button').length || 0; + const belowRows = belowSection?.querySelectorAll('button').length || 0; + + // Get the official spread from the header + const officialSpreadElement = document.querySelector('.text-3xl.font-bold'); + const officialSpread = officialSpreadElement?.textContent; + + return { + officialSpread, + aboveRowCount: aboveRows, + belowRowCount: belowRows, + aboveSectionHTML: aboveSection?.innerHTML?.substring(0, 500) || 'not found', + belowSectionHTML: belowSection?.innerHTML?.substring(0, 500) || 'not found', + }; + }); + + console.log('Spread info:', JSON.stringify(spreadInfo, null, 2)); +});