Allow Users to View and Redeem Bets
Helping components
BetCard
Let's create bet view to render user bets. Create BetCard.tsx
in the src/components
folder.
Because of limitations, graphql data doesn't have full range of statuses (like "Pending resolution"). But SDK has helpers and we will map statuses.
Each bet may contain more than one selection (combo bets), so we'll make universal view for single and combo bets wil redeeming logic, for that purpose SDK has useRedeemBet
hook.
'use client'
import { type Bet, useChain, useRedeemBet } from '@azuro-org/sdk'
import { getBetStatus, getGameStatus, BetStatus, GameStatus } from '@azuro-org/toolkit'
import dayjs from 'dayjs'
import Link from 'next/link'
import { useMemo } from 'react'
import cx from 'clsx'
const BetStatusText = {
[BetStatus.Accepted]: 'Accepted',
[BetStatus.Canceled]: 'Canceled',
[BetStatus.Live]: 'Live',
[BetStatus.PendingResolution]: 'Pending resolution',
[BetStatus.Resolved]: 'Resolved',
}
const GameStatusText = {
[GameStatus.Canceled]: 'Canceled',
[GameStatus.Live]: 'Live',
[GameStatus.Paused]: 'Paused',
[GameStatus.PendingResolution]: 'Pending resolution',
[GameStatus.Created]: 'Preparing',
[GameStatus.Resolved]: 'Resolved',
}
type Props = {
bet: Bet
}
export function BetCard(props: Props) {
const { bet } = props
const {
createdAt, status: graphBetStatus, amount, outcomes,
payout, possibleWin, freebetId,
isWin, isCanceled, isRedeemed, isLive
} = bet
const { betToken } = useChain()
const { submit, isPending, isProcessing } = useRedeemBet()
const betStatus = useMemo(() => {
return getBetStatus({
graphStatus: graphBetStatus,
games: outcomes.map(({ game }) => game),
isLiveBet: isLive,
})
}, [])
const isDisabled = isPending || isProcessing
let winAmount
let buttonTitle
if (isCanceled) {
winAmount = '––'
buttonTitle = 'Refund'
}
else {
winAmount = `${isWin ? '+' : ''}${possibleWin} ${betToken.symbol}`
buttonTitle = 'Redeem'
}
if (isPending) {
buttonTitle = 'Waiting for approval'
}
else if (isProcessing) {
buttonTitle = 'Processing...'
}
const handleRedeem = async () => {
try {
await submit({ bets: [ bet ] })
} catch {}
}
return (
<div className="p-4 bg-zinc-50 mt-2 first-of-type:mt-0 rounded-lg">
<div className="flex items-center justify-between">
<p>{dayjs(+createdAt * 1000).format('DD.MM.YYYY, hh:mm A')}</p>
<p>{BetStatusText[betStatus]}</p>
</div>
{
outcomes.map((outcome) => {
const { odds, marketName, game, selectionName, isWin, isLose } = outcome
const { status: gameStatus, gameId, participants, startsAt, sport: { slug: sportSlug }, league } = game
const { name: leagueName, country: { name: countryName } } = league
const className = cx("mt-4 p-4 rounded-md", {
'bg-zinc-200': !isWin && !isLose,
'bg-green-100': isWin,
'bg-red-100': isLose,
})
return (
<div key={gameId} className={className}>
<div className="flex items-center justify-between flex-wrap">
<div className="flex items-center flex-wrap">
<p className='mr-4'>{dayjs(+startsAt * 1000).format('DD.MM.YYYY, hh:mm A')}</p>
<p>{`${countryName}: ${leagueName}`}</p>
</div>
<p>{GameStatusText[getGameStatus({ graphStatus: gameStatus, startsAt: +startsAt, isGameInLive: isLive })]}</p>
</div>
<div className="flex items-center">
<Link href={`/events/${sportSlug}/${gameId}`} className="flex items-center mr-4">
{
participants.map(({ image, name }) => (
<div key={name} className="flex items-center ml-2 first-of-type:ml-0">
<div className="flex items-center justify-center w-8 h-8 mr-2 border border-zinc-300 rounded-full">
{
Boolean(image) && (
<img className="w-4 h-4" src={image!} alt="" />
)
}
</div>
<span className="text-md">{name}</span>
</div>
))
}
</Link>
</div>
<div className="grid md:grid-cols-3 md:gap-16">
<div>
<p>Market</p>
<p>{ marketName }</p>
</div>
<div>
<p>Outcome</p>
<p>{ selectionName }</p>
</div>
<div className="min-w-40 pr-4">
<p>Odds</p>
<p>{odds}</p>
</div>
</div>
</div>
)
})
}
<div className="flex items-center justify-between mt-2 flex-wrap">
<div className='flex items-center'>
<p>Bet Amount:</p>
<p>{`${amount} ${betToken.symbol}`}</p>
</div>
<div className="flex items-center flex-wrap">
<div className='flex items-center mr-4'>
<p>{isWin ? 'Winning:' : 'Possible Win:'}</p>
<p>{winAmount}</p>
</div>
{
isRedeemed ? (
<p>Redeemed</p>
) : (
Boolean(payout || (isCanceled && !freebetId) || isRedeemed) && (
<button
className={cx('md:w-[200px] py-3.5 text-white font-semibold text-center rounded-xl', {
'bg-blue-500 hover:bg-blue-600 transition shadow-md': !isDisabled,
'bg-zinc-300 cursor-not-allowed': isDisabled,
})}
disabled={isDisabled}
onClick={handleRedeem}
>
{buttonTitle}
</button>
)
)
}
</div>
</div>
</div>
)
}
RedeemAll button component
It's possible to redeem all single and combo bets in a single transaction. So we can create component RedeemAll
and add it to the UI.
SDK hook useRedeemBet
accepts array of bets, so it will be easy to implement. The only limitation, that we can't redeem freebets*, so we must filter them.
* Freebets have their own logic which you can learn about in Freebets section once you've mastered the basic app logic and are ready to delve deeper.
Create RedeemAll.tsx
in the src/components
folder:
'use client'
import React from 'react'
import { type Bet, useRedeemBet } from '@azuro-org/sdk'
import cx from 'clsx'
type Props = {
bets: Bet[]
}
export function RedeemAll(props: Props) {
const { bets } = props
const { submit, isPending, isProcessing } = useRedeemBet()
const unredeemedBets = bets?.filter((bet) => (
!bet.freebetContractAddress
&& bet.isRedeemable
))
const isDisabled = !unredeemedBets.length || isPending || isProcessing
const handleRedeem = async () => {
try {
await submit({ bets: unredeemedBets })
} catch {}
}
let buttonTitle = 'Redeem all'
if (isPending) {
buttonTitle = 'Waiting for approval'
}
else if (isProcessing) {
buttonTitle = 'Processing...'
}
return (
<button
className={cx('md:w-[200px] py-3.5 text-white font-semibold text-center rounded-xl mb-4', {
'bg-blue-500 hover:bg-blue-600 transition shadow-md': !isDisabled,
'bg-zinc-300 cursor-not-allowed': isDisabled,
})}
disabled={isDisabled}
onClick={handleRedeem}
>
{buttonTitle}
</button>
)
}
Don't forget to add export to src/components/index.ts
.
Now we're ready to implement bets history page.
Bets history page
Create folder bets
in the folder src/app
and file page.tsx
inside.
Here we'll use usePrematchBets
and useLiveBets
hook from the SDK to fetch bets and our created components BetCard
and RedeemAll
to render them.
'use client'
import { useMemo } from 'react'
import { usePrematchBets, useLiveBets, OrderDirection } from '@azuro-org/sdk'
import { useAccount } from 'wagmi'
import { BetCard, RedeemAll } from '@/components'
export default function Bets() {
const { address } = useAccount()
const props = {
filter: {
bettor: address!,
},
orderDir: OrderDirection.Desc,
}
const { loading: isPrematchLoading, bets: prematchBets } = usePrematchBets(props)
const { loading: isLiveLoading, bets: liveBets } = useLiveBets(props)
const bets = useMemo(() => {
return [ ...liveBets, ...prematchBets ].sort((betA, betB) => betB.createdAt - betA.createdAt)
}, [ prematchBets, liveBets ])
if (isLiveLoading || isPrematchLoading) {
return <div>Loading...</div>
}
if (!bets.length) {
return <div>You don't have bets yet</div>
}
return (
<div>
<RedeemAll bets={bets} />
{
bets.map(bet => (
<BetCard key={`${bet.createdAt}-${bet.tokenId}`} bet={bet} />
))
}
</div>
)
}
That's all! Let's check how it looks, open localhost:3000 (opens in a new tab) from your browser. If you already made a bet, you will see something like this:
Learn more
In our tutorial we're building simple betting app. If you're ready to go deeper, learn things from SDK that we used in this section: