Skip to content

<goma-suggested-bet>

A featured "Bet the same" suggested-bet card — a premium, high-intent card for a ranked carousel. The card is framed by the cyan card-border gradient (the same cardBorderLineGradient* frame as the events-horizontal featured pre-live card). It renders:

  • a rank badge (#1, #2, …) — a glowing tab hanging down into the card from its top-centre edge (square top flush with the card border, rounded bottom),
  • an N-selections header,
  • the per-selection legs — competition + country flag, the highlighted pick, the market, and the fixture,
  • a Bet / Total Odds / Winnings summary, and
  • a "Bet the same" CTA that appends every leg to the shared cross-widget betslip and injects the card's stake.

All data is host-fed via the bet property — there is no suggested-bet API yet; the host curates the card.

@gomagaming/suggested-bet

Element tag

html
<goma-suggested-bet></goma-suggested-bet>

Installation

bash
pnpm add @gomagaming/suggested-bet vue pinia vue-router vue-i18n @vueuse/core

The framework packages are peer dependencies installed once at the app level; bundlers dedup them across every @gomagaming/* widget.

Configuration

js
el.config = {
  locale: 'en',
  // Optional restyle:
  theme: { '--goma-buttonBackgroundPrimary': '#ff6600' },
}

The card reads no live feed of its own, so it needs no socket / API connection config. The connection fields only matter when a betslip widget on the same page resolves live odds for the loaded selections.

Data sources

The dataSource prop chooses how each leg's event data (teams, competition, odds) is obtained:

dataSourceHow legs resolve
'prop' (default)Fully host-fed — each selection carries its own display fields and odds. Works offline.
'subscribe'Each selection is just { eventId, bettingOfferId }. The widget subscribes to those events' sport feed and resolves teams, competition, market name, pick name and live odds, recomputing Total Odds + Winnings as odds tick. Needs socket config.
'rpc'Same resolution as subscribe but a one-shot snapshot — the subscription is released once the legs first resolve, so odds don't keep ticking.

In rpc/subscribe mode you send only the two IDs per leg:

js
el.dataSource = 'subscribe'
el.config = { socketUrl: 'wss://…', socketRealm: 'sportsbook', ucsOperatorId: '…' }
el.bet = {
  rank: 1,
  stake: 10,
  selections: [
    { eventId: '123', bettingOfferId: '456' },
    // …more legs (optional `sportId` per leg — defaults to 1/Football —
    //   tells the widget which sport feed to subscribe to)
  ],
}

The widget walks bettingOfferId → outcome → market through the betting store's entity maps (the same model <goma-events-horizontal> uses — the offer carries outcomeId / marketId / bettingTypeName), resolving the market name and pick (outcome) name, and reads teams / competition / flag / live odds off the event. The eventId is optional — it's derived from the offer when omitted — but giving it (with sportId) tells the widget which sport feed to subscribe to. Two caveats:

  • The offer must be one the subscribed feed actually streams (the matches-aggregator feed carries the event's markets + offers). For an offer the feed doesn't include, supply market / pick (and odd) labels inline as a fallback, or use prop mode.
  • Until the feed returns the offer/event, that leg shows a "resolving" placeholder (visible in the playground, which has no live socket). Any host-fed label on a selection is used as a fallback in the meantime.

The bet shape (prop mode)

In prop mode the card is fully host-fed via the bet property (a JS object, or a JSON string via the bet attribute):

js
el.bet = {
  rank: 1,            // badge index; omit / set showRank=false to hide
  stake: 10,          // the "Bet" figure (and the stake injected on CTA)
  totalOdds: 8.0,     // optional — falls back to the product of leg odds
  winnings: 80,       // optional — falls back to stake × totalOdds
  selections: [
    {
      // ── betslip identity (handed to the betslip on CTA) ──
      outcomeId: 'o1',
      bettingOfferId: 'o1',
      eventId: 'evt-1',
      marketId: 'm1',
      marketName: 'Double chance',
      outcomeName: 'Barcelona ou Match nul',
      odd: 2.0,
      sportId: 1,
      competitionId: 'c1',
      homeParticipantName: 'Barcelona',
      awayParticipantName: 'Real Madrid',
      // ── display helpers (optional; sensible fallbacks below) ──
      competitionName: 'La Liga',
      countryName: 'Spain',
      flagUrl: 'https://flagcdn.com/w40/es.png', // overrides flagBaseUrl+venueId
      venueId: 11,                                // used with flagBaseUrl
      sportIconUrl: '',                           // overrides iconBaseUrl+sportId
    },
    // …more legs
  ],
}

Display fallbacks

Each leg renders from the display helpers, falling back to the betslip identity fields, so a minimal selection still renders:

RowFieldFallback
CompetitioncompetitionLabel"{competitionName} - {countryName}", then tournamentName
Pick (highlighted)pickoutcomeName
MarketmarketmarketName
Fixturefixture"{homeParticipantName} x {awayParticipantName}", then eventName

Scrollable variant

When the number of legs exceeds scrollAfter (default 3), the legs list becomes a fixed-height scrollable region with a themed scrollbar — matching the design's scrollable variant. Set scrollAfter to 0 to never scroll.

For a carousel where every card must be the same height regardless of leg count, set cardHeight (number → px, or a CSS length string). The card is then exactly that tall and the legs list fills the middle and scrolls when it overflows — the header, summary and CTA stay pinned. cardHeight takes precedence over scrollAfter.

Props / attributes

PropertyAttributeTypeDefaultDescription
betbetobject | nullnullThe host-fed featured bet (see shape above).
dataSourcedata-source'prop' | 'rpc' | 'subscribe''prop'How legs resolve (see Data sources). rpc/subscribe need socket config.
sportEventLimitsport-event-limitnumber50Max events pulled per sport feed when resolving legs in rpc/subscribe.
currencycurrencystring'€'Currency symbol for the Bet / Winnings figures.
currencyPositioncurrency-position'suffix' | 'prefix''suffix''suffix'10,00€; 'prefix'$10.00. The numeric part is formatted with the active locale.
showRankshow-rankbooleantrueShow the rank badge.
scrollAfterscroll-afternumber3Legs above this count scroll; 0 disables scrolling. Ignored when cardHeight is set.
cardHeightcard-heightnumber | string | nullnullFixed card height (number → px, or a CSS length). All cards line up and the legs list scrolls if it overflows.
iconBaseUrlicon-base-urlstring''${iconBaseUrl}/icons/sports/dark_mode/{sportId}.svg. Per-leg sportIconUrl overrides it.
flagBaseUrlflag-base-urlstringglastcoper flag CDN${flagBaseUrl}{venueId}.png. Per-leg flagUrl overrides it.
localelocalestring'en'i18n locale; also drives currency number formatting.
themethemeobject{}--goma-* overrides.

Events

EventDetailWhen
goma:bet-the-same{ rank, stake, totalOdds, winnings, selectionCount, selections[] }The CTA was activated. Every leg has already been appended to the shared betslip and the stake injected; the detail carries the loaded selections.
ready{}Widget mounted, initial render complete.
error{ message, code, component? }Render error caught by the widget's boundary.

All events are composed + bubbles, so they cross the Shadow DOM boundary to host listeners.

Betslip integration

The CTA loads selections into the shared cross-widget betslip (the goma:betslip:v3 storage envelope) using append semantics — each leg is added if not already present (dedup on outcomeId), and the card's stake is injected. Drop a betslip widget on the same page and it picks the legs up with no extra wiring:

html
<goma-suggested-bet id="sb"></goma-suggested-bet>
<goma-betslip-sidebar></goma-betslip-sidebar>

<script type="module">
  import '@gomagaming/suggested-bet'
  import '@gomagaming/betslip-sidebar'

  document.getElementById('sb').bet = { /* …featured bet… */ }
</script>

To run your own backend (instead of, or alongside, the betslip widget), listen for goma:bet-the-same and read detail.selections.

Embedding examples

Vanilla / CDN

html
<goma-suggested-bet id="sb"></goma-suggested-bet>
<script type="module">
  import '@gomagaming/suggested-bet'

  const el = document.getElementById('sb')
  el.config = { locale: 'en' }
  el.bet = { rank: 1, stake: 10, totalOdds: 8, selections: [ /* … */ ] }
  el.addEventListener('goma:bet-the-same', (e) => console.log(e.detail))
</script>

React

jsx
import '@gomagaming/suggested-bet'

export function SuggestedBet({ bet }) {
  return (
    <goma-suggested-bet
      ref={(el) => {
        if (!el) return
        el.config = { locale: 'en' }
        el.bet = bet
      }}
    />
  )
}

Vue

vue
<script setup>
import '@gomagaming/suggested-bet'
import { ref, watchEffect } from 'vue'

const elRef = ref(null)
const props = defineProps({ bet: Object })
watchEffect(() => {
  if (elRef.value) {
    elRef.value.config = { locale: 'en' }
    elRef.value.bet = props.bet
  }
})
</script>

<template>
  <goma-suggested-bet ref="elRef" />
</template>

Theming

All colours map to existing --goma-* tokens — no new tokens. The card paints:

TokenDefaultPaints
--goma-backgroundPrimary#03061bCard background
--goma-backgroundSecondary#161732Leg background
--goma-highlightPrimary#ff6600Rank badge background + highlighted pick
--goma-highlightPrimaryContrast#ffffffRank badge text
--goma-buttonBackgroundPrimary#ff6600CTA background
--goma-buttonTextPrimary#ffffffCTA text
--goma-textPrimary#ffffffMarket + summary text
--goma-textSecondary#939dffCompetition / fixture / selection count
--goma-iconSecondary#5559b4Sport icon glyph
--goma-separatorLine#2a2d60Leg + summary dividers, scrollbar track
--goma-separatorLineSecondary#393d83Scrollbar thumb
--goma-cardBorderLineGradient3#b25633Card-border gradient — accent end (top-right)
--goma-cardBorderLineGradient2#7c4b57Card-border gradient — mid stop
--goma-cardBorderLineGradient1#393d83Card-border gradient — far end

See the theming guide for the full token reference.

Source layout

LayerFile
Element classpackages/suggested-bet/src/SuggestedBetElement.js
App wrapperpackages/suggested-bet/src/SuggestedBetApp.vue
Cardpackages/suggested-bet/src/components/SuggestedBet/SuggestedBetCard.vue
Legpackages/suggested-bet/src/components/SuggestedBet/SuggestedBetLeg.vue
View-model + formatting + betslip loadpackages/suggested-bet/src/composables/useSuggestedBet.js
Event-resolution driver (rpc/subscribe)packages/suggested-bet/src/composables/useSuggestedBetEvents.js
Scrollbar CSSpackages/suggested-bet/src/styles/card.css
Demo entrydemo/src/playground.js
Playground manifestpackages/suggested-bet/src/playground.manifest.js
Testspackages/suggested-bet/tests/{SuggestedBet,playground-manifest}.test.js