Adjusted links.
@ -18,6 +18,7 @@ import { Live } from './pages/Live'
|
||||
import { NewBets } from './pages/NewBets'
|
||||
import { Watchlist } from './pages/Watchlist'
|
||||
import { HowItWorks } from './pages/HowItWorks'
|
||||
import { EventDetail } from './pages/EventDetail'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@ -60,6 +61,7 @@ function App() {
|
||||
<Route path="/new-bets" element={<NewBets />} />
|
||||
<Route path="/watchlist" element={<Watchlist />} />
|
||||
<Route path="/how-it-works" element={<HowItWorks />} />
|
||||
<Route path="/events/:id" element={<EventDetail />} />
|
||||
|
||||
<Route
|
||||
path="/profile"
|
||||
|
||||
96
frontend/src/pages/EventDetail.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { sportEventsApi } from '@/api/sport-events'
|
||||
import { SpreadGrid } from '@/components/bets/SpreadGrid'
|
||||
import { Button } from '@/components/common/Button'
|
||||
import { Loading } from '@/components/common/Loading'
|
||||
import { Header } from '@/components/layout/Header'
|
||||
import { ChevronLeft } from 'lucide-react'
|
||||
|
||||
export const EventDetail = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const eventId = parseInt(id || '0', 10)
|
||||
const { isAuthenticated } = useAuthStore()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data: event, isLoading, error } = useQuery({
|
||||
queryKey: ['sport-event', eventId, isAuthenticated],
|
||||
queryFn: () =>
|
||||
isAuthenticated
|
||||
? sportEventsApi.getEventWithGrid(eventId)
|
||||
: sportEventsApi.getPublicEventWithGrid(eventId),
|
||||
enabled: eventId > 0,
|
||||
})
|
||||
|
||||
const handleBetCreated = () => {
|
||||
// Refetch event data to show new bet
|
||||
queryClient.invalidateQueries({ queryKey: ['sport-event', eventId] })
|
||||
queryClient.invalidateQueries({ queryKey: ['public-sport-events'] })
|
||||
}
|
||||
|
||||
const handleBetTaken = () => {
|
||||
// Refetch event data to update bet status
|
||||
queryClient.invalidateQueries({ queryKey: ['sport-event', eventId] })
|
||||
queryClient.invalidateQueries({ queryKey: ['public-sport-events'] })
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="max-w-7xl mx-auto 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>
|
||||
<div className="mt-8">
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !event) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="max-w-7xl mx-auto 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>
|
||||
<div className="mt-8 text-center py-12 bg-white rounded-lg shadow">
|
||||
<p className="text-gray-600">Event not found</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="max-w-7xl mx-auto 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>
|
||||
<div className="mt-6">
|
||||
<SpreadGrid
|
||||
event={event}
|
||||
onBetCreated={handleBetCreated}
|
||||
onBetTaken={handleBetTaken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -3,16 +3,14 @@ import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { sportEventsApi } from '@/api/sport-events'
|
||||
import { SpreadGrid } from '@/components/bets/SpreadGrid'
|
||||
import { Button } from '@/components/common/Button'
|
||||
import { Loading } from '@/components/common/Loading'
|
||||
import { Header } from '@/components/layout/Header'
|
||||
import { ChevronLeft, TrendingUp, Clock, ArrowRight } from 'lucide-react'
|
||||
import { TrendingUp, Clock, ArrowRight } from 'lucide-react'
|
||||
|
||||
export const Home = () => {
|
||||
const navigate = useNavigate()
|
||||
const { isAuthenticated } = useAuthStore()
|
||||
const [selectedEventId, setSelectedEventId] = useState<number | null>(null)
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
// Use public API for events (works for both authenticated and non-authenticated)
|
||||
@ -21,24 +19,6 @@ export const Home = () => {
|
||||
queryFn: () => sportEventsApi.getPublicEvents(),
|
||||
})
|
||||
|
||||
// Use authenticated API for event details if logged in, otherwise public
|
||||
const { data: selectedEvent, isLoading: isLoadingEvent } = useQuery({
|
||||
queryKey: ['sport-event', selectedEventId, isAuthenticated],
|
||||
queryFn: () =>
|
||||
isAuthenticated
|
||||
? sportEventsApi.getEventWithGrid(selectedEventId!)
|
||||
: sportEventsApi.getPublicEventWithGrid(selectedEventId!),
|
||||
enabled: selectedEventId !== null,
|
||||
})
|
||||
|
||||
const handleEventClick = (eventId: number) => {
|
||||
setSelectedEventId(eventId)
|
||||
}
|
||||
|
||||
const handleBackToList = () => {
|
||||
setSelectedEventId(null)
|
||||
}
|
||||
|
||||
const handleSignUp = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
navigate(`/register?email=${encodeURIComponent(email)}`)
|
||||
@ -56,47 +36,6 @@ export const Home = () => {
|
||||
)
|
||||
}
|
||||
|
||||
// Selected event view (spread grid)
|
||||
if (selectedEventId) {
|
||||
if (isLoadingEvent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Button variant="secondary" onClick={handleBackToList}>
|
||||
<ChevronLeft size={20} className="mr-2" />
|
||||
Back to Events
|
||||
</Button>
|
||||
<div className="mt-8">
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedEvent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Button variant="secondary" onClick={handleBackToList}>
|
||||
<ChevronLeft size={20} className="mr-2" />
|
||||
Back to Events
|
||||
</Button>
|
||||
<div className="mt-6">
|
||||
<SpreadGrid
|
||||
event={selectedEvent}
|
||||
onBetCreated={() => {}}
|
||||
onBetTaken={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total open bets across all events
|
||||
const totalOpenBets = events?.length || 0
|
||||
|
||||
@ -131,11 +70,13 @@ export const Home = () => {
|
||||
</form>
|
||||
)}
|
||||
|
||||
{isAuthenticated && (
|
||||
{isAuthenticated && events?.[0] && (
|
||||
<div className="flex gap-4">
|
||||
<Button size="lg" onClick={() => events?.[0] && handleEventClick(events[0].id)}>
|
||||
Start Betting <ArrowRight size={18} className="ml-2" />
|
||||
</Button>
|
||||
<Link to={`/events/${events[0].id}`}>
|
||||
<Button size="lg">
|
||||
Start Betting <ArrowRight size={18} className="ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -262,9 +203,9 @@ export const Home = () => {
|
||||
const isUrgent = hoursUntil >= 0 && hoursUntil < 24
|
||||
|
||||
return (
|
||||
<button
|
||||
<Link
|
||||
key={event.id}
|
||||
onClick={() => handleEventClick(event.id)}
|
||||
to={`/events/${event.id}`}
|
||||
className="grid grid-cols-12 gap-4 px-6 py-5 w-full text-left hover:bg-gray-50 transition-colors items-center"
|
||||
>
|
||||
<div className="col-span-4">
|
||||
@ -305,7 +246,7 @@ export const Home = () => {
|
||||
Spreads: {event.min_spread} to {event.max_spread}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@ -12,7 +12,7 @@ export const NewBets = () => {
|
||||
queryFn: () => sportEventsApi.getPublicEvents(),
|
||||
})
|
||||
|
||||
// Simulate recent bets from events
|
||||
// Show events with simulated bet activity
|
||||
const recentBets = events?.slice(0, 10).map((event, index) => ({
|
||||
id: index,
|
||||
event,
|
||||
@ -39,7 +39,7 @@ export const NewBets = () => {
|
||||
{recentBets?.map((bet) => (
|
||||
<Link
|
||||
key={bet.id}
|
||||
to="/"
|
||||
to={`/events/${bet.event.id}`}
|
||||
className="block bg-white rounded-xl shadow-sm hover:shadow-md transition-shadow p-6 border border-gray-100"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
@ -64,7 +64,7 @@ export const NewBets = () => {
|
||||
<Clock size={14} />
|
||||
{bet.timeAgo}
|
||||
</span>
|
||||
<Button size="sm">Take Bet</Button>
|
||||
<Button size="sm">View Event</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@ -1,18 +1,32 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Header } from '@/components/layout/Header'
|
||||
import { Button } from '@/components/common/Button'
|
||||
import { Loading } from '@/components/common/Loading'
|
||||
import { sportEventsApi } from '@/api/sport-events'
|
||||
import { Trophy, ArrowRight } from 'lucide-react'
|
||||
|
||||
export const Sports = () => {
|
||||
const { data: events, isLoading } = useQuery({
|
||||
queryKey: ['public-sport-events'],
|
||||
queryFn: () => sportEventsApi.getPublicEvents(),
|
||||
})
|
||||
|
||||
const sports = [
|
||||
{ name: 'Football', icon: '🏈', leagues: ['NFL', 'NCAA Football'] },
|
||||
{ name: 'Basketball', icon: '🏀', leagues: ['NBA', 'NCAA Basketball'] },
|
||||
{ name: 'Hockey', icon: '🏒', leagues: ['NHL'] },
|
||||
{ name: 'Soccer', icon: '⚽', leagues: ['Premier League', 'La Liga', 'Bundesliga', 'MLS'] },
|
||||
{ name: 'Baseball', icon: '⚾', leagues: ['MLB'] },
|
||||
{ name: 'MMA', icon: '🥊', leagues: ['UFC', 'Bellator'] },
|
||||
{ name: 'Football', icon: '🏈', filter: 'football' },
|
||||
{ name: 'Basketball', icon: '🏀', filter: 'basketball' },
|
||||
{ name: 'Hockey', icon: '🏒', filter: 'hockey' },
|
||||
{ name: 'Soccer', icon: '⚽', filter: 'soccer' },
|
||||
{ name: 'Baseball', icon: '⚾', filter: 'baseball' },
|
||||
{ name: 'MMA', icon: '🥊', filter: 'mma' },
|
||||
]
|
||||
|
||||
// Group events by sport
|
||||
const eventsBySport = sports.map(sport => ({
|
||||
...sport,
|
||||
events: events?.filter(e => e.sport.toLowerCase() === sport.filter) || [],
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
@ -23,30 +37,49 @@ export const Sports = () => {
|
||||
<p className="text-xl text-gray-600">Choose your sport and start betting</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{sports.map((sport) => (
|
||||
<Link
|
||||
key={sport.name}
|
||||
to="/"
|
||||
className="bg-white rounded-xl shadow-sm hover:shadow-md transition-shadow p-6 border border-gray-100"
|
||||
>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<span className="text-4xl">{sport.icon}</span>
|
||||
<h2 className="text-2xl font-bold text-gray-900">{sport.name}</h2>
|
||||
{isLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{eventsBySport.map((sport) => (
|
||||
<div
|
||||
key={sport.name}
|
||||
className="bg-white rounded-xl shadow-sm p-6 border border-gray-100"
|
||||
>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<span className="text-4xl">{sport.icon}</span>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">{sport.name}</h2>
|
||||
<p className="text-sm text-gray-500">{sport.events.length} events</p>
|
||||
</div>
|
||||
</div>
|
||||
{sport.events.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{sport.events.slice(0, 3).map((event) => (
|
||||
<Link
|
||||
key={event.id}
|
||||
to={`/events/${event.id}`}
|
||||
className="block p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<p className="font-medium text-gray-900 text-sm">
|
||||
{event.home_team} vs {event.away_team}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{event.league}</p>
|
||||
</Link>
|
||||
))}
|
||||
{sport.events.length > 3 && (
|
||||
<p className="text-xs text-gray-400 text-center pt-2">
|
||||
+{sport.events.length - 3} more events
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400">No upcoming events</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{sport.leagues.map((league) => (
|
||||
<span
|
||||
key={league}
|
||||
className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm"
|
||||
>
|
||||
{league}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-12 text-center">
|
||||
<Link to="/">
|
||||
|
||||
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 242 KiB |