<goma-multi-banners>
A swipeable hero carousel of injectable banner cards. One widget renders three card types side by side:
- sport — a sporting event. Set an
eventId(live or prelive) and the widget resolves the teams, start time and odds itself; or omiteventIdand supply those fields inline. The background image is always host-fed. - casino — a casino-game promo: category, title, description, CTA button.
- info — a generic promo banner: title, description, CTA button.
All data is host-fed via the banners property — nothing is pulled from a CMS. The carousel ships with prev/next arrows, pagination dots, swipe/drag, and opt-in autoplay.
@gomagaming/multi-bannersElement tag
html
<goma-multi-banners></goma-multi-banners>Installation
bash
pnpm add @gomagaming/multi-banners 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
The runtime config object carries shared widget settings and — only needed for eventId-based sport banners — the sports-data connection fields.
js
el.config = {
locale: 'en',
// Required only when a sport banner uses `eventId` (live/prelive lookup):
socketUrl: 'wss://…',
socketRealm: 'sportsbook',
ucsOperatorId: '2838',
bettingApiBaseUrl: 'https://…',
// Optional restyle:
theme: { '--goma-buttonBackgroundPrimary': '#ff6600' },
}Casino and info banners — and sport banners supplied inline — need no connection config at all.
Data sources
| Banner shape | How data gets in |
|---|---|
sport with eventId | Widget subscribes to the event's sport feed (live + prelive) over WAMP and fills in teams, start time and odds. Host supplies only eventId (+ backgroundImage, optional sportId). Live odds tick in place. |
sport without eventId | Host supplies homeTeam, awayTeam, time labels and outcomes inline. |
| casino / info | Always fully host-fed. |
eventId resolution note. Events are resolved from the sport's matches-aggregator feed (the same feed
<goma-events-horizontal>uses). An event must be present in that live/prelive feed for the widget to fill it in; until it arrives the card shows a placeholder. For full control, supply the fields inline. SetsportIdon the banner (default1) and raisesportEventLimitif you reference less-popular events.
Live vs prelive sport cards
A sport card adapts to the event's state, mirroring the live/prelive game cards elsewhere in the family:
- Live (
isLivetrue) — the right-hand block shows the live clock / match part (e.g.45' - 1st Half) and the running score (home - away). ForeventId-backed cards the score and clock tick live off the same feed the odds use; an inline live card with no score data falls back to a0 - 0placeholder. - Prelive — the right-hand block shows the start date + time (e.g.
Today/14:30), fromstartTimeor the explicitdateLabel/timeLabelfields in inline mode.
The odds row adapts its column count to the number of outcomes (1, 2 or 3), so a two-way market (e.g. an Asian-handicap line) fills the card width rather than leaving an empty third column.
The banners array
ts
type MultiBannerItem =
| {
id: string
type: 'sport'
backgroundImage?: string
ctaUrl?: string
// eventId mode:
eventId?: string | number
sportId?: number // default 1 — scopes the feed lookup
// inline mode (used when eventId is absent):
competition?: string
sportIcon?: string // icon URL
countryFlag?: string // flag URL
homeTeam?: string
awayTeam?: string
isLive?: boolean
startTime?: number // epoch ms (or use dateLabel/timeLabel)
dateLabel?: string // e.g. "Today"
timeLabel?: string // e.g. "14:30"
outcomes?: Array<{ id?: string; label: string; odds: number; bettingOfferId?: string; isAvailable?: boolean }>
}
| {
id: string
type: 'casino'
backgroundImage?: string
ctaUrl?: string
gameId?: string
category?: string
categoryIcon?: string // icon URL
title?: string
description?: string
ctaLabel?: string // CTA only renders when set
}
| {
id: string
type: 'info'
backgroundImage?: string
ctaUrl?: string
title?: string
description?: string
ctaLabel?: string
}Props / attributes
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
banners | banners | MultiBannerItem[] | [] | The banner list (JSON when set as an attribute). |
selectedOutcomes | selected-outcomes | string[] | [] | Two-way mirror of selected odds ids (sport cards). |
showArrows | show-arrows | boolean | true | Show the prev/next arrows. |
showDots | show-dots | boolean | true | Show pagination dots. |
centeredSlides | centered-slides | boolean | true | Centre the active slide. |
centeredSlidesBounds | centered-slides-bounds | boolean | true | With centeredSlides, pin the first slide to the left edge and the last to the right (only in-between slides centre) — the natural hero behaviour. |
loop | loop | boolean | true | Rewind to the start past the last slide (auto-off for one banner). |
enableSwipe | enable-swipe | boolean | true | Allow touch/drag swiping. |
autoplay | autoplay | boolean | false | Auto-advance on an interval. |
autoplayInterval | autoplay-interval | number | 5000 | Autoplay delay (ms). |
spaceBetween | space-between | number | 16 | Gap between slides (px). |
sportEventLimit | sport-event-limit | number | 50 | Max events pulled per sport feed for eventId lookups. |
locale | locale | string | 'en' | i18n locale. |
theme | theme | object | {} | --goma-* overrides. |
Events
All events bubble and are composed: true (cross the Shadow DOM boundary).
| Event | detail | Fired when |
|---|---|---|
goma:banner-select | { type, id, ctaUrl?, eventId?, gameId? } | Any banner is activated (CTA tap, or sport-card body tap). |
goma:event-navigate | { eventId, homeTeam, awayTeam, competitionName } | A sport card's body is tapped — drives <goma-game-details>. |
goma:outcome-select | { outcomeId, bettingOfferId, eventId, marketId, … } | An odds button on a sport card is selected (same shape as <goma-events-horizontal> — a betslip widget picks it up). |
ready | {} | Widget mounted. |
error | { message, code, component? } | Render error caught by the boundary. |
Cross-widget cooperation
- Drop a
<goma-betslip-floating>/<goma-betslip-sidebar>on the page and odds taps on sport banners flow into it via the shared betslip storage — same contract as<goma-events-horizontal>. - Drop a
<goma-game-details>and a sport-card body tap drives it throughgoma:event-navigate.
Embedding examples
Vanilla / CDN
html
<goma-multi-banners id="b"></goma-multi-banners>
<script type="module">
import '@gomagaming/multi-banners'
const el = document.getElementById('b')
el.config = { socketUrl: 'wss://…', ucsOperatorId: '2838', locale: 'en' }
el.banners = [
{ id: 's1', type: 'sport', eventId: '123456', sportId: 1, backgroundImage: 'https://…/match.jpg' },
{ id: 'c1', type: 'casino', backgroundImage: 'https://…/slot.jpg', category: 'Slots', title: 'Gates of Olympus', description: 'Spin to win', ctaLabel: 'Play', ctaUrl: 'https://…' },
{ id: 'i1', type: 'info', backgroundImage: 'https://…/promo.jpg', title: 'Welcome Bonus', description: 'Up to €100', ctaLabel: 'Claim', ctaUrl: 'https://…' },
]
el.addEventListener('goma:banner-select', (e) => console.log(e.detail))
</script>React
jsx
import '@gomagaming/multi-banners'
export function Banners({ banners, config }) {
return (
<goma-multi-banners
ref={(el) => {
if (!el) return
el.config = config
el.banners = banners
}}
/>
)
}Vue
vue
<script setup>
import '@gomagaming/multi-banners'
import { ref, watchEffect } from 'vue'
const el = ref(null)
const props = defineProps(['banners', 'config'])
watchEffect(() => {
if (!el.value) return
el.value.config = props.config
el.value.banners = props.banners
})
</script>
<template>
<goma-multi-banners ref="el" @goma:banner-select="$emit('select', $event.detail)" />
</template>Theming
All colours are --goma-* tokens (see the theming reference):
| Token | Used for |
|---|---|
--goma-backgroundPrimary | Section background behind the cards |
--goma-backgroundOdds | Odds-button background |
--goma-buttonBackgroundPrimary / --goma-buttonTextPrimary | CTA button |
--goma-highlightPrimary | Focus rings |
--goma-textPrimary | Text + active pagination dot |
--goma-textSecondary | Secondary text |
--goma-separatorLine | Inactive pagination dots |
The card background image is host-fed; a fixed dark gradient over it keeps text legible regardless of the image.
Source layout
| Layer | File |
|---|---|
| Element class | packages/multi-banners/src/MultiBannersElement.js |
| App wrapper | packages/multi-banners/src/MultiBannersApp.vue |
| Carousel | packages/multi-banners/src/components/MultiBanners/BannerCarousel.vue |
| Card shell + cards | packages/multi-banners/src/components/MultiBanners/{BannerCardShell,SportBannerCard,CasinoBannerCard,InfoBannerCard}.vue |
| Composables | packages/multi-banners/src/composables/{useBannerEvents,useSportBannerCard}.js |
| Multi-event subscription | packages/sports-domain/src/api/everymatrix/actions.js (createBannerEventsActions) |
| Per-widget CSS | packages/multi-banners/src/styles/carousel.css |
| Tests | packages/multi-banners/tests/{MultiBanners,playground-manifest}.test.js |