Updated events page to display all user bets.

This commit is contained in:
2026-01-10 14:01:21 -06:00
parent accd4487b0
commit 3cf9e594e9
14 changed files with 283 additions and 27 deletions

View File

@ -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,

View File

@ -17,6 +17,11 @@ export const spreadBetsApi = {
return response.data
},
getMyBetHistory: async (): Promise<SpreadBetDetail[]> => {
const response = await apiClient.get<SpreadBetDetail[]>('/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

View File

@ -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, string> = {
[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<TabType>('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 (
<tr
key={bet.id}
className={`hover:bg-gray-50 transition-colors ${isCurrentEvent ? 'bg-blue-50' : ''}`}
>
<td className="px-4 py-3 text-sm">
<Link
to={`/events/${bet.event_id}`}
className="font-medium text-gray-900 hover:text-blue-600"
>
{bet.event_home_team} vs {bet.event_away_team}
</Link>
<div className="text-xs text-gray-500">{formatGameTime(bet.event_game_time)}</div>
</td>
<td className="px-4 py-3 text-sm">
<span className={`font-medium ${bet.team === 'home' ? 'text-green-600' : 'text-red-600'}`}>
{bet.team === 'home' ? bet.event_home_team : bet.event_away_team}
</span>
</td>
<td className="px-4 py-3 text-sm font-medium text-gray-900 text-center">
{bet.spread > 0 ? '+' : ''}{bet.spread}
</td>
<td className="px-4 py-3 text-sm font-medium text-gray-900 text-right">
${Number(bet.stake_amount).toLocaleString()}
</td>
<td className="px-4 py-3 text-sm text-center">
<div className="flex items-center justify-center gap-1">
{isCurrentEvent && (
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-blue-100 text-blue-700">
Current
</span>
)}
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${STATUS_STYLES[bet.status]}`}>
{bet.status}
</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-right">
{bet.status === SpreadBetStatus.COMPLETED ? (
<div className="flex items-center justify-end gap-1">
{isWinner ? (
<>
<span className="font-medium text-green-600">
+${Number(bet.payout_amount || 0).toLocaleString()}
</span>
<Trophy size={14} className="text-yellow-500" />
</>
) : isLoser ? (
<>
<span className="font-medium text-red-600">
-${Number(bet.stake_amount).toLocaleString()}
</span>
<XCircle size={14} className="text-red-500" />
</>
) : (
<span className="text-gray-400">-</span>
)}
</div>
) : (
<span className="text-gray-400">-</span>
)}
</td>
</tr>
)
}
return (
<div className="bg-white rounded-xl shadow-sm border p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-gray-900">My Bets</h2>
<Link
to="/my-bets"
className="text-sm text-primary hover:underline flex items-center gap-1"
>
View All <ExternalLink size={14} />
</Link>
</div>
{/* Tabs */}
<div className="flex gap-2 mb-4 border-b">
<button
onClick={() => setActiveTab('active')}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'active'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Active ({activeBets.length})
</button>
<button
onClick={() => setActiveTab('history')}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'history'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
History {historyBets.length > 0 && `(${historyBets.length})`}
</button>
</div>
{/* Content */}
{isLoading ? (
<div className="space-y-2">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-12 bg-gray-100 rounded animate-pulse" />
))}
</div>
) : bets.length === 0 ? (
<p className="text-gray-500 text-center py-8">
{activeTab === 'active'
? "You don't have any active bets."
: "No bet history yet."}
</p>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-4 py-3">Event</th>
<th className="px-4 py-3">Team</th>
<th className="px-4 py-3 text-center">Spread</th>
<th className="px-4 py-3 text-right">Stake</th>
<th className="px-4 py-3 text-center">Status</th>
<th className="px-4 py-3 text-right">Result</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{bets.map(renderBetRow)}
</tbody>
</table>
</div>
)}
</div>
)
}

View File

@ -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 = () => {
<div className="px-4 sm:px-6 lg:px-8 py-8">
<Link to="/">
<Button variant="secondary">
<ChevronLeft size={20} className="mr-2" />
Back to Events
</Button>
</Link>
@ -62,7 +63,7 @@ export const EventDetail = () => {
<div className="px-4 sm:px-6 lg:px-8 py-8">
<Link to="/">
<Button variant="secondary">
<ChevronLeft size={20} className="mr-2" />
Back to Events
</Button>
</Link>
@ -80,7 +81,7 @@ export const EventDetail = () => {
<div className="px-4 sm:px-6 lg:px-8 py-8">
<Link to="/">
<Button variant="secondary" className="mb-6">
<ChevronLeft size={20} className="mr-2" />
Back to Events
</Button>
</Link>
@ -94,6 +95,11 @@ export const EventDetail = () => {
/>
</div>
{/* My Other Bets - Show user's bets on other events */}
<div className="mb-8">
<MyOtherBets currentEventId={eventId} />
</div>
{/* Spread Grid - Visual betting grid */}
{/* <div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">Spread Grid</h2>

View File

@ -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"
/>
<Button type="submit" size="lg">
Sign Up <ArrowRight size={18} className="ml-2" />
Sign Up
</Button>
</form>
)}
@ -91,7 +91,7 @@ export const Home = () => {
<div className="flex gap-4">
<Link to={`/events/${events[0].id}`}>
<Button size="lg">
Start Betting <ArrowRight size={18} className="ml-2" />
Start Betting
</Button>
</Link>
</div>

View File

@ -107,7 +107,7 @@ export const HowItWorks = () => {
<div className="flex gap-4 justify-center">
<Link to="/register">
<Button size="lg" className="bg-white text-primary hover:bg-gray-100">
Create Account <ArrowRight size={18} className="ml-2" />
Create Account
</Button>
</Link>
<Link to="/">

View File

@ -25,7 +25,7 @@ export const Live = () => {
</p>
<Link to="/">
<Button size="lg">
View Upcoming Events <ArrowRight size={18} className="ml-2" />
View Upcoming Events
</Button>
</Link>
</div>

View File

@ -75,7 +75,7 @@ export const NewBets = () => {
<div className="mt-12 text-center">
<Link to="/">
<Button variant="secondary" size="lg">
View All Events <ArrowRight size={18} className="ml-2" />
View All Events
</Button>
</Link>
</div>

View File

@ -42,7 +42,7 @@ export const SportEvents = () => {
<Layout>
<div className="space-y-4">
<Button variant="secondary" onClick={handleBackToList}>
<ChevronLeft size={20} className="mr-2" />
Back to Events
</Button>
<Loading />
@ -57,7 +57,7 @@ export const SportEvents = () => {
{selectedEvent ? (
<>
<Button variant="secondary" onClick={handleBackToList}>
<ChevronLeft size={20} className="mr-2" />
Back to Events
</Button>
<SpreadGrid

View File

@ -84,7 +84,7 @@ export const Sports = () => {
<div className="mt-12 text-center">
<Link to="/">
<Button size="lg">
View All Events <ArrowRight size={18} className="ml-2" />
View All Events
</Button>
</Link>
</div>

View File

@ -23,7 +23,7 @@ export const Watchlist = () => {
<div className="flex gap-4 justify-center">
<Link to="/register">
<Button size="lg">
Create Account <ArrowRight size={18} className="ml-2" />
Create Account
</Button>
</Link>
<Link to="/">

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

View File

@ -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);
});