Updated events page to display all user bets.
This commit is contained in:
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
198
frontend/src/components/bets/MyOtherBets.tsx
Normal file
198
frontend/src/components/bets/MyOtherBets.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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="/">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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="/">
|
||||
|
||||
BIN
frontend/test-results/event-with-other-bets.png
Normal file
BIN
frontend/test-results/event-with-other-bets.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 359 KiB |
@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user