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.jsonUse 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.