<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-betElement tag
html
<goma-suggested-bet></goma-suggested-bet>Installation
bash
pnpm add @gomagaming/suggested-bet vue pinia vue-router vue-i18n @vueuse/coreThe 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:
dataSource | How 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(andodd) labels inline as a fallback, or usepropmode. - 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:
| Row | Field | Fallback |
|---|---|---|
| Competition | competitionLabel | "{competitionName} - {countryName}", then tournamentName |
| Pick (highlighted) | pick | outcomeName |
| Market | market | marketName |
| Fixture | fixture | "{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
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
bet | bet | object | null | null | The host-fed featured bet (see shape above). |
dataSource | data-source | 'prop' | 'rpc' | 'subscribe' | 'prop' | How legs resolve (see Data sources). rpc/subscribe need socket config. |
sportEventLimit | sport-event-limit | number | 50 | Max events pulled per sport feed when resolving legs in rpc/subscribe. |
currency | currency | string | '€' | Currency symbol for the Bet / Winnings figures. |
currencyPosition | currency-position | 'suffix' | 'prefix' | 'suffix' | 'suffix' → 10,00€; 'prefix' → $10.00. The numeric part is formatted with the active locale. |
showRank | show-rank | boolean | true | Show the rank badge. |
scrollAfter | scroll-after | number | 3 | Legs above this count scroll; 0 disables scrolling. Ignored when cardHeight is set. |
cardHeight | card-height | number | string | null | null | Fixed card height (number → px, or a CSS length). All cards line up and the legs list scrolls if it overflows. |
iconBaseUrl | icon-base-url | string | '' | ${iconBaseUrl}/icons/sports/dark_mode/{sportId}.svg. Per-leg sportIconUrl overrides it. |
flagBaseUrl | flag-base-url | string | glastcoper flag CDN | ${flagBaseUrl}{venueId}.png. Per-leg flagUrl overrides it. |
locale | locale | string | 'en' | i18n locale; also drives currency number formatting. |
theme | theme | object | {} | --goma-* overrides. |
Events
| Event | Detail | When |
|---|---|---|
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:
| Token | Default | Paints |
|---|---|---|
--goma-backgroundPrimary | #03061b | Card background |
--goma-backgroundSecondary | #161732 | Leg background |
--goma-highlightPrimary | #ff6600 | Rank badge background + highlighted pick |
--goma-highlightPrimaryContrast | #ffffff | Rank badge text |
--goma-buttonBackgroundPrimary | #ff6600 | CTA background |
--goma-buttonTextPrimary | #ffffff | CTA text |
--goma-textPrimary | #ffffff | Market + summary text |
--goma-textSecondary | #939dff | Competition / fixture / selection count |
--goma-iconSecondary | #5559b4 | Sport icon glyph |
--goma-separatorLine | #2a2d60 | Leg + summary dividers, scrollbar track |
--goma-separatorLineSecondary | #393d83 | Scrollbar thumb |
--goma-cardBorderLineGradient3 | #b25633 | Card-border gradient — accent end (top-right) |
--goma-cardBorderLineGradient2 | #7c4b57 | Card-border gradient — mid stop |
--goma-cardBorderLineGradient1 | #393d83 | Card-border gradient — far end |
See the theming guide for the full token reference.
Source layout
| Layer | File |
|---|---|
| Element class | packages/suggested-bet/src/SuggestedBetElement.js |
| App wrapper | packages/suggested-bet/src/SuggestedBetApp.vue |
| Card | packages/suggested-bet/src/components/SuggestedBet/SuggestedBetCard.vue |
| Leg | packages/suggested-bet/src/components/SuggestedBet/SuggestedBetLeg.vue |
| View-model + formatting + betslip load | packages/suggested-bet/src/composables/useSuggestedBet.js |
| Event-resolution driver (rpc/subscribe) | packages/suggested-bet/src/composables/useSuggestedBetEvents.js |
| Scrollbar CSS | packages/suggested-bet/src/styles/card.css |
| Demo entry | demo/src/playground.js |
| Playground manifest | packages/suggested-bet/src/playground.manifest.js |
| Tests | packages/suggested-bet/tests/{SuggestedBet,playground-manifest}.test.js |