diff --git a/backend/app/routers/spread_bets.py b/backend/app/routers/spread_bets.py index 4b53c77..1a906c6 100644 --- a/backend/app/routers/spread_bets.py +++ b/backend/app/routers/spread_bets.py @@ -214,6 +214,57 @@ async def get_my_active_bets( ] +@router.get("/my-history", response_model=List[SpreadBetDetail]) +async def get_my_bet_history( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """Get user's completed, cancelled, and disputed bets (history).""" + result = await db.execute( + select(SpreadBet) + .options( + selectinload(SpreadBet.event), + selectinload(SpreadBet.creator), + selectinload(SpreadBet.taker) + ) + .where( + and_( + SpreadBet.status.in_([SpreadBetStatus.COMPLETED, SpreadBetStatus.CANCELLED, SpreadBetStatus.DISPUTED]), + (SpreadBet.creator_id == current_user.id) | (SpreadBet.taker_id == current_user.id) + ) + ) + .order_by(SpreadBet.created_at.desc()) + .limit(50) + ) + bets = result.scalars().all() + + return [ + SpreadBetDetail( + id=bet.id, + event_id=bet.event_id, + spread=bet.spread, + team=bet.team, + creator_id=bet.creator_id, + taker_id=bet.taker_id, + stake_amount=bet.stake_amount, + house_commission_percent=bet.house_commission_percent, + status=bet.status, + payout_amount=bet.payout_amount, + winner_id=bet.winner_id, + created_at=bet.created_at, + matched_at=bet.matched_at, + completed_at=bet.completed_at, + creator_username=bet.creator.username, + taker_username=bet.taker.username if bet.taker else None, + event_home_team=bet.event.home_team, + event_away_team=bet.event.away_team, + event_official_spread=bet.event.official_spread, + event_game_time=bet.event.game_time + ) + for bet in bets + ] + + @router.delete("/{bet_id}") async def cancel_spread_bet( bet_id: int, diff --git a/frontend/src/api/spread-bets.ts b/frontend/src/api/spread-bets.ts index e701cf7..8e80cc1 100644 --- a/frontend/src/api/spread-bets.ts +++ b/frontend/src/api/spread-bets.ts @@ -17,6 +17,11 @@ export const spreadBetsApi = { return response.data }, + getMyBetHistory: async (): Promise => { + const response = await apiClient.get('/api/v1/spread-bets/my-history') + return response.data + }, + cancelBet: async (betId: number): Promise<{ message: string }> => { const response = await apiClient.delete<{ message: string }>(`/api/v1/spread-bets/${betId}`) return response.data diff --git a/frontend/src/components/bets/MyOtherBets.tsx b/frontend/src/components/bets/MyOtherBets.tsx new file mode 100644 index 0000000..92fe9a0 --- /dev/null +++ b/frontend/src/components/bets/MyOtherBets.tsx @@ -0,0 +1,198 @@ +import { useState } from 'react' +import { Link } from 'react-router-dom' +import { useQuery } from '@tanstack/react-query' +import { useAuthStore } from '@/store' +import { spreadBetsApi } from '@/api/spread-bets' +import type { SpreadBetDetail } from '@/types/spread-bet' +import { SpreadBetStatus } from '@/types/spread-bet' +import { ExternalLink, Trophy, XCircle } from 'lucide-react' + +interface MyOtherBetsProps { + currentEventId?: number +} + +const STATUS_STYLES: Record = { + [SpreadBetStatus.OPEN]: 'bg-green-100 text-green-700', + [SpreadBetStatus.MATCHED]: 'bg-yellow-100 text-yellow-700', + [SpreadBetStatus.COMPLETED]: 'bg-blue-100 text-blue-700', + [SpreadBetStatus.CANCELLED]: 'bg-gray-100 text-gray-700', + [SpreadBetStatus.DISPUTED]: 'bg-red-100 text-red-700', +} + +function formatGameTime(gameTime: string): string { + const date = new Date(gameTime) + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + }) +} + +type TabType = 'active' | 'history' + +export const MyOtherBets = ({ currentEventId }: MyOtherBetsProps) => { + const { isAuthenticated, user } = useAuthStore() + const [activeTab, setActiveTab] = useState('active') + + const { data: activeBets = [], isLoading: isLoadingActive } = useQuery({ + queryKey: ['my-active-bets'], + queryFn: () => spreadBetsApi.getMyActiveBets(), + enabled: isAuthenticated, + }) + + const { data: historyBets = [], isLoading: isLoadingHistory } = useQuery({ + queryKey: ['my-bet-history'], + queryFn: () => spreadBetsApi.getMyBetHistory(), + enabled: isAuthenticated && activeTab === 'history', + }) + + if (!isAuthenticated) { + return null + } + + const isLoading = activeTab === 'active' ? isLoadingActive : isLoadingHistory + const bets = activeTab === 'active' ? activeBets : historyBets + + const renderBetRow = (bet: SpreadBetDetail) => { + const isCurrentEvent = bet.event_id === currentEventId + const isWinner = bet.winner_id === user?.id + const isLoser = bet.status === SpreadBetStatus.COMPLETED && bet.winner_id && bet.winner_id !== user?.id + + return ( + + + + {bet.event_home_team} vs {bet.event_away_team} + +
{formatGameTime(bet.event_game_time)}
+ + + + {bet.team === 'home' ? bet.event_home_team : bet.event_away_team} + + + + {bet.spread > 0 ? '+' : ''}{bet.spread} + + + ${Number(bet.stake_amount).toLocaleString()} + + +
+ {isCurrentEvent && ( + + Current + + )} + + {bet.status} + +
+ + + {bet.status === SpreadBetStatus.COMPLETED ? ( +
+ {isWinner ? ( + <> + + +${Number(bet.payout_amount || 0).toLocaleString()} + + + + ) : isLoser ? ( + <> + + -${Number(bet.stake_amount).toLocaleString()} + + + + ) : ( + - + )} +
+ ) : ( + - + )} + + + ) + } + + return ( +
+
+

My Bets

+ + View All + +
+ + {/* Tabs */} +
+ + +
+ + {/* Content */} + {isLoading ? ( +
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+ ) : bets.length === 0 ? ( +

+ {activeTab === 'active' + ? "You don't have any active bets." + : "No bet history yet."} +

+ ) : ( +
+ + + + + + + + + + + + + {bets.map(renderBetRow)} + +
EventTeamSpreadStakeStatusResult
+
+ )} +
+ ) +} diff --git a/frontend/src/pages/EventDetail.tsx b/frontend/src/pages/EventDetail.tsx index 0741b05..3b44fbf 100644 --- a/frontend/src/pages/EventDetail.tsx +++ b/frontend/src/pages/EventDetail.tsx @@ -4,6 +4,7 @@ import { useAuthStore } from '@/store' import { sportEventsApi } from '@/api/sport-events' import { SpreadGrid } from '@/components/bets/SpreadGrid' import { TradingPanel } from '@/components/bets/TradingPanel' +import { MyOtherBets } from '@/components/bets/MyOtherBets' import { Button } from '@/components/common/Button' import { Loading } from '@/components/common/Loading' import { Header } from '@/components/layout/Header' @@ -43,7 +44,7 @@ export const EventDetail = () => {
@@ -62,7 +63,7 @@ export const EventDetail = () => {
@@ -80,7 +81,7 @@ export const EventDetail = () => {
@@ -94,6 +95,11 @@ export const EventDetail = () => { />
+ {/* My Other Bets - Show user's bets on other events */} +
+ +
+ {/* Spread Grid - Visual betting grid */} {/*

Spread Grid

diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 3f2fcaf..0aa2b5b 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -82,7 +82,7 @@ export const Home = () => { className="flex-1 px-4 py-3 rounded-lg text-gray-900 focus:outline-none focus:ring-2 focus:ring-primary" /> )} @@ -91,7 +91,7 @@ export const Home = () => {
diff --git a/frontend/src/pages/HowItWorks.tsx b/frontend/src/pages/HowItWorks.tsx index 98103c3..de7259d 100644 --- a/frontend/src/pages/HowItWorks.tsx +++ b/frontend/src/pages/HowItWorks.tsx @@ -107,7 +107,7 @@ export const HowItWorks = () => {
diff --git a/frontend/src/pages/Live.tsx b/frontend/src/pages/Live.tsx index 932895f..a9ac0db 100644 --- a/frontend/src/pages/Live.tsx +++ b/frontend/src/pages/Live.tsx @@ -25,7 +25,7 @@ export const Live = () => {

diff --git a/frontend/src/pages/NewBets.tsx b/frontend/src/pages/NewBets.tsx index 16672f4..284f719 100644 --- a/frontend/src/pages/NewBets.tsx +++ b/frontend/src/pages/NewBets.tsx @@ -75,7 +75,7 @@ export const NewBets = () => {
diff --git a/frontend/src/pages/SportEvents.tsx b/frontend/src/pages/SportEvents.tsx index 8e05a5c..f6b2ac8 100644 --- a/frontend/src/pages/SportEvents.tsx +++ b/frontend/src/pages/SportEvents.tsx @@ -42,7 +42,7 @@ export const SportEvents = () => {
@@ -57,7 +57,7 @@ export const SportEvents = () => { {selectedEvent ? ( <> {
diff --git a/frontend/src/pages/Watchlist.tsx b/frontend/src/pages/Watchlist.tsx index 1ba09f4..26d1300 100644 --- a/frontend/src/pages/Watchlist.tsx +++ b/frontend/src/pages/Watchlist.tsx @@ -23,7 +23,7 @@ export const Watchlist = () => {
diff --git a/frontend/test-results/event-with-other-bets.png b/frontend/test-results/event-with-other-bets.png new file mode 100644 index 0000000..4ef3dfd Binary files /dev/null and b/frontend/test-results/event-with-other-bets.png differ diff --git a/frontend/test-results/rewards-page.png b/frontend/test-results/rewards-page.png deleted file mode 100644 index 9e32277..0000000 Binary files a/frontend/test-results/rewards-page.png and /dev/null differ diff --git a/frontend/tests/debug-trading-panel.spec.ts b/frontend/tests/debug-trading-panel.spec.ts index e5be3a6..3ff1675 100644 --- a/frontend/tests/debug-trading-panel.spec.ts +++ b/frontend/tests/debug-trading-panel.spec.ts @@ -1,31 +1,27 @@ import { test } from '@playwright/test'; -test('debug rewards page crash', async ({ page }) => { +test('check event page with my other bets', async ({ page }) => { const errors: string[] = []; - page.on('console', msg => { - if (msg.type() === 'error') { - console.log('[error]', msg.text()); - errors.push(msg.text()); - } - }); - page.on('pageerror', error => { console.log('Page error:', error.message); errors.push(error.message); }); - // Navigate to rewards page - console.log('Navigating to /rewards...'); - await page.goto('/rewards', { waitUntil: 'domcontentloaded' }); + // Navigate to event detail + await page.goto('/events/1', { waitUntil: 'networkidle' }); await page.waitForTimeout(3000); // Take screenshot - await page.screenshot({ path: 'test-results/rewards-page.png', fullPage: true }); + await page.screenshot({ path: 'test-results/event-with-other-bets.png', fullPage: true }); - // Check for content + // Check for the "My Other Bets" section + const myOtherBetsSection = await page.locator('text=My Other Bets').count(); + console.log('My Other Bets section found:', myOtherBetsSection > 0); + + // Check body text const bodyText = await page.locator('body').innerText(); - console.log('Body text (first 500 chars):', bodyText.substring(0, 500)); + console.log('Has "My Other Bets":', bodyText.includes('My Other Bets')); - console.log('All errors:', errors); + console.log('Page errors:', errors); });