gatsby-core-theme 44.22.2 → 44.22.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## [44.22.4](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/compare/v44.22.3...v44.22.4) (2026-04-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add as props the transaltions ([28b263c](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/28b263c9b790482b35e8e3abab6cc16c9d94dc4e))
7
+ * add new mode for spotlight ([ff84c7f](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/ff84c7f124190ca5f7fee9dac749919b7cd61b8a))
8
+
9
+
10
+ * Merge branch 'EN-266-Spotlight' into 'master' ([3c9b8a5](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/3c9b8a599924ed5b0cb136c448d014b78ae8fdb8))
11
+
12
+ ## [44.22.3](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/compare/v44.22.2...v44.22.3) (2026-04-27)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * add unlimited popup items feature ([3f94e3c](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/3f94e3cea9b3df95f3a8f47e2634d532c767c8b8))
18
+ * fix test ([cc755db](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/cc755dbe11e633a0bad4c70690ac8df3482d6f55))
19
+
20
+
21
+ * Merge branch 'add-unlimited-popup-items-feature' into 'master' ([6da3990](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/6da3990039afc5afad4880d1fb6ab10d7ff3a459))
22
+
1
23
  ## [44.22.2](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/compare/v44.22.1...v44.22.2) (2026-04-24)
2
24
 
3
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gatsby-core-theme",
3
- "version": "44.22.2",
3
+ "version": "44.22.4",
4
4
  "description": "Gatsby Theme NPM Package",
5
5
  "author": "",
6
6
  "license": "ISC",
@@ -10,6 +10,7 @@ import Toplist from "~organisms/toplist";
10
10
  import AdminButton from "~atoms/admin/button";
11
11
  import Accordian from "~organisms/accordion";
12
12
  import Anchor from "~organisms/anchor/template-one";
13
+ import getSpotlightComponent from "~molecules/spotlights_v2";
13
14
 
14
15
  const Modules = ({ module, page, pageContext, modulePosition }) => {
15
16
  const { admin } = useContext(Context) || {};
@@ -63,52 +64,7 @@ const Modules = ({ module, page, pageContext, modulePosition }) => {
63
64
  case "comments":
64
65
  return lazy(() => import("~organisms/comments"));
65
66
  case "spotlights":
66
- if (module?.mode === "image_text") {
67
- if (module?.style === "template_two") {
68
- return lazy(() =>
69
- import("~molecules/spotlights_v2/image-text/template-two")
70
- );
71
- }
72
- if (module?.style === "template_three") {
73
- return lazy(() =>
74
- import("~molecules/spotlights_v2/image-text/template-three")
75
- );
76
- }
77
- if (module?.style === "template_four") {
78
- return lazy(() =>
79
- import("~molecules/spotlights_v2/image-text/template-four")
80
- );
81
- }
82
- if (module?.style === "template_five") {
83
- return lazy(() =>
84
- import("~molecules/spotlights_v2/image-text/template-five")
85
- );
86
- }
87
- if (module?.style === "template_six") {
88
- return lazy(() =>
89
- import("~molecules/spotlights_v2/image-text/template-six")
90
- );
91
- }
92
- return lazy(() =>
93
- import("~molecules/spotlights_v2/image-text/template-one")
94
- );
95
- }
96
- if (module?.mode === "icon") {
97
- return lazy(() =>
98
- import("~molecules/spotlights_v2/icon/template-one")
99
- );
100
- }
101
- if (module?.mode === "image") {
102
- if (module?.style === "template_two") {
103
- return lazy(() =>
104
- import("~molecules/spotlights_v2/image/template-two")
105
- );
106
- }
107
- return lazy(() =>
108
- import("~molecules/spotlights_v2/image/template-one")
109
- );
110
- }
111
- break;
67
+ return getSpotlightComponent(module);
112
68
  case "disquss":
113
69
  return lazy(() => import(`~atoms/disquss`));
114
70
  default:
@@ -0,0 +1,42 @@
1
+ import { lazy } from "react";
2
+
3
+ const getSpotlightComponent = (module) => {
4
+ const { mode, style } = module || {};
5
+
6
+ if (mode === "image_text") {
7
+ switch (style) {
8
+ case "template_two":
9
+ return lazy(() => import("~molecules/spotlights_v2/image-text/template-two"));
10
+ case "template_three":
11
+ return lazy(() => import("~molecules/spotlights_v2/image-text/template-three"));
12
+ case "template_four":
13
+ return lazy(() => import("~molecules/spotlights_v2/image-text/template-four"));
14
+ case "template_five":
15
+ return lazy(() => import("~molecules/spotlights_v2/image-text/template-five"));
16
+ case "template_six":
17
+ return lazy(() => import("~molecules/spotlights_v2/image-text/template-six"));
18
+ default:
19
+ return lazy(() => import("~molecules/spotlights_v2/image-text/template-one"));
20
+ }
21
+ }
22
+
23
+ if (mode === "icon") {
24
+ return lazy(() => import("~molecules/spotlights_v2/icon/template-one"));
25
+ }
26
+
27
+ if (mode === "image") {
28
+ return style === "template_two"
29
+ ? lazy(() => import("~molecules/spotlights_v2/image/template-two"))
30
+ : lazy(() => import("~molecules/spotlights_v2/image/template-one"));
31
+ }
32
+
33
+ if (mode === "sport_odds") {
34
+ return style === "template_two"
35
+ ? lazy(() => import("~molecules/spotlights_v2/sport-odds/template-two"))
36
+ : lazy(() => import("~molecules/spotlights_v2/sport-odds/template-one"));
37
+ }
38
+
39
+ return null;
40
+ };
41
+
42
+ export default getSpotlightComponent;
@@ -0,0 +1,153 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import keygen from "~helpers/keygen";
4
+ import LazyImage from "~hooks/lazy-image";
5
+ import Button from "../../../../atoms/button/operator-cta";
6
+ import Tnc from '../../../tnc'
7
+ import useTranslate from "~hooks/useTranslate/useTranslate";
8
+ import FunIcon from "../../../../../images/icons/funIcon";
9
+ import styles from "./template-one.module.scss";
10
+
11
+ const DEFAULT_CTA_TRANSLATE = {
12
+ active: { translationKey: 'play_now', defaultValue: 'Visit' },
13
+ not_recommended: {
14
+ translationKey: 'not_recommended',
15
+ defaultValue: 'Not Recommended'
16
+ },
17
+ coming_soon: {
18
+ translationKey: 'coming_soon',
19
+ defaultValue: 'Soon Available'
20
+ },
21
+ inactive: {
22
+ translationKey: 'inactive',
23
+ defaultValue: 'Not Accepting New Players'
24
+ },
25
+ blacklisted: { translationKey: 'blacklisted', defaultValue: 'Blacklisted' }
26
+ };
27
+
28
+ export default function TemplateOne({
29
+ module,
30
+ showOperatorHeader = false,
31
+ ctaTranslate = DEFAULT_CTA_TRANSLATE,
32
+ }) {
33
+ const items = module?.items;
34
+ const ctaText = module?.link_text;
35
+ const date = useTranslate('date', 'Date:');
36
+ const funbetLabel = useTranslate('funbet', 'Funbet');
37
+
38
+ return (
39
+ <div className={styles.sportOdds}>
40
+ {(items || []).map((item) => {
41
+ const operatorName = item?.relation?.name;
42
+ const operatorLogo = item?.relation?.logo;
43
+
44
+ return (
45
+ <div key={keygen()} className={styles.card}>
46
+ <div className={styles.header}>
47
+ {
48
+ showOperatorHeader ?
49
+ <>
50
+ {operatorLogo?.url ? (
51
+ <LazyImage
52
+ src={operatorLogo.url}
53
+ width={20}
54
+ height={20}
55
+ alt={operatorLogo.alt || operatorName}
56
+ />
57
+ ) : (
58
+ <span className={styles.operatorIcon} aria-hidden="true" />
59
+ )}
60
+ {operatorName && <span>{operatorName}</span>}
61
+ </> : <>
62
+ <FunIcon />
63
+ <span>{funbetLabel}</span>
64
+ </>
65
+ }
66
+ </div>
67
+
68
+ <div className={styles.body}>
69
+ <div className={styles.main}>
70
+ <div className={styles.info}>
71
+ {item?.main_title && (
72
+ <p className={styles.title}>{item.main_title}</p>
73
+ )}
74
+ {item?.secondary_title && (
75
+ <p className={styles.time}>{date}<span>{item.secondary_title}</span></p>
76
+ )}
77
+ </div>
78
+
79
+ <div className={styles.action}>
80
+ {(item?.odds_text || item?.odds_value) && (
81
+ <div className={styles.odds}>
82
+ {operatorLogo?.url && (
83
+ <LazyImage
84
+ src={operatorLogo.url}
85
+ width={48}
86
+ height={48}
87
+ alt={operatorLogo.alt || operatorName}
88
+ />
89
+ )}
90
+ <span>
91
+ {item?.odds_text}
92
+ {item?.odds_text && item?.odds_value ? ": " : ""}
93
+ {item?.odds_value}
94
+ </span>
95
+ </div>
96
+ )}
97
+
98
+ <Button
99
+ operator={item?.relation}
100
+ buttonType="primary"
101
+ buttonSize="m"
102
+ btnText={ctaText}
103
+ isInternalLink={false}
104
+ targetBlank
105
+ moduleName={module?.name}
106
+ tracker={module?.bonus?.tracking_link_name || 'main'}
107
+ translationsObj={ctaTranslate}
108
+ />
109
+ </div>
110
+ </div>
111
+
112
+ <Tnc operator={item?.relation} tracker={item?.bonus?.tracking_link_name} />
113
+ </div>
114
+ </div>
115
+ );
116
+ })}
117
+ </div>
118
+ );
119
+ }
120
+
121
+ TemplateOne.propTypes = {
122
+ module: PropTypes.shape({
123
+ items: PropTypes.arrayOf(PropTypes.shape({})),
124
+ link_text: PropTypes.string,
125
+ name: PropTypes.string,
126
+ bonus: PropTypes.shape({
127
+ tracking_link_name: PropTypes.string,
128
+ }),
129
+ }),
130
+ showOperatorHeader: PropTypes.bool,
131
+ ctaTranslate: PropTypes.shape({
132
+ active: PropTypes.shape({
133
+ translationKey: PropTypes.string,
134
+ defaultValue: PropTypes.string,
135
+ }),
136
+ not_recommended: PropTypes.shape({
137
+ translationKey: PropTypes.string,
138
+ defaultValue: PropTypes.string,
139
+ }),
140
+ coming_soon: PropTypes.shape({
141
+ translationKey: PropTypes.string,
142
+ defaultValue: PropTypes.string,
143
+ }),
144
+ inactive: PropTypes.shape({
145
+ translationKey: PropTypes.string,
146
+ defaultValue: PropTypes.string,
147
+ }),
148
+ blacklisted: PropTypes.shape({
149
+ translationKey: PropTypes.string,
150
+ defaultValue: PropTypes.string,
151
+ }),
152
+ }),
153
+ };
@@ -0,0 +1,121 @@
1
+ .sportOdds {
2
+ max-width: var(--main-container-max);
3
+ margin: 0 auto;
4
+
5
+ @include flex-direction(column);
6
+
7
+ gap: 1.6rem;
8
+ }
9
+
10
+ .card {
11
+ overflow: hidden;
12
+ background: var(--spotlight-sport-odds-card-bg, #F4F4F5);
13
+ border: 1px solid var(--spotlight-sport-odds-card-border, #D4D4D8);
14
+ }
15
+
16
+ .header {
17
+ @include flex-direction(row);
18
+ @include flex-align(center, start);
19
+
20
+ padding: .8rem 1.6rem;
21
+ background: var(--spotlight-sport-odds-header-bg, #3F3F46);
22
+ color: var(--spotlight-sport-odds-header-text, #fff);
23
+ font-size: 1.4rem;
24
+ font-weight: 700;
25
+
26
+ img, svg{
27
+ margin-right: .8rem;
28
+ }
29
+ }
30
+
31
+ .operatorIcon {
32
+ width: 2rem;
33
+ height: 2rem;
34
+ border-radius: 100%;
35
+ background: var(--spotlight-sport-odds-operator-icon-bg, #f4a62a);
36
+ }
37
+
38
+ .body {
39
+ padding: 1.6rem;
40
+
41
+ @include flex-direction(column);
42
+
43
+ gap: 1rem;
44
+ }
45
+
46
+ .main {
47
+ @include flex-direction(column);
48
+
49
+ gap: .8rem;
50
+
51
+ @include min(tablet) {
52
+ @include flex-direction(row);
53
+ @include flex-align(center, space-between);
54
+ }
55
+ }
56
+
57
+ .info {
58
+ @include flex-direction(column);
59
+
60
+ gap: 0.4rem;
61
+ }
62
+
63
+ .title {
64
+ color: var(--spotlight-sport-odds-title, #18181B);
65
+ font-size: 1.8rem;
66
+ font-weight: 700;
67
+ line-height: 2.7rem;
68
+ margin: 0;
69
+ }
70
+
71
+ .time {
72
+ color: var(--spotlight-sport-odds-text, #5c5c5c);
73
+ font-size: 1.4rem;
74
+ line-height: 2.1rem;
75
+ gap: .5rem;
76
+
77
+ @include flex-direction(row);
78
+ @include flex-align(center, start);
79
+
80
+ >span{
81
+ font-weight: 700;
82
+ }
83
+ }
84
+
85
+ .action {
86
+ @include flex-direction(row);
87
+ @include flex-align(center, start);
88
+
89
+ gap: 1.2rem;
90
+
91
+ >a{
92
+ width: 100%;
93
+ }
94
+ }
95
+
96
+ .odds {
97
+ @include flex-direction(row);
98
+ @include flex-align(center, start);
99
+
100
+ gap: 0.8rem;
101
+ border-radius: 0.6rem;
102
+ background: var(--spotlight-sport-odds-odds-bg, #FFF);
103
+ color: var(--spotlight-sport-odds-odds-text, #1b1b1c);
104
+ border: 1px solid var(--spotlight-sport-odds-card-bg, #E4E4E7) ;
105
+ font-size: 1.3rem;
106
+ font-weight: 600;
107
+ min-width: 14.3rem;
108
+ min-height: 4.8rem;
109
+ width: 100%;
110
+ }
111
+
112
+ .oddsPartner {
113
+ padding: 0.4rem 0.6rem;
114
+ border-radius: 0.4rem;
115
+ background: var(--spotlight-sport-odds-partner-bg, #0a8537);
116
+ color: var(--spotlight-sport-odds-partner-text, #fff);
117
+ font-size: 1.1rem;
118
+ font-weight: 700;
119
+ }
120
+
121
+
@@ -0,0 +1,84 @@
1
+ /* eslint-disable import/no-extraneous-dependencies */
2
+ import React from 'react';
3
+ import {
4
+ Title,
5
+ Description,
6
+ Primary,
7
+ PRIMARY_STORY,
8
+ ArgsTable,
9
+ } from '@storybook/addon-docs/blocks';
10
+ import TemplateOne from '.';
11
+
12
+ export default {
13
+ title: 'Theme/Modules/Spotlight/Sport Odds/Template One',
14
+ component: TemplateOne,
15
+ argTypes: {},
16
+ parameters: {
17
+ docs: {
18
+ description: {
19
+ component: 'Sport odds spotlight that renders a list of upcoming events with operator CTAs and tracker links.',
20
+ },
21
+ page: () => (
22
+ <>
23
+ <Title />
24
+ <Description />
25
+ <Primary />
26
+ <ArgsTable story={PRIMARY_STORY} />
27
+ </>
28
+ ),
29
+ },
30
+ },
31
+ };
32
+
33
+ const Template = (args) => <TemplateOne {...args} />;
34
+
35
+ export const Default = Template.bind({});
36
+ Default.args = {
37
+ module: {
38
+ mode: 'sport_odds',
39
+ style: 'template_one',
40
+ link_text: 'Play Now',
41
+ name: 'sport-odds-spotlight',
42
+ bonus: { tracking_link_name: 'main' },
43
+ items: [
44
+ {
45
+ main_title: 'Real Madrid – Barcelona',
46
+ secondary_title: '02/10/2026 18:45',
47
+ odds_text: 'Odds',
48
+ odds_value: '2.10',
49
+ relation: {
50
+ name: 'Funbet',
51
+ status: 'active',
52
+ links: { main: 'https://funbet.com' },
53
+ logo: {
54
+ url: 'https://assets-srv.s3.eu-west-1.amazonaws.com/1680163403/betibet-logo.png',
55
+ alt: 'Funbet logo',
56
+ },
57
+ },
58
+ bonus: { tracking_link_name: 'main' },
59
+ },
60
+ {
61
+ main_title: 'Manchester United – Chelsea',
62
+ secondary_title: '03/10/2026 20:00',
63
+ odds_text: 'Odds',
64
+ odds_value: '1.85',
65
+ relation: {
66
+ name: 'Betway',
67
+ status: 'active',
68
+ links: { main: 'https://betway.com' },
69
+ },
70
+ bonus: { tracking_link_name: 'main' },
71
+ },
72
+ {
73
+ main_title: 'PSG – Bayern Munich',
74
+ secondary_title: '04/10/2026 21:00',
75
+ odds_text: 'Odds',
76
+ odds_value: '3.40',
77
+ relation: {
78
+ name: 'Bet365',
79
+ status: 'inactive',
80
+ },
81
+ },
82
+ ],
83
+ },
84
+ };
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+
5
+ import TemplateOne from '.';
6
+
7
+ const buildModule = () => ({
8
+ mode: 'sport_odds',
9
+ style: 'template_one',
10
+ link_text: 'Play now',
11
+ name: 'sport-odds-module',
12
+ bonus: { tracking_link_name: 'main' },
13
+ items: [
14
+ {
15
+ main_title: 'Real Madrid – Barcelona',
16
+ secondary_title: '02/10/2026 18:45',
17
+ odds_text: 'Odds',
18
+ odds_value: '2.10',
19
+ relation: {
20
+ name: 'Funbet',
21
+ status: 'active',
22
+ links: { main: 'https://funbet.com' },
23
+ logo: {
24
+ url: 'https://example.com/logo.png',
25
+ alt: 'Funbet logo',
26
+ },
27
+ },
28
+ bonus: { tracking_link_name: 'main' },
29
+ },
30
+ {
31
+ main_title: 'Manchester United – Chelsea',
32
+ relation: { name: 'Betway', status: 'inactive' },
33
+ },
34
+ ],
35
+ });
36
+
37
+ describe('Sport Odds TemplateOne Component', () => {
38
+ test('renders all items from module', () => {
39
+ const { container, getByText } = render(<TemplateOne module={buildModule()} />);
40
+ expect(container.querySelectorAll('div').length).toBeGreaterThan(0);
41
+ expect(getByText('Real Madrid – Barcelona')).toBeInTheDocument();
42
+ expect(getByText('Manchester United – Chelsea')).toBeInTheDocument();
43
+ });
44
+
45
+ test('renders odds text and value when provided', () => {
46
+ const { getByText } = render(<TemplateOne module={buildModule()} />);
47
+ expect(getByText(/Odds/)).toBeInTheDocument();
48
+ expect(getByText(/2\.10/)).toBeInTheDocument();
49
+ });
50
+
51
+ test('falls back to main tracker when bonus is missing', () => {
52
+ const module = buildModule();
53
+ delete module.bonus;
54
+ const { container } = render(<TemplateOne module={module} />);
55
+ expect(container.querySelectorAll('div').length).toBeGreaterThan(0);
56
+ });
57
+ });
@@ -0,0 +1,162 @@
1
+ import React, { useRef } from "react";
2
+ import PropTypes from "prop-types";
3
+ import keygen from "~helpers/keygen";
4
+ import LazyImage from "~hooks/lazy-image";
5
+ import Button from "../../../../atoms/button/operator-cta";
6
+ import Tnc from "../../../tnc";
7
+ import ScrollX from "~hooks/scroll-x";
8
+ import useTranslate from "~hooks/useTranslate/useTranslate";
9
+ import FunIcon from "../../../../../images/icons/funIcon";
10
+ import styles from "./template-two.module.scss";
11
+
12
+ const DEFAULT_CTA_TRANSLATE = {
13
+ active: { translationKey: 'play_now', defaultValue: 'Visit' },
14
+ not_recommended: {
15
+ translationKey: 'not_recommended',
16
+ defaultValue: 'Not Recommended'
17
+ },
18
+ coming_soon: {
19
+ translationKey: 'coming_soon',
20
+ defaultValue: 'Soon Available'
21
+ },
22
+ inactive: {
23
+ translationKey: 'inactive',
24
+ defaultValue: 'Not Accepting New Players'
25
+ },
26
+ blacklisted: { translationKey: 'blacklisted', defaultValue: 'Blacklisted' }
27
+ };
28
+
29
+ export default function TemplateTwo({
30
+ module,
31
+ showOperatorHeader = false,
32
+ ctaTranslate = DEFAULT_CTA_TRANSLATE,
33
+ }) {
34
+ const container = useRef(null);
35
+ const items = module?.items;
36
+ const ctaText = module?.link_text;
37
+ const date = useTranslate('date', 'Date:');
38
+ const funbetLabel = useTranslate('funbet', 'Funbet');
39
+
40
+ return (
41
+ <ScrollX refTag={container} scroll>
42
+ <div
43
+ ref={container}
44
+ className={styles.sportOdds}
45
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
46
+ tabIndex={0}
47
+ >
48
+ {(items || []).map((item) => {
49
+ const operatorName = item?.relation?.name;
50
+ const operatorLogo = item?.relation?.logo;
51
+
52
+ return (
53
+ <div key={keygen()} className={styles.card}>
54
+ <div className={styles.header}>
55
+ {
56
+ showOperatorHeader ?
57
+ <>
58
+ {operatorLogo?.url ? (
59
+ <LazyImage
60
+ src={operatorLogo.url}
61
+ width={20}
62
+ height={20}
63
+ alt={operatorLogo.alt || operatorName}
64
+ />
65
+ ) : (
66
+ <span className={styles.operatorIcon} aria-hidden="true" />
67
+ )}
68
+ {operatorName && <span>{operatorName}</span>}
69
+ </> : <>
70
+ <FunIcon />
71
+ <span>{funbetLabel}</span>
72
+ </>
73
+ }
74
+ </div>
75
+
76
+ <div className={styles.body}>
77
+ <div className={styles.main}>
78
+ <div className={styles.info}>
79
+ {item?.main_title && (
80
+ <p className={styles.title}>{item.main_title}</p>
81
+ )}
82
+ {item?.secondary_title && (
83
+ <p className={styles.time}>{date}<span>{item.secondary_title}</span></p>
84
+ )}
85
+ </div>
86
+
87
+ <div className={styles.action}>
88
+ {(item?.odds_text || item?.odds_value) && (
89
+ <div className={styles.odds}>
90
+ {operatorLogo?.url && (
91
+ <LazyImage
92
+ src={operatorLogo.url}
93
+ width={48}
94
+ height={48}
95
+ alt={operatorLogo.alt || operatorName}
96
+ />
97
+ )}
98
+ <span>
99
+ {item?.odds_text}
100
+ {item?.odds_text && item?.odds_value ? ": " : ""}
101
+ {item?.odds_value}
102
+ </span>
103
+ </div>
104
+ )}
105
+
106
+ <Button
107
+ operator={item?.relation}
108
+ buttonType="primary"
109
+ buttonSize="m"
110
+ btnText={ctaText}
111
+ isInternalLink={false}
112
+ targetBlank
113
+ moduleName={module?.name}
114
+ tracker={module?.bonus?.tracking_link_name || 'main'}
115
+ translationsObj={ctaTranslate}
116
+ />
117
+ </div>
118
+ </div>
119
+
120
+ <Tnc operator={item?.relation} tracker={item?.bonus?.tracking_link_name} />
121
+ </div>
122
+ </div>
123
+ );
124
+ })}
125
+ </div>
126
+ </ScrollX>
127
+ );
128
+ }
129
+
130
+ TemplateTwo.propTypes = {
131
+ module: PropTypes.shape({
132
+ items: PropTypes.arrayOf(PropTypes.shape({})),
133
+ link_text: PropTypes.string,
134
+ name: PropTypes.string,
135
+ bonus: PropTypes.shape({
136
+ tracking_link_name: PropTypes.string,
137
+ }),
138
+ }),
139
+ showOperatorHeader: PropTypes.bool,
140
+ ctaTranslate: PropTypes.shape({
141
+ active: PropTypes.shape({
142
+ translationKey: PropTypes.string,
143
+ defaultValue: PropTypes.string,
144
+ }),
145
+ not_recommended: PropTypes.shape({
146
+ translationKey: PropTypes.string,
147
+ defaultValue: PropTypes.string,
148
+ }),
149
+ coming_soon: PropTypes.shape({
150
+ translationKey: PropTypes.string,
151
+ defaultValue: PropTypes.string,
152
+ }),
153
+ inactive: PropTypes.shape({
154
+ translationKey: PropTypes.string,
155
+ defaultValue: PropTypes.string,
156
+ }),
157
+ blacklisted: PropTypes.shape({
158
+ translationKey: PropTypes.string,
159
+ defaultValue: PropTypes.string,
160
+ }),
161
+ }),
162
+ };
@@ -0,0 +1,117 @@
1
+ .sportOdds {
2
+ max-width: var(--main-container-max);
3
+ margin: 0 auto;
4
+
5
+ @include flex-direction(row);
6
+ @include flex-align(stretch, start);
7
+
8
+ gap: 1.6rem;
9
+ overflow-x: auto;
10
+ scroll-snap-type: x mandatory;
11
+
12
+ &::-webkit-scrollbar {
13
+ display: none;
14
+ }
15
+ }
16
+
17
+ .card {
18
+ flex: 0 0 auto;
19
+ width: 28rem;
20
+ scroll-snap-align: start;
21
+ overflow: hidden;
22
+ background: var(--spotlight-sport-odds-card-bg, #F4F4F5);
23
+ border: 1px solid var(--spotlight-sport-odds-card-border, #D4D4D8);
24
+ box-shadow: 0 4px 6px -2px rgb(27 27 28 / 2%),
25
+ 0 12px 16px -4px rgb(27 27 28 / 5%);
26
+
27
+ @include flex-direction(column);
28
+ }
29
+
30
+ .header {
31
+ @include flex-direction(row);
32
+ @include flex-align(center, start);
33
+
34
+ gap: 0.8rem;
35
+ padding: 1.2rem 1.6rem;
36
+ background: var(--spotlight-sport-odds-header-bg, #3F3F46);
37
+ color: var(--spotlight-sport-odds-header-text, #fff);
38
+ font-weight: 700;
39
+ }
40
+
41
+ .operatorIcon {
42
+ width: 2rem;
43
+ height: 2rem;
44
+ border-radius: 100%;
45
+ background: var(--spotlight-sport-odds-operator-icon-bg, #f4a62a);
46
+ }
47
+
48
+ .body {
49
+ padding: 1.6rem;
50
+
51
+ @include flex-direction(column);
52
+
53
+ gap: 1.2rem;
54
+ height: 100%;
55
+ }
56
+
57
+ .main {
58
+ @include flex-direction(column);
59
+
60
+ gap: 1.2rem;
61
+ }
62
+
63
+ .info {
64
+ @include flex-direction(column);
65
+
66
+ gap: 0.4rem;
67
+ }
68
+
69
+ .action {
70
+ @include flex-direction(column);
71
+
72
+ gap: 0.8rem;
73
+
74
+ > a {
75
+ width: 100%;
76
+ }
77
+ }
78
+
79
+ .title {
80
+ color: var(--spotlight-sport-odds-title, #18181B);
81
+ font-size: 1.6rem;
82
+ font-weight: 700;
83
+ line-height: 2.2rem;
84
+ margin: 0;
85
+ }
86
+
87
+ .time {
88
+ color: var(--spotlight-sport-odds-text, #5c5c5c);
89
+ font-size: 1.3rem;
90
+ }
91
+
92
+ .odds {
93
+ @include flex-direction(row);
94
+ @include flex-align(center, start);
95
+
96
+ gap: 0.8rem;
97
+ background: var(--spotlight-sport-odds-odds-bg, #FFF);
98
+ border: 1px solid var(--spotlight-sport-odds-card-bg, #E4E4E7);
99
+ color: var(--spotlight-sport-odds-odds-text, #1b1b1c);
100
+ font-weight: 600;
101
+ height: 4.8rem;
102
+ border-radius: .2rem;
103
+ box-sizing: border-box;
104
+
105
+ >img{
106
+ border-radius: .2rem;
107
+ }
108
+ }
109
+
110
+ .oddsPartner {
111
+ padding: 0.4rem 0.8rem;
112
+ border-radius: 0.4rem;
113
+ background: var(--spotlight-sport-odds-partner-bg, #0a8537);
114
+ color: var(--spotlight-sport-odds-partner-text, #fff);
115
+ font-size: 1.2rem;
116
+ font-weight: 700;
117
+ }
@@ -0,0 +1,80 @@
1
+ /* eslint-disable import/no-extraneous-dependencies */
2
+ import React from 'react';
3
+ import {
4
+ Title,
5
+ Description,
6
+ Primary,
7
+ PRIMARY_STORY,
8
+ ArgsTable,
9
+ } from '@storybook/addon-docs/blocks';
10
+ import TemplateTwo from '.';
11
+
12
+ export default {
13
+ title: 'Theme/Modules/Spotlight/Sport Odds/Template Two',
14
+ component: TemplateTwo,
15
+ argTypes: {},
16
+ parameters: {
17
+ docs: {
18
+ description: {
19
+ component: 'Horizontally scrollable sport odds spotlight rendering a list of upcoming events.',
20
+ },
21
+ page: () => (
22
+ <>
23
+ <Title />
24
+ <Description />
25
+ <Primary />
26
+ <ArgsTable story={PRIMARY_STORY} />
27
+ </>
28
+ ),
29
+ },
30
+ },
31
+ };
32
+
33
+ const Template = (args) => <TemplateTwo {...args} />;
34
+
35
+ export const Default = Template.bind({});
36
+ Default.args = {
37
+ module: {
38
+ mode: 'sport_odds',
39
+ style: 'template_two',
40
+ link_text: 'Bet Now',
41
+ name: 'sport-odds-spotlight',
42
+ bonus: { tracking_link_name: 'main' },
43
+ items: [
44
+ {
45
+ main_title: 'Real Madrid – Barcelona: Mais de 2,5 gols',
46
+ secondary_title: '02/10/2026 18:45',
47
+ odds_text: 'Quota',
48
+ odds_value: '12',
49
+ relation: {
50
+ name: 'Funbet',
51
+ status: 'active',
52
+ links: { main: 'https://funbet.com' },
53
+ },
54
+ bonus: { tracking_link_name: 'main' },
55
+ },
56
+ {
57
+ main_title: 'Manchester United – Chelsea: Resultado Exato',
58
+ secondary_title: '03/10/2026 20:00',
59
+ odds_text: 'Quota',
60
+ odds_value: '8',
61
+ relation: {
62
+ name: 'Betway',
63
+ status: 'active',
64
+ links: { main: 'https://betway.com' },
65
+ },
66
+ bonus: { tracking_link_name: 'main' },
67
+ },
68
+ {
69
+ main_title: 'PSG – Bayern Munich: Ambas Marcam',
70
+ secondary_title: '04/10/2026 21:00',
71
+ odds_text: 'Quota',
72
+ odds_value: '1.95',
73
+ relation: {
74
+ name: 'Bet365',
75
+ status: 'inactive',
76
+ },
77
+ },
78
+ ],
79
+ },
80
+ };
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+
5
+ import TemplateTwo from '.';
6
+
7
+ const buildModule = () => ({
8
+ mode: 'sport_odds',
9
+ style: 'template_two',
10
+ link_text: 'Bet now',
11
+ name: 'sport-odds-module',
12
+ bonus: { tracking_link_name: 'main' },
13
+ items: [
14
+ {
15
+ main_title: 'Real Madrid – Barcelona: Mais de 2,5 gols',
16
+ secondary_title: 'Início: 02/10/2026 18:45',
17
+ odds_text: 'Quota',
18
+ odds_value: '12',
19
+ relation: {
20
+ name: 'Funbet',
21
+ status: 'active',
22
+ links: { main: 'https://funbet.com' },
23
+ logo: {
24
+ url: 'https://example.com/logo.png',
25
+ alt: 'Funbet logo',
26
+ },
27
+ },
28
+ bonus: { tracking_link_name: 'main' },
29
+ },
30
+ {
31
+ main_title: 'Manchester United – Chelsea',
32
+ relation: { name: 'Betway', status: 'inactive' },
33
+ },
34
+ ],
35
+ });
36
+
37
+ describe('Sport Odds TemplateTwo Component', () => {
38
+ test('renders all items from module', () => {
39
+ const { container, getByText } = render(<TemplateTwo module={buildModule()} />);
40
+ expect(container.querySelectorAll('div').length).toBeGreaterThan(0);
41
+ expect(getByText('Real Madrid – Barcelona: Mais de 2,5 gols')).toBeInTheDocument();
42
+ expect(getByText('Manchester United – Chelsea')).toBeInTheDocument();
43
+ });
44
+
45
+ test('renders odds text and value when provided', () => {
46
+ const { getByText } = render(<TemplateTwo module={buildModule()} />);
47
+ expect(getByText(/Quota/)).toBeInTheDocument();
48
+ expect(getByText(/12/)).toBeInTheDocument();
49
+ });
50
+
51
+ test('falls back to main tracker when bonus is missing', () => {
52
+ const module = buildModule();
53
+ delete module.bonus;
54
+ const { container } = render(<TemplateTwo module={module} />);
55
+ expect(container.querySelectorAll('div').length).toBeGreaterThan(0);
56
+ });
57
+ });
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ export default function FunIcon({ width = 24, height = 24 }) {
5
+ return (
6
+ <svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 24 24" fill="none">
7
+ <path d="M4 0.5H20C21.933 0.5 23.5 2.067 23.5 4V20C23.5 21.933 21.933 23.5 20 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4C0.5 2.067 2.067 0.5 4 0.5Z" fill="white" />
8
+ <path d="M4 0.5H20C21.933 0.5 23.5 2.067 23.5 4V20C23.5 21.933 21.933 23.5 20 23.5H4C2.067 23.5 0.5 21.933 0.5 20V4C0.5 2.067 2.067 0.5 4 0.5Z" stroke="#FACC15" />
9
+ <g clipPath="url(#clip0_7697_529)">
10
+ <path d="M17.9163 12.0004C17.9163 8.73279 15.2678 6.08355 12.0003 6.08337C8.73262 6.08337 6.08331 8.73268 6.08331 12.0004C6.08349 15.2679 8.73273 17.9164 12.0003 17.9164C15.2677 17.9162 17.9161 15.2678 17.9163 12.0004ZM14.1155 12.8246C14.3732 12.5449 14.8058 12.5007 15.1165 12.7338C15.4478 12.9823 15.5154 13.4522 15.2669 13.7836L14.6663 13.3334C15.2157 13.7454 15.2622 13.7804 15.2659 13.7836V13.7845L15.265 13.7855C15.2644 13.7863 15.2637 13.7875 15.263 13.7885C15.2616 13.7903 15.26 13.7928 15.2581 13.7953C15.2542 13.8003 15.2493 13.8065 15.2435 13.8138C15.2315 13.8289 15.2147 13.8485 15.1946 13.8724C15.1545 13.9203 15.098 13.9854 15.0257 14.0609C14.8816 14.2112 14.6716 14.4091 14.3997 14.6068C13.8586 15.0003 13.0401 15.4163 12.0003 15.4164C10.9605 15.4164 10.1421 15.0003 9.60089 14.6068C9.3289 14.409 9.11802 14.2113 8.97394 14.0609C8.90159 13.9854 8.84515 13.9203 8.80499 13.8724C8.78503 13.8486 8.76906 13.8289 8.75714 13.8138C8.75135 13.8065 8.74636 13.8003 8.74249 13.7953C8.74055 13.7928 8.73807 13.7903 8.73663 13.7885C8.73591 13.7875 8.73528 13.7863 8.73468 13.7855L8.7337 13.7845V13.7836C8.48525 13.4523 8.55202 12.9824 8.88312 12.7338C9.1938 12.5007 9.62646 12.545 9.88409 12.8246L9.93292 12.8832C9.93647 12.8877 9.94406 12.8962 9.95441 12.9086C9.97546 12.9336 10.0097 12.9735 10.0569 13.0228C10.1524 13.1225 10.2964 13.2584 10.4827 13.3939C10.8582 13.667 11.3737 13.9164 12.0003 13.9164C12.6266 13.9163 13.1415 13.6669 13.5169 13.3939C13.7031 13.2585 13.8472 13.1225 13.9427 13.0228C13.9901 12.9733 14.0252 12.9336 14.0462 12.9086C14.0564 12.8964 14.0631 12.8877 14.0667 12.8832L14.0677 12.8812L14.1155 12.8246ZM10.0062 9.25037C10.4204 9.25037 10.7562 9.58615 10.7562 10.0004C10.756 10.4144 10.4203 10.7504 10.0062 10.7504H10.0003C9.5862 10.7504 9.25048 10.4144 9.25031 10.0004C9.25031 9.58615 9.58609 9.25037 10.0003 9.25037H10.0062ZM14.0062 9.25037C14.4204 9.25037 14.7562 9.58615 14.7562 10.0004C14.756 10.4144 14.4203 10.7504 14.0062 10.7504H14.0003C13.5862 10.7504 13.2505 10.4144 13.2503 10.0004C13.2503 9.58615 13.5861 9.25037 14.0003 9.25037H14.0062ZM19.4163 12.0004C19.4161 16.0962 16.0962 19.4162 12.0003 19.4164C7.9043 19.4164 4.58349 16.0963 4.58331 12.0004C4.58331 7.90425 7.90419 4.58337 12.0003 4.58337C16.0963 4.58355 19.4163 7.90436 19.4163 12.0004Z" fill="#27272A" />
11
+ </g>
12
+ <defs>
13
+ <clipPath id="clip0_7697_529">
14
+ <rect width="16" height="16" fill="white" transform="translate(4 4)" />
15
+ </clipPath>
16
+ </defs>
17
+ </svg>
18
+ );
19
+ }
20
+
21
+ FunIcon.propTypes = {
22
+ width: PropTypes.number,
23
+ height: PropTypes.number,
24
+ };
@@ -142,6 +142,7 @@ export function clean(object) {
142
142
 
143
143
  export function removeUnwantedSections(obj, pageType, ribbonsData) {
144
144
  const limit = process.env.RECOMMENDED_CASINOS_NUMBER || 3;
145
+ const unlimitedPopupItems = process.env.UNLIMITED_POPUP_ITEMS === "true";
145
146
  const marketSection = {
146
147
  games: ["post_main_games", "pre_main_games"],
147
148
  operator: [
@@ -181,9 +182,13 @@ export function removeUnwantedSections(obj, pageType, ribbonsData) {
181
182
  (key === "recommended_casinos" || key === "popup") &&
182
183
  Array.isArray(acc[key]?.modules?.[0]?.items?.[0]?.items)
183
184
  ) {
184
- acc[key].modules[0].items[0].items = acc[
185
- key
186
- ].modules[0].items[0].items.slice(0, limit);
185
+ const shouldLimitItems = key !== "popup" || !unlimitedPopupItems;
186
+
187
+ if (shouldLimitItems) {
188
+ acc[key].modules[0].items[0].items = acc[
189
+ key
190
+ ].modules[0].items[0].items.slice(0, limit);
191
+ }
187
192
  const { items } = acc[key].modules[0].items[0];
188
193
 
189
194
  // Connect ribbons ID to label
@@ -135,6 +135,72 @@ describe("Common Helper", () => {
135
135
  });
136
136
  });
137
137
 
138
+ test("limits popup and recommended_casinos by default", () => {
139
+ const previousLimit = process.env.RECOMMENDED_CASINOS_NUMBER;
140
+ const previousUnlimitedUpper = process.env.UNLIMITED_POPUP_ITEMS;
141
+ process.env.RECOMMENDED_CASINOS_NUMBER = "2";
142
+ delete process.env.UNLIMITED_POPUP_ITEMS;
143
+
144
+ const sections = {
145
+ popup: {
146
+ modules: [{
147
+ items: [{
148
+ items: [{ id: 1 }, { id: 2 }, { id: 3 }],
149
+ }],
150
+ }],
151
+ },
152
+ recommended_casinos: {
153
+ modules: [{
154
+ items: [{
155
+ items: [{ id: 11 }, { id: 12 }, { id: 13 }],
156
+ }],
157
+ }],
158
+ },
159
+ };
160
+
161
+ const result = removeUnwantedSections(sections, "operator", {});
162
+ expect(result.popup.modules[0].items[0].items).toHaveLength(2);
163
+ expect(result.recommended_casinos.modules[0].items[0].items).toHaveLength(2);
164
+
165
+ if (previousLimit === undefined) delete process.env.RECOMMENDED_CASINOS_NUMBER;
166
+ else process.env.RECOMMENDED_CASINOS_NUMBER = previousLimit;
167
+ if (previousUnlimitedUpper === undefined) delete process.env.UNLIMITED_POPUP_ITEMS;
168
+ else process.env.UNLIMITED_POPUP_ITEMS = previousUnlimitedUpper;
169
+ });
170
+
171
+ test("does not limit popup when UNLIMITED_POPUP_ITEMS is true", () => {
172
+ const previousLimit = process.env.RECOMMENDED_CASINOS_NUMBER;
173
+ const previousUnlimitedUpper = process.env.UNLIMITED_POPUP_ITEMS;
174
+ process.env.RECOMMENDED_CASINOS_NUMBER = "2";
175
+ process.env.UNLIMITED_POPUP_ITEMS = "true";
176
+
177
+ const sections = {
178
+ popup: {
179
+ modules: [{
180
+ items: [{
181
+ items: [{ id: 1 }, { id: 2 }, { id: 3 }],
182
+ }],
183
+ }],
184
+ },
185
+ recommended_casinos: {
186
+ modules: [{
187
+ items: [{
188
+ items: [{ id: 11 }, { id: 12 }, { id: 13 }],
189
+ }],
190
+ }],
191
+ },
192
+ };
193
+
194
+ const result = removeUnwantedSections(sections, "operator", {});
195
+ expect(result.popup.modules[0].items[0].items).toHaveLength(3);
196
+ expect(result.recommended_casinos.modules[0].items[0].items).toHaveLength(2);
197
+
198
+ if (previousLimit === undefined) delete process.env.RECOMMENDED_CASINOS_NUMBER;
199
+ else process.env.RECOMMENDED_CASINOS_NUMBER = previousLimit;
200
+ if (previousUnlimitedUpper === undefined) delete process.env.UNLIMITED_POPUP_ITEMS;
201
+ else process.env.UNLIMITED_POPUP_ITEMS = previousUnlimitedUpper;
202
+ });
203
+
138
204
  test("removes null and undefined values from objects", () => {
139
205
  const input = { a: 1, b: null, c: undefined, d: 2 };
140
206
  const output = clean(input);