Mr. Doge
Components

Match Card

Compact match row with teams, score or kickoff time, status, and an optional odds row.

"use client"import { MatchCard } from "@/registry/mrdoge-ui/match-card/match-card"import { matchToMatchCardProps } from "@/lib/mrdoge-adapters/match-card"import { useLiveMatch } from "@/registry/mrdoge-ui/use-live-match/use-live-match"import { DEMO_MATCH_ID } from "@/components/docs/sample-data"export function MatchCardLiveDemo() {  const match = useLiveMatch({ matchId: DEMO_MATCH_ID })  if (match === null) {    return <p className="text-sm text-fd-muted-foreground">Couldn't load this match right now.</p>  }  return (    <div className="w-full max-w-sm">      {match === undefined ? <MatchCard loading /> : <MatchCard {...matchToMatchCardProps(match)} />}    </div>  )}

With odds

Pass odds to render an odds row using the same options as Odds Selector. Set oddsPosition="right" to place it beside the match instead of below (shown second below).

oddsPosition="right" falls back to stacked below a 32rem container width: a CSS container query, tracking the card's own width rather than the viewport. Resize your browser to see it switch on the row card above.

This example subscribes to live odds (Business tier). If none are available for a match, its odds row doesn't render.

"use client"import { useState } from "react"import { MatchCard } from "@/registry/mrdoge-ui/match-card/match-card"import { matchToMatchCardProps } from "@/lib/mrdoge-adapters/match-card"import { useLiveMatch } from "@/registry/mrdoge-ui/use-live-match/use-live-match"import { useOdds } from "@/registry/mrdoge-ui/use-odds/use-odds"import { useOddsMovement } from "@/registry/mrdoge-ui/use-odds-movement/use-odds-movement"import {  useSharedLiveMatchId,  useSharedUpcomingMatchId,  MATCH_RESULT_BET_TYPES,} from "@/components/docs/demos/use-shared-demo-matches"function MatchCardOddsSlot({  matchId,  oddsPosition,}: {  matchId: string | null | undefined  oddsPosition?: "bottom" | "right"}) {  const match = useLiveMatch({ matchId: matchId ?? undefined })  const markets = useOdds({ matchId: match?.id, betTypes: MATCH_RESULT_BET_TYPES })  // markets is null when no market matched (not loading); preserve that  // distinction rather than letting `null?.[0]` collapse it into undefined  // (which would mean "still loading" and get stuck showing a skeleton).  const market = markets === null ? null : markets?.[0]  const movementById = useOddsMovement(market)  const [selectedOddsId, setSelectedOddsId] = useState<string | undefined>()  if (matchId === null || match === null) {    return <p className="text-sm text-fd-muted-foreground">No trending match to show right now.</p>  }  if (match === undefined) {    return <MatchCard loading oddsLoading oddsPosition={oddsPosition} />  }  return (    <MatchCard      {...matchToMatchCardProps(match, market ?? undefined, movementById)}      oddsLoading={market === undefined}      oddsPosition={oddsPosition}      selectedOddsId={selectedOddsId}      onSelectOdds={setSelectedOddsId}    />  )}export function MatchCardLiveWithOddsDemo() {  const upcomingMatchId = useSharedUpcomingMatchId()  const liveMatchId = useSharedLiveMatchId()  return (    <div className="flex w-full max-w-xl flex-col gap-4">      {/* Stacked variant only needs the same width as the Basic example.          The row variant below is the one that actually needs max-w-xl, for          its w-64 side-by-side odds column. */}      <div className="mx-auto w-full max-w-sm">        <MatchCardOddsSlot matchId={upcomingMatchId} />      </div>      <MatchCardOddsSlot matchId={liveMatchId} oddsPosition="right" />    </div>  )}

Selecting an option

Odds selection is controlled, the same pattern as Odds Selector. Match Card holds no selection state itself. Track the selected id and pass it back in:

const [selectedOddsId, setSelectedOddsId] = useState<string>()

<MatchCard
  {...matchCardProps}
  selectedOddsId={selectedOddsId}
  onSelectOdds={setSelectedOddsId}
/>

Pressing the selected option again deselects it: onSelectOdds fires with undefined, so setSelectedOddsId needs no extra logic. Omit both props and the row stays clickable, but nothing renders as selected.

Odds loading state

Odds typically resolve after the match itself, via a separate odds.list/odds.subscribe call. Pass oddsLoading while waiting to show a skeleton in the odds area instead of leaving it empty:

const markets = useOdds({ matchId, betTypes })
const market = markets?.[0]

<MatchCard
  {...matchCardProps}
  oddsLoading={market === undefined}
/>

Only set it on a card that actually uses odds; omit both odds and oddsLoading otherwise. loading (below) accepts the same oddsLoading/oddsPosition, so the skeleton can reserve odds space before the match itself has loaded.

Loading

Pass loading on its own to render a skeleton matching the card's real dimensions. Add oddsLoading (and oddsPosition for the row variant) to also reserve space for the odds row. Both examples above use this while their data loads.

import { MatchCard } from "@/registry/mrdoge-ui/match-card/match-card"export function MatchCardLoadingDemo() {  return (    <div className="flex w-full max-w-sm flex-col gap-4">      <MatchCard loading />      <MatchCard loading oddsLoading />    </div>  )}

Installation

pnpm dlx shadcn@latest add https://mrdoge.co/r/match-card.json

Use with the Mr. Doge SDK

MatchCard takes plain props, so it works with any data source. See the Match Card Adapter for the real function mapping a matches.get()/odds.list() response onto these props. It's the one behind both examples above.

Props

loading: true is mutually exclusive with every other prop except oddsLoading and oddsPosition; see Loading above. Everything below applies when loading is omitted or false.

Prop

Type

MatchCardTeam

Prop

Type

MatchCardOdds

Prop

Type

MatchCardSkeleton

<MatchCard loading /> renders this internally, but it's also exported on its own (import { MatchCardSkeleton } from "@/components/match-card") for a loading list of several cards before any of them have data yet. Takes className, oddsLoading, and oddsPosition, same meaning as on loading above.

On this page