Create a Page With Sport Events
Azuro stores data from the blockchain in the Subgraph (GraphQL). You don't need any queries, all it's packaged under the hood of our SDK.
UI components
In this section we will create components for the Page.
OutcomeButton
This component will serve to display all outcomes with live-updated odds and status, alongside "add to betslip" functionality. There is a hooks in SDK to make it easy.
Create src/components/OutcomeButton.tsx
.
'use client'
import { type MarketOutcome, useSelection, useBaseBetslip } from '@azuro-org/sdk'
import cx from 'clsx'
type OutcomeProps = {
className?: string
outcome: MarketOutcome
}
export function OutcomeButton(props: OutcomeProps) {
const { className, outcome } = props
const { items, addItem, removeItem } = useBaseBetslip()
const { odds, isLocked, isOddsFetching } = useSelection({
selection: outcome,
initialOdds: outcome.odds,
initialStatus: outcome.status,
})
const isActive = Boolean(items?.find((item) => {
const propsKey = `${outcome.coreAddress}-${outcome.lpAddress}-${outcome.gameId}-${outcome.conditionId}-${outcome.outcomeId}`
const itemKey = `${item.coreAddress}-${item.lpAddress}-${item.game.gameId}-${item.conditionId}-${item.outcomeId}`
return propsKey === itemKey
}))
const buttonClassName = cx(`flex justify-between p-5 transition rounded-2xl cursor-pointer w-full disabled:cursor-not-allowed disabled:opacity-50 ${className}`, {
'bg-slate-200 hover:bg-slate-300': isActive,
'bg-zinc-50 hover:bg-zinc-100': !isActive,
})
const handleClick = () => {
const item = {
gameId: String(outcome.gameId),
conditionId: String(outcome.conditionId),
outcomeId: String(outcome.outcomeId),
coreAddress: outcome.coreAddress,
lpAddress: outcome.lpAddress,
isExpressForbidden: outcome.isExpressForbidden,
}
if (isActive) {
removeItem(String(outcome.gameId))
} else {
addItem(item)
}
}
return (
<button
className={buttonClassName}
onClick={handleClick}
disabled={isLocked}
>
<span className="text-zinc-500">{outcome.selectionName}</span>
<span className="font-medium">{isOddsFetching ? '--' : odds.toFixed(2)}</span>
</button>
)
}
Don't forget to add export to src/components/index.ts
.
League
The League
component to render leagues with games and markets.
'use client'
import { GamesQuery, SportsQuery, useGameStatus, useGameMarkets, useLive } from '@azuro-org/sdk'
import Link from 'next/link'
import cx from 'clsx'
import { useParams } from 'next/navigation'
import dayjs from 'dayjs'
import { OutcomeButton } from './index'
type GameProps = {
className?: string
game: GamesQuery['games'][0]
}
function Game(props: GameProps) {
const { className, game } = props
const { gameId, title, startsAt, status: graphStatus } = game
const { isLive } = useLive()
const { status } = useGameStatus({
graphStatus,
startsAt: +startsAt,
isGameExistInLive: isLive,
})
const { markets } = useGameMarkets({
gameStatus: status,
gameId,
})
return (
<div className={cx(className, "p-2 bg-zinc-200 rounded-lg flex items-center justify-between")}>
<div className='max-w-[220px] w-full'>
<Link
className="text-sm mb-2 hover:underline block whitespace-nowrap overflow-hidden text-ellipsis w-full"
href={`/event/${gameId}`}
>
{title}
</Link>
<div>{dayjs(+startsAt * 1000).format('DD MMM HH:mm')}</div>
</div>
{
Boolean(markets?.[0]?.outcomeRows[0]) && (
<div className="min-w-[500px]">
<div className="text-center">{markets![0].name}</div>
<div className="flex items-center">
{
markets![0].outcomeRows[0].map((outcome) => (
<OutcomeButton
className="ml-2 first-of-type:ml-0"
key={outcome.selectionName}
outcome={outcome}
/>
))
}
</div>
</div>
)
}
<Link
className="text-md p-2 rounded-lg bg-zinc-100 hover:underline"
href={`/event/${gameId}`}
>
All Markets =>
</Link>
</div>
)
}
type LeagueProps = {
className?: string
sportSlug: string
countryName: string
countrySlug: string
league: SportsQuery['sports'][0]['countries'][0]['leagues'][0]
}
export function League(props: LeagueProps) {
const { className, sportSlug, countryName, countrySlug, league } = props
const { games } = league
const params = useParams()
const isLeaguePage = params.league
return (
<div
className={cx(className, {
"p-4 bg-zinc-50 rounded-md": !isLeaguePage
})}>
<div className={cx("flex items-center mb-2", {
"text-sm": !isLeaguePage,
"text-lg font-bold": isLeaguePage
})}>
{
isLeaguePage && (
<>
<Link
className="hover:underline w-fit"
href={`/events/${sportSlug}/${countrySlug}`}
>
<div className="ml-2">{countryName}</div>
</Link>
<div className="mx-2">·</div>
</>
)
}
<Link
className="hover:underline w-fit"
href={`/events/${sportSlug}/${countrySlug}/${league.slug}`}
>
{league.name}
</Link>
</div>
{
games.map(game => (
<Game
key={game.gameId}
className="mt-2 first-of-type:mt-0"
game={game}
/>
))
}
</div>
)
}
Don't forget to add export to src/components/index.ts
.
Country
The Country
component to render countries.
'use client'
import { SportsQuery } from '@azuro-org/sdk'
import { useParams } from 'next/navigation'
import Link from 'next/link'
import cx from 'clsx'
import { League } from './League'
type CountryProps = {
className?: string
sportSlug: string
country: SportsQuery['sports'][0]['countries'][0]
}
export function Country(props: CountryProps) {
const { className, sportSlug, country } = props
const { leagues } = country
const params = useParams()
const isCountryPage = params.country
const isLeaguePage = params.league
return (
<div
className={cx(className, {
'p-4 bg-zinc-100 rounded-3xl': !isCountryPage && !isLeaguePage
})}>
{
!isLeaguePage && (
<Link
className={cx('mb-2 hover:underline', {
'text-md font-medium': !isCountryPage,
'text-lg font-bold': isCountryPage
})}
href={`/events/${sportSlug}/${country.slug}`}
>
{country.name}
</Link>
)
}
{
leagues.map(league => (
<League
key={league.slug}
className="mt-2 first-of-type:mt-0"
league={league}
sportSlug={sportSlug}
countryName={country.name}
countrySlug={country.slug}
/>
))
}
</div>
)
}
Don't forget to add export to src/components/index.ts
.
Sport
The Sport
component to render list of sports.
'use client'
import { SportsQuery } from '@azuro-org/sdk'
import { useParams } from 'next/navigation'
import Link from 'next/link'
import cx from 'clsx'
import { Country } from './Country'
type SportProps = {
sport: SportsQuery['sports'][0]
}
export function Sport(props: SportProps) {
const { sport } = props
const { countries } = sport
const params = useParams()
const isSportPage = params.sport !== 'top'
return (
<div
className={cx({
'p-4 bg-zinc-50 rounded-3xl mt-2 first-of-type:mt-0': !isSportPage
})}
>
{
!isSportPage && (
<Link
className="text-lg mb-2 hover:underline font-bold"
href={`/events/${sport.slug}`}
>
{sport.name}
</Link>
)
}
{
countries.map(country => (
<Country
key={country.slug}
className="mt-2 first-of-type:mt-0"
country={country}
sportSlug={sport.slug}
/>
))
}
</div>
)
}
Don't forget to add export to src/components/index.ts
.
Sports navigation bar
Now let's create navigation bar component - src/components/SportsNavigation.tsx
.
Azuro SDK has special hook to fetch navigation - useSportsNavigation
.
'use client'
import { useSportsNavigation, useLive } from '@azuro-org/sdk'
import { ActiveLink } from '@/components'
export function SportsNavigation() {
const { isLive } = useLive()
const { loading, sports } = useSportsNavigation({
withGameCount: true,
isLive,
})
if (loading) {
return <div>Loading...</div>
}
// it's simple sort by games count, you can implement your own
const sortedSports = [ ...sports || [] ].sort((a, b) => b.games!.length - a.games!.length)
return (
<div className="w-full mb-8 overflow-hidden">
<div className="w-full overflow-x-auto no-scrollbar">
<div className="flex space-x-1">
<ActiveLink
className="py-2 px-4 bg-zinc-100 whitespace-nowrap rounded-full"
activeClassName="!bg-purple-200"
href="/events/top"
>
Top
</ActiveLink>
{
sortedSports.map(({ slug, name, games }) => (
<ActiveLink
key={slug}
className="flex items-center py-2 px-4 bg-zinc-100 whitespace-nowrap rounded-full"
activeClassName="!bg-purple-200"
href={`/events/${slug}`}
>
<span>{name}</span>
{
games && (
<span className="pl-1.5 text-zinc-400">{games.length}</span>
)
}
</ActiveLink>
))
}
<div className="flex-none w-3 h-4" />
</div>
</div>
</div>
)
}
Don't forget to add export to src/components/index.ts
.
Now we're ready to render sports.
Fetch data
First of all you need to create shared layout. Create file layout.tsx
in the folder (src/app/events
).
For top events we'll use different filters - sort games by turnover and limit them by 10. For sport categories - just filter by sport
'use client'
import { SportsNavigation, Sport } from '@/components'
import { useParams } from 'next/navigation'
import { useSports, type UseSportsProps, Game_OrderBy, OrderDirection } from '@azuro-org/sdk'
const useData = () => {
const params = useParams()
const { isLive } = useLive()
const isTopPage = params.sport === 'top'
const props: UseSportsProps = isTopPage ? {
gameOrderBy: Game_OrderBy.Turnover,
filter: {
limit: 10,
}
} : {
gameOrderBy: Game_OrderBy.StartsAt,
orderDir: OrderDirection.Asc,
filter: {
sportSlug: params.sport as string,
countrySlug: params.country as string,
leagueSlug: params.league as string,
}
}
const { loading, sports } = useSports({ ...props, isLive })
return {
sports,
loading,
}
}
export default function EventsLayout() {
const { loading, sports } = useData()
return (
<>
<SportsNavigation />
{
loading ? (
<div>Loading...</div>
) : (
<div>
{
sports.map((sport) => (
<Sport key={sport.slug} sport={sport} />
))
}
</div>
)
}
</>
)
}
Next step: add routing structure for correct handling filters by country and league:
- in the path
src/app/events/[sport]
, create folder[country]
(with brackets) with subfolder[league]
- copy
src/app/events/[sport]/page.tsx
to created subfolders./[country]
,./[country]/[league]
.
So final structure for events
will be:
app
↳ events
↳ layout.tsx
↳ [sport]
↳ page.tsx
↳ [country]
↳ page.tsx
↳ [league]
↳ page.tsx
Well done! Now we have page with active events and navigation between sports. Let's check how it works, open localhost:3000 (opens in a new tab) from your browser.
The resulting output should be:
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: