Event Card
A single-event card — a competition header over the two competitors and their kickoff time, live score, or final result. Team sports show one score column; tennis shows a per-set breakdown with an expandable scoreboard. Live events auto-refresh on an interval. Sport-agnostic: works for any sport the Sports API covers (football, basketball, tennis, ice hockey, American football, …), adapting its status labels and score layout to each. Football events can additionally let signed-in users place predictions across several markets.
Import
import { EventCard } from "fansunited-frontend-components";
import { EventCardProps } from "fansunited-frontend-core";Required props
| Prop | Type | Description |
|---|---|---|
eventId | string | Event ID to render, e.g. "fb:m:12345". |
sdk | FansUnitedSDKModel | SDK instance. |
language | LanguageType | Display language. |
Optional props
| Prop | Type | Description |
|---|---|---|
themeOptions | CustomThemeOptions | See Theming. |
type | EventCardType | "header" | "detailed". The kind of card. Defaults to "header". "detailed" is reserved for a future release — see Card type. |
template | EventCardTemplateType | "compact" | "standard" | "hero". Visual density of the card. Defaults to "standard". See Templates. |
showCompetition | boolean | Show the competition header (crest + name + stage/round or country subtitle). Defaults to true. |
showOdds | boolean | Render betting odds in each template. Defaults to true. Not yet supported by the backend — see Odds. |
liveRefresh | boolean | Poll the events API while the event is LIVE so score/status stay current. Defaults to true. See Live refresh. |
refreshInterval | number | Live-refresh poll interval in milliseconds. Defaults to 30000 (30s). |
userIsLoggedIn | boolean | Whether the visitor is signed in. Required for predictions — see Predictions. Defaults to false. |
predictions | EventCardPredictionsConfig | Let signed-in users place single predictions on the event. See Predictions. |
Templates
template picks the visual prominence of the same card — it does not change what data is shown.
| Value | Layout |
|---|---|
"compact" | A livescore-style row: competition header over a match row, kickoff time / status on the left, competitors stacked on the right with a single score column (team sports) or a per-set grid (tennis). |
"standard" (default) | The two competitors flank a central column showing the kickoff time (upcoming) or the score / final result, each crest beside its name. |
"hero" | A large card — each competitor's crest stacked over its name, flanking a centered score / kickoff time in an inset panel, with a left accent bar on live events. |
Card type
type is a higher-level switch than template:
| Value | Behavior |
|---|---|
"header" (default) | The condensed event card described on this page (competition header + competitors + time/score). The only kind implemented today. |
"detailed" | A richer event view (lineups, stats, timeline, …). Reserved, not implemented yet — the prop and its contract are in place so it's stable before the feature lands. Passing it has no effect today; the header card is rendered regardless. |
Predictions
Signed-in users can place Fans United single predictions on the event, across several markets, in
a collapsible drawer at the bottom of the card. All three templates support it, each with a drawer
designed for its own layout — see Prediction drawer per template.
<EventCard
eventId="fb:m:12345"
sdk={sdk}
language="en"
userIsLoggedIn
predictions={{
markets: ["FT_1X2", "CORRECT_SCORE_ADVANCED", "OVER_GOALS_2_5"],
}}
/>;EventCardPredictionsConfig
EventCardPredictionsConfig| Field | Type | Description |
|---|---|---|
markets | EventCardPredictionMarket[] | Markets to offer, rendered in this order. Omitted or empty means no prediction UI. Values outside the supported set are skipped. |
display | "drawer" | "inline" | Whether the markets sit behind a collapsible footer row ("drawer", the default) or are rendered directly ("inline"). See Display mode. |
cutoffMinutes | number | How many minutes before kickoff predictions close. Defaults to 15, which is also the minimum — the Prediction API rejects predictions made inside the last 15 minutes before kickoff, so smaller values are raised to 15. Use a larger value to close the drawer earlier. |
allowDelete | boolean | Let users remove a prediction they already made. Defaults to true. Removal stays available while the match is in play — see Behaviour. |
defaultExpanded | boolean | Start the drawer expanded instead of collapsed. Defaults to false. Only applies when display is "drawer" — it keeps the collapse control, unlike display: "inline". |
Requirements
There is no enabled flag — the drawer renders when all of these hold, and is absent otherwise
(the card then looks exactly as it does without predictions configured):
- the event is a football event (the prediction API is football-only);
userIsLoggedInistrue— an anonymous visitor sees no prediction UI and no sign-in prompt;predictions.marketscontains at least one supported market;- the event resolves to a Football API match — see Event ids vs match ids.
The SDK must also be able to authenticate the user (an authProvider supplying their id token) —
the prediction endpoints all require it.
Event ids vs match ids
The card renders events from the Sports API, but predictions are made against the Football
API, and those are two different data providers. Their ids are kept aligned, so an event and its
match usually share one id — but that isn't guaranteed: a newly created football match can be 123
in the Football API and 456 in the Sports API.
Because the Prediction API only understands Football API match ids, the widget never sends an event id
to it. Instead, for a football event with predictions configured, it looks the id up via
sdk.football.getMatchById and then verifies the result is the same fixture — both team ids must
match exactly (the two APIs do share competitor ids) and the kickoff times must agree. Only then are
predictions offered, against the id the Football API returned.
If the lookup 404s, fails, or comes back as a different fixture, the prediction drawer simply isn't
rendered and the card behaves as it does without predictions configured. Predicting on the wrong
match would be worse than not offering predictions at all, so the check errs toward hiding.
This costs one extra getMatchById call, made only for football events that actually have predictions
configured for a signed-in user — never for other sports, anonymous visitors, or cards without a
market list.
Display mode
predictions.display picks how prominent the markets are. Both modes are available in every
template, styled to suit each one, and behave identically otherwise.
| Value | Behaviour |
|---|---|
"drawer" (default) | The markets sit behind a collapsible "Make your prediction" footer row. The card keeps the footprint it has without predictions until the user engages, so adding a market list doesn't make every card taller. |
"inline" | The markets render directly, with no collapse control and no chevron. For placements where predicting is the point of the card. |
The heading row is kept in both modes — as the toggle in "drawer", as a plain label in "inline" —
because without it the market rows would sit under the score with no context.
defaultExpanded is a third, distinct option: "drawer" with defaultExpanded: true opens on load
but the user can still collapse it, whereas "inline" has nothing to collapse.
<EventCard
eventId="fb:m:12345"
sdk={sdk}
language="en"
userIsLoggedIn
predictions={{
markets: ["FT_1X2", "CORRECT_SCORE_ADVANCED"],
display: "inline",
}}
/>;Prediction drawer per template
The behaviour is identical across templates; only the presentation differs, so the drawer belongs to
the card it sits in rather than looking bolted on.
| Template | Drawer |
|---|---|
"compact" | A thin footer strip below the match row. Each market is one line — question on the left, control on the right. Competitors are identified by a small crest (no name — the rows above already spell those out) and the steppers run horizontally, so no row grows taller than the livescore rhythm above it. |
"standard" | A full-bleed footer whose divider spans the card. Each market is a block: the question above its control, with competitors shown as crest + name. |
"hero" | The same layout as "standard", in a second inset panel below the score panel so the card reads as two deliberate blocks. The live accent bar stays on the score panel. |
Supported markets
Every MarketEnum value is supported.
| Answered by | Markets |
|---|---|
| Home / Draw / Away | FT_1X2, HT_1X2 |
Two of three outcomes (1X / 12 / X2) | DOUBLE_CHANCE |
| Half-time / full-time double | HT_FT |
| Exact scoreline | CORRECT_SCORE, CORRECT_SCORE_HT, CORRECT_SCORE_ADVANCED |
| A corner count | CORNERS_MATCH |
| Yes / No | BOTH_TEAMS_SCORE, OVER_GOALS_0_5…OVER_GOALS_6_5, OVER_CORNERS_6_5…OVER_CORNERS_13_5, RED_CARD_MATCH, PENALTY_MATCH |
| A player | PLAYER_SCORE, PLAYER_SCORE_FIRST_GOAL, PLAYER_SCORE_TWICE, PLAYER_SCORE_HATTRICK, PLAYER_YELLOW_CARD, PLAYER_RED_CARD |
A market outside this set is skipped and a console.warn names it, rather than the row silently
rendering nothing.
Player markets pick from both squads, grouped by team, with shirt numbers and headshots.
Configuring one costs two extra getTeamById calls (made only when a player market is present) —
squads rather than lineups, because lineups are confirmed about an hour before kickoff, by which
point predictions have already closed. PLAYER_SCORE_FIRST_GOAL additionally offers Own goal and
No goal will be scored.
Layout by option count. Markets with a handful of options render as pills; HT_FT renders as a
labelled 3×3 grid in standard / hero; and anything with a long list — HT_FT in compact, and
the player markets everywhere — uses a one-line select, so a compact row never grows taller than the
livescore rows above it.
Behaviour
- Before the cutoff each market is a row with its own selector. Picking values and hitting submit
sends one prediction per market; only markets the user actually touched are sent, so a card with a
correct-score market doesn't silently save0:0for someone who only picked a winner. A market
that fails to save keeps its value inline for a retry — the ones that succeeded stay saved. - A saved market shows the value read-only with edit and remove affordances. Editing re-opens the
selector and re-submitting overwrites; removing deletes that market's prediction only (each market
is its own prediction record). - After the cutoff, while the match is in play, the drawer becomes a read-only summary of the
markets the user predicted. Editing is no longer possible, but they can still remove a
prediction (unlessallowDeleteisfalse). Removing the last one hides the drawer entirely. - Once the match is finished or cancelled the summary is fully read-only — the result is graded,
so removal stops too — and each market shows its outcome and the points earned. With nothing
predicted, the drawer never appears.
Live refresh
When liveRefresh is true (the default) and the event is LIVE, the card re-fetches on refreshInterval so the score and status stay current. Only live events poll — upcoming and finished events are fetched once. Set liveRefresh={false} to disable polling entirely.
Sports & statuses
- Team sports render a single aggregate score (goals / points).
- Tennis renders a per-set breakdown — a compact per-set grid (
compacttemplate) or a result-first summary with an expandable full scoreboard (standard/hero), including tiebreak superscripts. - Status labels are localized and sport-aware. An event resolves to one of four states — upcoming, live, finished, cancelled. The finished label adapts to the sport: clock-based sports (football, basketball, hockey) show
FT; tennis, which has no game clock, showsFinished. - If the event can't be loaded, the card renders a contained "load failed" / "not available" message rather than breaking the host layout.
Examples
Basic (standard template)
import { EventCard } from "fansunited-frontend-components";
<EventCard eventId="fb:m:12345" sdk={sdk} language="en" />;Compact livescore row
<EventCard
eventId="fb:m:12345"
sdk={sdk}
language="en"
template="compact"
/>;Hero card without the competition header
<EventCard
eventId="fb:m:12345"
sdk={sdk}
language="en"
template="hero"
showCompetition={false}
/>;Finished event, no live polling
<EventCard
eventId="fb:m:12345"
sdk={sdk}
language="en"
liveRefresh={false}
/>;Slower live refresh (60s)
<EventCard
eventId="fb:m:12345"
sdk={sdk}
language="en"
refreshInterval={60000}
/>;Predictions closing an hour early, expanded, no delete
<EventCard
eventId="fb:m:12345"
sdk={sdk}
language="en"
userIsLoggedIn=USERISSIGNEDIN
predictions={{
markets: ["FT_1X2", "BOTH_TEAMS_SCORE", "CORNERS_MATCH"],
cutoffMinutes: 60,
allowDelete: false,
defaultExpanded: true,
}}
/>;Updated 4 days ago
