Developer Guides
Tutorial: build your dApp
Step 3: Create a Page With Sport Events

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.

Setup Watchers

Odds frequently fluctuate, and certain games or markets might temporarily pause for various reasons. To keep actual data in the UI, we need to setup watchers. They keep all the data up-to-date.

Create small component src/components/Watchers.tsx

src/components/Watchers.tsx
'use client'
import { useWatchers } from '@azuro-org/sdk'
 
export function Watchers() {
  useWatchers()
 
  return null
}

Yes, it’s that simple. We created component, because hook must have access to Providers contexts. Then add export to src/components/index.ts and use it in src/app/layout.tsx:

src/app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import '@rainbow-me/rainbowkit/styles.css'
import { Providers, Header, Watchers } from '@/components'
 
const inter = Inter({ subsets: ['latin'] })
 
export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
      <Providers>
        <Header />
        <main className="container pt-5 pb-10">
          {children}
        </main>
        <Watchers />
      </Providers>
      </body>
    </html>
  )
}
 

Events page

Game data render

To render each game we will create a small component "GameCard" (create file src/components/GameCard.tsx). Azuro SDK have helping types, so we'll use it for props:

src/components/GameCard.tsx
import { GamesQuery } from '@azuro-org/sdk'
import Link from 'next/link'
import dayjs from 'dayjs'
 
 
type Props = {
  game: GamesQuery['games'][0]
}
 
export function GameCard(props: Props) {
  const { gameId, sport, league, participants, startsAt } = props.game
 
  return (
    <Link
      className="p-4 bg-zinc-50 rounded-3xl hover:bg-zinc-100 transition"
      href={`/events/${sport.slug}/${gameId}`}
    >
      <div className="flex justify-between text-sm">
        <span>{sport.name}</span>
        <span>{dayjs(startsAt * 1000).format('DD MMM HH:mm')}</span>
      </div>
      <div className="mt-2 text-sm text-zinc-400">
        {league.country.name} &middot; {league.name}
      </div>
      <div className="mt-3 space-y-1">
        {
          participants.map(({ image, name }) => (
            <div key={name} className="flex items-center">
              <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>
          ))
        }
      </div>
    </Link>
  )
}

Don't forget to add export to src/components/index.ts. Now we're ready to render games.

Fetch games data

In one of previous chapters, we added file src/app/events/[sport]/page.tsx. Let's open it and add data fetching. For top events we'll use different filters - sort games by turnover and limit them by 6. For sport categories - just filter by sport

src/app/events/[sport]/page.tsx
'use client'
import { useParams } from 'next/navigation'
import { useGames, Game_OrderBy } from '@azuro-org/sdk'
import { GameCard } from '@/components'
 
 
const useData = () => {
  const params = useParams<{ sport: string }>()
 
  const props =
    params.sport === 'top'
      ? {
        orderBy: Game_OrderBy.Turnover,
        filter: {
          limit: 6,
        },
      }
      : {
        filter: {
          sportSlug: params.sport,
        },
      }
 
  return useGames(props)
}
 
export default function EventsPage() {
  const { loading, data } = useData()
 
  return (
    <>
      {
        loading ? (
          <div>Loading...</div>
        ) : (
          <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
            {
              data?.games.map((game) => (
                <GameCard key={game.id} game={game} />
              ))
            }
          </div>
        )
      }
    </>
  )
}

Sports navigation bar

To show navigation between sports, let's add small helping component "ActiveLink". It's just a wrapper of Next.JS Link but handles active state. Create component src/components/ActiveLink.tsx

src/components/ActiveLink.tsx
'use client'
import React, { useState, useEffect } from 'react'
import { usePathname } from 'next/navigation'
import Link, { LinkProps } from 'next/link'
 
 
type ActiveLinkProps = LinkProps & {
  className?: string
  activeClassName: string
  regex?: string
}
 
export const ActiveLink: React.FC<React.PropsWithChildren<ActiveLinkProps>> = (props) => {
  const { children, className, activeClassName, regex, ...rest } = props
 
  const activePathname = usePathname()
  const [ computedClassName, setComputedClassName ] = useState(className)
 
  useEffect(() => {
    // Dynamic route will be matched via props.as
    // Static route will be matched via props.href
    const linkPathname = new URL(
      (rest.as || rest.href) as string,
      location.href
    ).pathname
 
    const isMatch = regex ? new RegExp(regex).test(activePathname) : activePathname === linkPathname
 
    const newClassName = isMatch
      ? `${className} ${activeClassName}`.trim()
      : className
 
    if (newClassName !== computedClassName) {
      setComputedClassName(newClassName)
    }
  }, [
    activePathname,
    rest.as,
    rest.href,
    activeClassName,
    className,
  ])
 
  return (
    <Link className={computedClassName} {...rest}>
      {children}
    </Link>
  )
}

Now let's create navigation bar component - src/components/SportsNavigation.tsx. Azuro SDK has special hook to fetch navigation - useSportsNavigation.

src/components/SportsNavigation.tsx
'use client'
import { useSportsNavigation } from '@azuro-org/sdk'
import { ActiveLink } from '@/components'
 
 
export function SportsNavigation() {
  const { loading, data } = useSportsNavigation({
    withGameCount: true,
  })
 
  if (loading) {
    return <div>Loading...</div>
  }
 
  // it's simple sort by games count, you can implement your own
  const sortedSports = [ ...data?.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 exports to src/components/index.ts:

src/components/index.ts
export * from './Providers'
export * from './Header'
export * from './Watchers'
export * from './GameCard'
export * from './ActiveLink'
export * from './SportsNavigation'

Now, let's add navigation to our EventsPage, open src/app/events/[sport]/page.tsx and add SportsNavigation, so final code is:

src/app/events/[sport]/page.tsx
'use client'
import { useParams } from 'next/navigation'
import { useGames, Game_OrderBy } from '@azuro-org/sdk'
import { GameCard, SportsNavigation } from '@/components'
 
 
const useData = () => {
  const params = useParams<{ sport: string }>()
 
  const props =
    params.sport === 'top'
      ? {
        orderBy: Game_OrderBy.Turnover,
        filter: {
          limit: 6,
        },
      }
      : {
        filter: {
          sportSlug: params.sport,
        },
      }
 
  return useGames(props)
}
 
export default function Events() {
  const { loading, data } = useData()
 
  return (
    <>
      <SportsNavigation />
      {
        loading ? (
          <div>Loading...</div>
        ) : (
          <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
            {
              data?.games.map((game) => (
                <GameCard key={game.id} game={game} />
              ))
            }
          </div>
        )
      }
    </>
  )
}

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: