gatsby-core-theme 44.22.4 → 44.23.1

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,38 @@
1
+ ## [44.23.1](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/compare/v44.23.0...v44.23.1) (2026-05-04)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * improve style ([747562d](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/747562de00430c2c623b499540648c3c35818e02))
7
+ * separate context for cookie ([b99dcd1](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/b99dcd1dd37bca8cad937cdb67ecbbb37ce5c4cc))
8
+
9
+ # [44.23.0](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/compare/v44.22.4...v44.23.0) (2026-04-29)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * per-category cookie is authoritative; legacy fallback only when unset ([4dd15ad](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/4dd15adbe30a688bf6b7a255e54cf190864ce958))
15
+ * revert ([76d3a86](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/76d3a86e475a75b8949ee75591c34d53165b86e3))
16
+
17
+
18
+ ### Code Refactoring
19
+
20
+ * extract consent gates into cookies helper for site-level reuse ([1be4643](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/1be4643e6917c44eeaaf650d1559172ea26b05ae))
21
+ * self-remove consent listener after all gated categories init ([e502ecd](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/e502ecdf7e5dcd004e7591c5f601c2a5d2ab4a72))
22
+
23
+
24
+ * Merge branch 'en-492-cookie-consent' into 'master' ([b7e3d5b](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/b7e3d5b20f264814d77b3409445be72aa903071e))
25
+ * Merge branch 'en-492-cookie-consent' into 'master' ([c7e21bd](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/c7e21bd7a3e98a38d3da6351ffccb8b9a68d8c55))
26
+
27
+
28
+ ### Features
29
+
30
+ * add functionality category, gate optinmonster on it ([41c2d47](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/41c2d4725424d621824412f50af555118703a7e8))
31
+ * enable cookie consent on demo ([7dc252f](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/7dc252fb1c8ca024fff6e32b4868400f39f7866e))
32
+ * per-category script gating with site-level toggle ([7f7230c](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/7f7230cfef830356342c8afca2c7724a515564b3))
33
+ * reopen cookie settings modal via context + link-list sentinel ([6e68856](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/6e68856451984653f3578f1552f45936d821f693))
34
+ * script loading gated by cookie consent ([09ce499](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/09ce499b5567acf10ccca4611f7027f88781639b))
35
+
1
36
  ## [44.22.4](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/compare/v44.22.3...v44.22.4) (2026-04-28)
2
37
 
3
38
 
package/gatsby-browser.js CHANGED
@@ -26,7 +26,16 @@ async function yieldToMain() {
26
26
  });
27
27
  }
28
28
 
29
- let didInit = false;
29
+ const {
30
+ isAnalyticalGranted,
31
+ isMarketingGranted,
32
+ isFunctionalityGranted,
33
+ } = require("./src/helpers/cookies.mjs");
34
+
35
+ let piguardDidInit = false;
36
+ let analyticalDidInit = false;
37
+ let marketingDidInit = false;
38
+ let functionalityDidInit = false;
30
39
 
31
40
  function initGTM() {
32
41
  if (
@@ -141,30 +150,23 @@ const piguard = () => {
141
150
  document.head.appendChild(script);
142
151
  };
143
152
 
144
- async function scrollEvent() {
145
- if (didInit) return;
146
- didInit = true;
147
- await yieldToMain();
148
- initGTM();
149
-
153
+ function initPiguardCategory() {
154
+ if (piguardDidInit) return;
155
+ piguardDidInit = true;
150
156
  if (
151
157
  !document.getElementById("piguard") &&
152
158
  process.env.ENABLE_PIGUARD === "true"
153
159
  ) {
154
160
  piguard();
155
161
  }
162
+ }
156
163
 
157
- if (
158
- process.env.ENABLE_MICROSOFT === "true" &&
159
- !document.getElementById("microsoft-code")
160
- )
161
- microsoftAdvertising();
164
+ function initAnalyticalCategory() {
165
+ if (analyticalDidInit) return;
166
+ if (!isAnalyticalGranted()) return;
167
+ analyticalDidInit = true;
168
+ initGTM();
162
169
 
163
- if (
164
- process.env.ENABLE_OPTINMONSTR === "true" &&
165
- !document.getElementById("optin-monstr")
166
- )
167
- optinMonster();
168
170
  if (
169
171
  process.env.ENABLE_PIXEL === "true" &&
170
172
  !document.getElementById("pixel-code")
@@ -174,33 +176,73 @@ async function scrollEvent() {
174
176
  window.location.pathname !== process.env.PAGE_EXCLUDE_PIXEL
175
177
  ) {
176
178
  loadFacebookPixel();
177
-
178
179
  fbq("init", process.env.PIXEL_ID);
179
-
180
- // Initialize and track the PageView event
181
180
  fbq("track", "PageView");
182
181
  }
183
182
  }
184
183
  }
185
184
 
185
+ function initMarketingCategory() {
186
+ if (marketingDidInit) return;
187
+ if (!isMarketingGranted()) return;
188
+ marketingDidInit = true;
189
+
190
+ if (
191
+ process.env.ENABLE_MICROSOFT === "true" &&
192
+ !document.getElementById("microsoft-code")
193
+ )
194
+ microsoftAdvertising();
195
+ }
196
+
197
+ function initFunctionalityCategory() {
198
+ if (functionalityDidInit) return;
199
+ if (!isFunctionalityGranted()) return;
200
+ functionalityDidInit = true;
201
+
202
+ if (
203
+ process.env.ENABLE_OPTINMONSTR === "true" &&
204
+ !document.getElementById("optin-monstr")
205
+ )
206
+ optinMonster();
207
+ }
208
+
209
+ async function tryInitTrackingScripts() {
210
+ await yieldToMain();
211
+ initPiguardCategory();
212
+ initAnalyticalCategory();
213
+ initMarketingCategory();
214
+ initFunctionalityCategory();
215
+ }
216
+
217
+ const allGatedCategoriesInitialized = () =>
218
+ analyticalDidInit && marketingDidInit && functionalityDidInit;
219
+
186
220
  exports.onClientEntry = () => {
221
+ const consentHandler = async () => {
222
+ await tryInitTrackingScripts();
223
+ if (allGatedCategoriesInitialized()) {
224
+ window.removeEventListener("consent:accepted", consentHandler);
225
+ }
226
+ };
227
+ window.addEventListener("consent:accepted", consentHandler);
228
+
187
229
  if (
188
230
  process.env.PPC === "true" ||
189
231
  /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
190
232
  navigator.userAgent
191
233
  )
192
234
  ) {
193
- scrollEvent();
235
+ tryInitTrackingScripts();
194
236
  } else {
195
- document.addEventListener("scroll", scrollEvent, {
237
+ document.addEventListener("scroll", tryInitTrackingScripts, {
196
238
  passive: true,
197
239
  once: true,
198
240
  });
199
- document.addEventListener("mousemove", scrollEvent, {
241
+ document.addEventListener("mousemove", tryInitTrackingScripts, {
200
242
  passive: true,
201
243
  once: true,
202
244
  });
203
- document.addEventListener("touchstart", scrollEvent, {
245
+ document.addEventListener("touchstart", tryInitTrackingScripts, {
204
246
  passive: true,
205
247
  once: true,
206
248
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gatsby-core-theme",
3
- "version": "44.22.4",
3
+ "version": "44.23.1",
4
4
  "description": "Gatsby Theme NPM Package",
5
5
  "author": "",
6
6
  "license": "ISC",
@@ -14,10 +14,24 @@ import Button from "~atoms/button/button";
14
14
  import styles from "./cookie-modal.module.scss";
15
15
  import cookiesContent from '../../../constants/cookies';
16
16
  import { parseCookieTextWithLinks } from '../../../helpers/generators.mjs';
17
+ import { getCookie } from '~helpers/cookies';
18
+
19
+ const initialCategoryStates = () => {
20
+ const states = {};
21
+ (cookiesContent.modal?.categories || []).forEach((cat, idx) => {
22
+ if (cat.hasToggle === false) {
23
+ states[idx] = true;
24
+ return;
25
+ }
26
+ states[idx] = cat.cookieName ? getCookie(cat.cookieName) === 'true' : false;
27
+ });
28
+ return states;
29
+ };
17
30
 
18
31
  const CookieModal = ({
19
32
  handleAcceptCookies,
20
33
  handleDeclineCookies,
34
+ handleConfirmChoices,
21
35
  declineButtonType = "secondary",
22
36
  acceptButtonType = "primary",
23
37
  buttonSize = "m",
@@ -27,7 +41,7 @@ const CookieModal = ({
27
41
  hide,
28
42
  }) => {
29
43
  const [categorySection, setCategorySection] = useState(0);
30
- const [categoryStates, setCategoryStates] = useState({});
44
+ const [categoryStates, setCategoryStates] = useState(initialCategoryStates);
31
45
  const [menage, setMenage] = useState(true);
32
46
 
33
47
  const modal = useRef(null);
@@ -68,6 +82,7 @@ const CookieModal = ({
68
82
  textKey: item?.description?.translationKey,
69
83
  defaultText: item?.description?.label,
70
84
  alwaysEnabledLabel: item?.alwaysEnabledLabel,
85
+ cookieName: item?.cookieName,
71
86
  }));
72
87
 
73
88
 
@@ -204,7 +219,11 @@ const CookieModal = ({
204
219
  {!templateTwo && (
205
220
  <div className={styles?.lastButton || ""}>
206
221
  <Button
207
- onClick={() => handleAcceptCookies()}
222
+ onClick={() =>
223
+ handleConfirmChoices
224
+ ? handleConfirmChoices(categoryStates)
225
+ : handleAcceptCookies()
226
+ }
208
227
  btnText={useTranslate(
209
228
  modalSettings?.confirmButton?.translationKey,
210
229
  modalSettings?.confirmButton?.label
@@ -224,6 +243,7 @@ const CookieModal = ({
224
243
  CookieModal.propTypes = {
225
244
  handleAcceptCookies: PropTypes.func,
226
245
  handleDeclineCookies: PropTypes.func,
246
+ handleConfirmChoices: PropTypes.func,
227
247
  closeModal: PropTypes.func,
228
248
  logo: PropTypes.string,
229
249
  buttonSize: PropTypes.string,
@@ -1,13 +1,16 @@
1
1
  /* eslint-disable arrow-body-style */
2
2
  /* eslint-disable no-nested-ternary */
3
- import React from 'react';
3
+ import React, { useContext } from 'react';
4
4
  import PropTypes from 'prop-types';
5
5
 
6
- import keygen from '~helpers/keygen';
7
6
  import { imagePrettyUrl } from '~helpers/getters';
8
7
  import { getAltText } from '~helpers/image';
9
8
  import Link from '~hooks/link';
10
9
  import LazyImage from '~hooks/lazy-image';
10
+ import { Context } from '~context/MainProvider';
11
+
12
+ const COOKIE_SETTINGS_SENTINEL =
13
+ process.env.GATSBY_COOKIE_SETTINGS_SENTINEL || '#cookie-settings';
11
14
 
12
15
  /* eslint-disable react/jsx-no-target-blank */
13
16
 
@@ -25,22 +28,27 @@ const LinkList = ({
25
28
  gtmClass = '',
26
29
  showLinks = true,
27
30
  }) => {
31
+ const { openCookieSettings } = useContext(Context)?.cookieConsentContext || {};
32
+
28
33
  function renderLinkContent(item, index) {
29
34
  const icon = listIcon[index];
30
35
 
31
- const LinkImage = () => (
32
- <LazyImage
33
- src={imagePrettyUrl(
34
- item.image || item.logo?.url?.split('.com/')[1],
35
- width || item?.image_object?.width || item.logo?.width,
36
- height || item?.image_object?.height || item.logo?.height
37
- )}
38
- alt={getAltText(item?.image_object || item.logo, item.title)}
39
- width={width || item?.image_object?.width || item.logo?.width}
40
- height={height || item?.image_object?.height || item.logo?.height}
41
- loading={disableLazyLoad ? 'eager' : 'lazy'}
42
- />
43
- );
36
+ const imgWidth = width || item?.image_object?.width || item.logo?.width;
37
+ const imgHeight = height || item?.image_object?.height || item.logo?.height;
38
+ const imgEl =
39
+ (item.image || item.logo) ? (
40
+ <LazyImage
41
+ src={imagePrettyUrl(
42
+ item.image || item.logo?.url?.split('.com/')[1],
43
+ imgWidth,
44
+ imgHeight
45
+ )}
46
+ alt={getAltText(item?.image_object || item.logo, item.title)}
47
+ width={imgWidth}
48
+ height={imgHeight}
49
+ loading={disableLazyLoad ? 'eager' : 'lazy'}
50
+ />
51
+ ) : null;
44
52
 
45
53
  return (
46
54
  <>
@@ -48,49 +56,71 @@ const LinkList = ({
48
56
  <>
49
57
  {multiIcon ? icon : listIcon}
50
58
  <span>{item.title}</span>
51
- {(item.image || item.logo) && <LinkImage />}
59
+ {imgEl}
52
60
  </>
53
61
  )}
54
- {(item.image || item.logo) && imageOnly && <LinkImage />}
62
+ {imageOnly && imgEl}
55
63
  </>
56
64
  );
57
65
  }
58
66
 
59
- function renderItems(item, index) {
67
+ function renderLinkWrapper(item, index) {
60
68
  const link = item?.value || item?.url;
69
+ const content = renderLinkContent(item, index);
70
+ const ariaLabel = `${item.title || item.name || item.url} Link`;
71
+
72
+ if (link === COOKIE_SETTINGS_SENTINEL) {
73
+ return (
74
+ <button
75
+ type="button"
76
+ onClick={() => openCookieSettings && openCookieSettings()}
77
+ title={item.title || item.name}
78
+ className={gtmClass || ''}
79
+ aria-label={ariaLabel}
80
+ >
81
+ {content}
82
+ </button>
83
+ );
84
+ }
85
+
86
+ if (!link) return content;
87
+
88
+ if (link.includes('http') || link.includes('www')) {
89
+ return (
90
+ <a
91
+ href={link}
92
+ title={item.title || item.name}
93
+ rel={`noreferrer ${item.nofollow ? 'nofollow' : ''}`}
94
+ target="_blank"
95
+ className={gtmClass || ''}
96
+ aria-label={ariaLabel}
97
+ >
98
+ {content}
99
+ </a>
100
+ );
101
+ }
61
102
 
62
103
  return (
63
- <li key={keygen()}>
64
- {link ? (
65
- link.includes('http') || link.includes('www') ? (
66
- <a
67
- href={link}
68
- title={item.title || item.name}
69
- rel={`noreferrer ${item.nofollow ? 'nofollow' : ''}`}
70
- target="_blank"
71
- className={gtmClass || ''}
72
- aria-label={`${item.title || item.name} Link`}
73
- >
74
- {renderLinkContent(item, index)}
75
- </a>
76
- ) : (
77
- <Link
78
- to={showLinks && link}
79
- title={item.title || item.name}
80
- className={gtmClass || ''}
81
- rel={item.nofollow ? 'nofollow' : ''}
82
- aria-label={`${item.title || item.url} Link`}
83
- >
84
- {renderLinkContent(item, index)}
85
- </Link>
86
- )
87
- ) : (
88
- renderLinkContent(item, index)
89
- )}
90
- </li>
104
+ <Link
105
+ to={showLinks && link}
106
+ title={item.title || item.name}
107
+ className={gtmClass || ''}
108
+ rel={item.nofollow ? 'nofollow' : ''}
109
+ aria-label={ariaLabel}
110
+ >
111
+ {content}
112
+ </Link>
91
113
  );
92
114
  }
93
115
 
116
+ function getItemKey(item, index) {
117
+ return item?.value || item?.url || item?.title || item?.name || index;
118
+ }
119
+
120
+ function renderItems(item, index) {
121
+ return <li key={getItemKey(item, index)}>{renderLinkWrapper(item, index)}</li>;
122
+ }
123
+
94
124
  const renderSingleList = () => {
95
125
  return (
96
126
  <ul className={classes}>
@@ -102,13 +132,13 @@ const LinkList = ({
102
132
  };
103
133
 
104
134
  const renderLinklists = () => {
105
- return lists.children.map((list) => {
135
+ return lists.children.map((list, listIndex) => {
106
136
  return (
107
- <ul className={classes || ''} key={keygen()}>
108
- {showListTitle && <li key={keygen()}>{list.title}</li>}
137
+ <ul className={classes || ''} key={list?.title || list?.value || listIndex}>
138
+ {showListTitle && <li key={`title-${list?.title || listIndex}`}>{list.title}</li>}
109
139
  {list.children &&
110
- list.children.map((child) => {
111
- return renderItems(child);
140
+ list.children.map((child, childIndex) => {
141
+ return renderItems(child, childIndex);
112
142
  })}
113
143
  </ul>
114
144
  );
@@ -1,5 +1,7 @@
1
- import React, { useRef } from "react";
1
+ import React, { useRef, useState, useEffect } from "react";
2
2
  import PropTypes from "prop-types";
3
+ import { IoIosArrowBack } from "@react-icons/all-files/io/IoIosArrowBack";
4
+ import { IoIosArrowForward } from "@react-icons/all-files/io/IoIosArrowForward";
3
5
  import keygen from "~helpers/keygen";
4
6
  import LazyImage from "~hooks/lazy-image";
5
7
  import Button from "../../../../atoms/button/operator-cta";
@@ -26,104 +28,167 @@ const DEFAULT_CTA_TRANSLATE = {
26
28
  blacklisted: { translationKey: 'blacklisted', defaultValue: 'Blacklisted' }
27
29
  };
28
30
 
31
+ const BUTTON_SCROLL = 300;
32
+
29
33
  export default function TemplateTwo({
30
34
  module,
31
35
  showOperatorHeader = false,
32
36
  ctaTranslate = DEFAULT_CTA_TRANSLATE,
33
37
  }) {
34
38
  const container = useRef(null);
39
+ const [scrollX, setScrollX] = useState(0);
40
+ const [scrollEnd, setScrollEnd] = useState(false);
41
+ const [showButtons, setShowButtons] = useState(false);
42
+
35
43
  const items = module?.items;
36
44
  const ctaText = module?.link_text;
37
45
  const date = useTranslate('date', 'Date:');
38
46
  const funbetLabel = useTranslate('funbet', 'Funbet');
39
47
 
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>
48
+ const updateButtons = (shift) => {
49
+ setScrollEnd(
50
+ Math.floor(container.current.scrollWidth - (container.current.scrollLeft + shift)) <=
51
+ container.current.offsetWidth
52
+ );
53
+ };
75
54
 
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>
55
+ const scroll = (shift) => {
56
+ container.current.scrollTo({
57
+ left: container.current.scrollLeft + shift,
58
+ behavior: 'smooth',
59
+ });
60
+ updateButtons(shift);
61
+ setScrollX(scrollX + shift);
62
+ };
63
+
64
+ const onStopScrolling = () => {
65
+ setScrollX(container.current.scrollLeft);
66
+ updateButtons(0);
67
+ };
68
+
69
+ const onScroll = (scrollLeft) => {
70
+ updateButtons(0);
71
+ setScrollX(scrollLeft);
72
+ };
73
+
74
+ useEffect(() => {
75
+ const timer = setTimeout(() => {
76
+ if (container.current) {
77
+ setShowButtons(container.current.scrollWidth > container.current.offsetWidth);
78
+ }
79
+ }, 500);
80
+ return () => clearTimeout(timer);
81
+ }, []);
82
+
83
+ return (
84
+ <div className={styles.wrapper}>
85
+ <ScrollX refTag={container} scroll stopScrolling={onStopScrolling} onScroll={onScroll}>
86
+ <div
87
+ ref={container}
88
+ className={styles.sportOdds}
89
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
90
+ tabIndex={0}
91
+ >
92
+ {(items || []).map((item) => {
93
+ const operatorName = item?.relation?.name;
94
+ const operatorLogo = item?.relation?.logo;
86
95
 
87
- <div className={styles.action}>
88
- {(item?.odds_text || item?.odds_value) && (
89
- <div className={styles.odds}>
90
- {operatorLogo?.url && (
96
+ return (
97
+ <div key={keygen()} className={styles.card}>
98
+ <div className={styles.header}>
99
+ {
100
+ showOperatorHeader ?
101
+ <>
102
+ {operatorLogo?.url ? (
91
103
  <LazyImage
92
104
  src={operatorLogo.url}
93
- width={48}
94
- height={48}
105
+ width={20}
106
+ height={20}
95
107
  alt={operatorLogo.alt || operatorName}
96
108
  />
109
+ ) : (
110
+ <span className={styles.operatorIcon} aria-hidden="true" />
97
111
  )}
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>
112
+ {operatorName && <span>{operatorName}</span>}
113
+ </> : <>
114
+ <FunIcon />
115
+ <span>{funbetLabel}</span>
116
+ </>
117
+ }
118
118
  </div>
119
119
 
120
- <Tnc operator={item?.relation} tracker={item?.bonus?.tracking_link_name} />
120
+ <div className={styles.body}>
121
+ <div className={styles.main}>
122
+ <div className={styles.info}>
123
+ {item?.main_title && (
124
+ <p className={styles.title}>{item.main_title}</p>
125
+ )}
126
+ {item?.secondary_title && (
127
+ <p className={styles.time}>{date}<span>{item.secondary_title}</span></p>
128
+ )}
129
+ </div>
130
+
131
+ <div className={styles.action}>
132
+ {(item?.odds_text || item?.odds_value) && (
133
+ <div className={styles.odds}>
134
+ {operatorLogo?.url && (
135
+ <LazyImage
136
+ src={operatorLogo.url}
137
+ width={48}
138
+ height={48}
139
+ alt={operatorLogo.alt || operatorName}
140
+ />
141
+ )}
142
+ <span>
143
+ {item?.odds_text}
144
+ {item?.odds_text && item?.odds_value ? ": " : ""}
145
+ {item?.odds_value}
146
+ </span>
147
+ </div>
148
+ )}
149
+
150
+ <Button
151
+ operator={item?.relation}
152
+ buttonType="primary"
153
+ buttonSize="m"
154
+ btnText={ctaText}
155
+ isInternalLink={false}
156
+ targetBlank
157
+ moduleName={module?.name}
158
+ tracker={module?.bonus?.tracking_link_name || 'main'}
159
+ translationsObj={ctaTranslate}
160
+ />
161
+ </div>
162
+ </div>
163
+
164
+ <Tnc operator={item?.relation} tracker={item?.bonus?.tracking_link_name} />
165
+ </div>
121
166
  </div>
122
- </div>
123
- );
124
- })}
125
- </div>
126
- </ScrollX>
167
+ );
168
+ })}
169
+ </div>
170
+ </ScrollX>
171
+ {showButtons && (
172
+ <div className={styles.navigation}>
173
+ <button
174
+ className={`${styles.navButton} ${scrollX <= 0 ? styles.disabled : ''}`}
175
+ onClick={() => scroll(-BUTTON_SCROLL)}
176
+ type="button"
177
+ aria-label="Previous"
178
+ >
179
+ <IoIosArrowBack />
180
+ </button>
181
+ <button
182
+ className={`${styles.navButton} ${scrollEnd ? styles.disabled : ''}`}
183
+ onClick={() => scroll(BUTTON_SCROLL)}
184
+ type="button"
185
+ aria-label="Next"
186
+ >
187
+ <IoIosArrowForward />
188
+ </button>
189
+ </div>
190
+ )}
191
+ </div>
127
192
  );
128
193
  }
129
194
 
@@ -1,19 +1,80 @@
1
- .sportOdds {
1
+ .wrapper {
2
2
  max-width: var(--main-container-max);
3
3
  margin: 0 auto;
4
4
 
5
+ @include flex-direction(column);
6
+
7
+ gap: 1.6rem;
8
+ }
9
+
10
+ .sportOdds {
5
11
  @include flex-direction(row);
6
12
  @include flex-align(stretch, start);
7
13
 
8
14
  gap: 1.6rem;
9
15
  overflow-x: auto;
10
16
  scroll-snap-type: x mandatory;
11
-
17
+ padding-bottom: 2rem;
18
+
12
19
  &::-webkit-scrollbar {
20
+ height: 0.6rem;
21
+ border-radius: 0.6rem;
22
+
23
+ @include max(mobile) {
24
+ display: none;
25
+ }
26
+ }
27
+
28
+ &::-webkit-scrollbar-track {
29
+ background: var(--spotlight-sport-odds-scrollbar-track, #E4E4E7);
30
+ border-radius: 0.6rem;
31
+ }
32
+
33
+ &::-webkit-scrollbar-thumb {
34
+ background-color: var(--spotlight-sport-odds-scrollbar-thumb, #3F3F46);
35
+ border-radius: 0.6rem;
36
+ }
37
+
38
+ }
39
+
40
+ .navigation {
41
+ @include flex-direction(row);
42
+ @include flex-align(center, flex-start);
43
+
44
+ gap: 0.8rem;
45
+
46
+ @include max(mobile) {
13
47
  display: none;
14
48
  }
15
49
  }
16
50
 
51
+ .navButton {
52
+ @include flex-direction(row);
53
+ @include flex-align(center, center);
54
+
55
+ width: 4rem;
56
+ height: 4rem;
57
+ border-radius: 0.6rem;
58
+ background: var(--spotlight-sport-odds-nav-bg, #E4E4E7);
59
+ border: 0;
60
+ color: var(--spotlight-sport-odds-nav-text, #52525B);
61
+ cursor: pointer;
62
+ padding: 0;
63
+ font-size: 1.6rem;
64
+
65
+ &:hover {
66
+ background: var(--spotlight-sport-odds-nav-hover-bg, #E4E4E7);
67
+ color: var(--spotlight-sport-odds-nav-hover-text, #18181B);
68
+ }
69
+ }
70
+
71
+ .disabled {
72
+ opacity: 0.4;
73
+ cursor: not-allowed;
74
+ pointer-events: none;
75
+ background: var(--spotlight-sport-odds-nav-deactive-bg, #F4F4F5);
76
+ }
77
+
17
78
  .card {
18
79
  flex: 0 0 auto;
19
80
  width: 28rem;
@@ -53,6 +53,11 @@ describe('cookie consent component', () => {
53
53
  });
54
54
  afterEach(() => {
55
55
  cleanup();
56
- document.cookie = `CookieConsent=; expires=Thu, 01 Jan 1970 00:00:00; path=/`;
56
+ const expired = 'expires=Thu, 01 Jan 1970 00:00:00; path=/';
57
+ document.cookie = `CookieConsent=; ${expired}`;
58
+ document.cookie = `cookie_necessary=; ${expired}`;
59
+ document.cookie = `cookie_analytical=; ${expired}`;
60
+ document.cookie = `cookie_marketing=; ${expired}`;
61
+ document.cookie = `showCookie=; ${expired}`;
57
62
  sessionStorage.removeItem('CookieConsentDecline');
58
63
  });
@@ -2,16 +2,19 @@
2
2
  /* eslint-disable jsx-a11y/click-events-have-key-events */
3
3
  /* eslint-disable import/no-extraneous-dependencies */
4
4
  /* eslint-disable react-hooks/rules-of-hooks */
5
- import React, { useState, useEffect, lazy, Suspense } from "react";
5
+ import React, { useContext, useEffect, lazy, Suspense } from "react";
6
6
  import PropTypes from "prop-types";
7
7
 
8
8
  import useTranslate from "~hooks/useTranslate/useTranslate";
9
9
  import { setCookie, getCookie } from "~helpers/cookies";
10
+ import { Context, CookieVisibilityContext } from "~context/MainProvider";
10
11
  import styles from "./cookie-consent.module.scss";
11
12
 
12
13
  import cookiesContent from '../../../constants/cookies';
13
14
  import { parseCookieTextWithLinks } from '../../../helpers/generators.mjs'
14
15
 
16
+ const CookieModal = lazy(() => import("../../molecules/cookie-modal"));
17
+
15
18
  const CookieConsent = ({
16
19
  settingsCookie,
17
20
  children,
@@ -20,12 +23,14 @@ const CookieConsent = ({
20
23
  icon = null,
21
24
  showRejectButton = false,
22
25
  }) => {
23
- const [showModal, setShowModal] = useState(false);
24
-
25
- const [showCookieConsent, setShowCookieConsent] = useState(false);
26
-
27
- const CookieModal = lazy(() => import("../../molecules/cookie-modal"));
28
-
26
+ const {
27
+ openCookieBanner = () => {},
28
+ openCookieSettings = () => {},
29
+ closeCookieModal = () => {},
30
+ closeCookieConsent = () => {},
31
+ } = useContext(Context)?.cookieConsentContext || {};
32
+ const { showCookieBanner = false, showCookieModal = false } =
33
+ useContext(CookieVisibilityContext) || {};
29
34
 
30
35
  const shouldShowRejectButton =
31
36
  typeof cookiesContent.showRejectButton === "boolean"
@@ -44,37 +49,100 @@ const CookieConsent = ({
44
49
  cookiesContent.text?.translationKey,
45
50
  cookiesContent.text?.label
46
51
  );
47
- // when user declines
52
+
53
+ const categories = cookiesContent.modal?.categories || [];
54
+
55
+ const dispatchConsentEvent = () => {
56
+ if (typeof window !== "undefined") {
57
+ window.dispatchEvent(new CustomEvent("consent:accepted"));
58
+ }
59
+ };
60
+
61
+ // Necessary categories (hasToggle === false) are always written as true.
62
+ // Toggleable categories are written from the provided state map.
63
+ const writeCategoryCookies = (stateByIndex) => {
64
+ categories.forEach((cat, idx) => {
65
+ if (!cat.cookieName) return;
66
+ const value = cat.hasToggle === false ? true : Boolean(stateByIndex[idx]);
67
+ setCookie(cat.cookieName, value, 365, "/");
68
+ });
69
+ };
70
+
71
+ // Detect a toggle going from on -> off, which requires a reload to unload
72
+ // tracking scripts that have already executed in this session.
73
+ const wasAnyCategoryRevoked = (newStateByIndex) =>
74
+ categories.some((cat, idx) => {
75
+ if (!cat.cookieName || cat.hasToggle === false) return false;
76
+ const wasEnabled = getCookie(cat.cookieName) === "true";
77
+ const willBeEnabled = Boolean(newStateByIndex[idx]);
78
+ return wasEnabled && !willBeEnabled;
79
+ });
80
+
81
+ const closeBanner = () => {
82
+ setCookie("showCookie", false, 365, "/");
83
+ closeCookieConsent();
84
+ };
85
+
86
+ // when user declines (reject non-necessary): all toggleable categories -> false.
87
+ // Still dispatches the consent event because `necessary` stays true and every
88
+ // tracking script is currently gated on `necessary`. Revisit when scripts get
89
+ // re-tagged to analytical/marketing — decline should then NOT fire the event.
48
90
  const handleDecline = () => {
91
+ const allFalse = {};
92
+ categories.forEach((_, idx) => {
93
+ allFalse[idx] = false;
94
+ });
95
+ const revoked = wasAnyCategoryRevoked(allFalse);
96
+ writeCategoryCookies(allFalse);
49
97
  setCookie(cookieName, false, 365, "/");
50
- setCookie("showCookie", false, 365, "/");
51
- setShowCookieConsent(false);
52
- setShowModal(false);
53
- document.body.style.overflow = "auto";
98
+ if (revoked) {
99
+ window.location.reload();
100
+ return;
101
+ }
102
+ closeBanner();
103
+ dispatchConsentEvent();
54
104
  };
55
105
 
56
- // when user accepts
106
+ // when user accepts all: every toggleable category -> true
57
107
  const handleAccept = () => {
108
+ const allTrue = {};
109
+ categories.forEach((_, idx) => {
110
+ allTrue[idx] = true;
111
+ });
112
+ writeCategoryCookies(allTrue);
58
113
  setCookie(cookieName, true, 365, "/");
59
- setCookie("showCookie", false, 365, "/");
60
- setShowCookieConsent(false);
61
- setShowModal(false);
62
- document.body.style.overflow = "auto";
114
+ closeBanner();
115
+ dispatchConsentEvent();
116
+ };
117
+
118
+ // when user confirms a custom mix from the modal toggles
119
+ const handleConfirmChoices = (categoryStates) => {
120
+ const revoked = wasAnyCategoryRevoked(categoryStates);
121
+ writeCategoryCookies(categoryStates);
122
+ setCookie(cookieName, true, 365, "/");
123
+ if (revoked) {
124
+ window.location.reload();
125
+ return;
126
+ }
127
+ closeBanner();
128
+ dispatchConsentEvent();
63
129
  };
64
130
 
65
131
  const handleShowModalClick = () => {
66
- setShowModal(!showModal);
67
- document.body.style.overflow = "hidden";
132
+ if (showCookieModal) {
133
+ closeCookieModal();
134
+ } else {
135
+ openCookieSettings();
136
+ }
68
137
  };
69
138
 
70
139
  const closeModal = () => {
71
- setShowModal(false);
72
- document.body.style.overflow = "auto";
140
+ closeCookieModal();
73
141
  };
74
142
 
75
143
  useEffect(() => {
76
144
  if (typeof window !== `undefined` && getCookie("showCookie") !== "false") {
77
- setShowCookieConsent(true);
145
+ openCookieBanner();
78
146
  }
79
147
  }, []);
80
148
 
@@ -82,7 +150,7 @@ const CookieConsent = ({
82
150
  <>
83
151
  <div
84
152
  className={`${styles.cookieConsent} ${
85
- (showCookieConsent && styles.show) || ""
153
+ (showCookieBanner && styles.show) || ""
86
154
  }`}
87
155
  >
88
156
  <div className={`${styles?.consent || ""}`}>
@@ -130,13 +198,14 @@ const CookieConsent = ({
130
198
  </div>
131
199
  </div>
132
200
  </div>
133
- {settingsCookie && showModal && (
201
+ {settingsCookie && showCookieModal && (
134
202
  <Suspense fallback={null}>
135
203
  <CookieModal
136
204
  logo={logo}
137
- hide={!showModal}
205
+ hide={!showCookieModal}
138
206
  handleAcceptCookies={handleAccept}
139
207
  handleDeclineCookies={handleDecline}
208
+ handleConfirmChoices={handleConfirmChoices}
140
209
  closeModal={closeModal}
141
210
  />
142
211
  </Suspense>
@@ -57,6 +57,7 @@ const cookiesContent = {
57
57
  },
58
58
  categories: [
59
59
  {
60
+ cookieName: 'cookie_necessary',
60
61
  title: {
61
62
  label: 'Neccesary',
62
63
  translationKey: 'neccesary_cookie_title',
@@ -69,6 +70,7 @@ const cookiesContent = {
69
70
  hasToggle: false,
70
71
  },
71
72
  {
73
+ cookieName: 'cookie_analytical',
72
74
  title: {
73
75
  label: 'Analytical And Stadistical',
74
76
  translationKey: 'analytical_cookie_title',
@@ -81,6 +83,20 @@ const cookiesContent = {
81
83
  hasToggle: true,
82
84
  },
83
85
  {
86
+ cookieName: 'cookie_functionality',
87
+ title: {
88
+ label: 'Functionality',
89
+ translationKey: 'functionality_cookie_title',
90
+ },
91
+ description: {
92
+ label:
93
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nibh aliquam non sit morbi neque in sodales tellus. Cursus neque sed quis tincidunt sed vestibulum rhoncus dolor elementum. Imperdiet tortor dolor sit nisi, magnis cras id ut. Id dolor, vel neque lobortis. Diam commodo vitae vulputate at ultrices id odio praesent. Nisi, sit massa orci accumsan.',
94
+ translationKey: 'functionality_cookie_text',
95
+ },
96
+ hasToggle: true,
97
+ },
98
+ {
99
+ cookieName: 'cookie_marketing',
84
100
  title: {
85
101
  label: 'Marketing',
86
102
  translationKey: 'marketing_cookie_title',
@@ -1,7 +1,16 @@
1
1
  /* eslint-disable react/prop-types */
2
- import React, { createContext, useState } from "react";
2
+ import React, { createContext, useState, useCallback, useMemo } from "react";
3
+
4
+ const setBodyScrollLock = (locked) => {
5
+ if (typeof document === "undefined") return;
6
+ document.body.style.overflow = locked ? "hidden" : "auto";
7
+ };
3
8
 
4
9
  export const Context = createContext();
10
+ export const CookieVisibilityContext = createContext({
11
+ showCookieBanner: false,
12
+ showCookieModal: false,
13
+ });
5
14
 
6
15
  export default (props) => {
7
16
  const { value, children } = props;
@@ -9,25 +18,77 @@ export default (props) => {
9
18
  const [showTranslationKeys, setShowTranslationKeys] = useState(false);
10
19
  const [showFilter, setShowFilter] = useState(false);
11
20
  const [topListFilters, setTopListFilters] = useState();
12
- const topListContext = {
13
- showFilter,
14
- setShowFilter,
15
- topListFilters,
16
- setTopListFilters,
17
- };
21
+
22
+ const topListContext = useMemo(
23
+ () => ({
24
+ showFilter,
25
+ setShowFilter,
26
+ topListFilters,
27
+ setTopListFilters,
28
+ }),
29
+ [showFilter, topListFilters]
30
+ );
31
+
32
+ const [showCookieBanner, setShowCookieBanner] = useState(false);
33
+ const [showCookieModal, setShowCookieModal] = useState(false);
34
+
35
+ const openCookieBanner = useCallback(() => setShowCookieBanner(true), []);
36
+ const openCookieSettings = useCallback(() => {
37
+ setShowCookieModal(true);
38
+ setBodyScrollLock(true);
39
+ }, []);
40
+ const closeCookieModal = useCallback(() => {
41
+ setShowCookieModal(false);
42
+ setBodyScrollLock(false);
43
+ }, []);
44
+ const closeCookieConsent = useCallback(() => {
45
+ setShowCookieBanner(false);
46
+ setShowCookieModal(false);
47
+ setBodyScrollLock(false);
48
+ }, []);
49
+
50
+ const cookieConsentContext = useMemo(
51
+ () => ({
52
+ openCookieBanner,
53
+ openCookieSettings,
54
+ closeCookieModal,
55
+ closeCookieConsent,
56
+ }),
57
+ [openCookieBanner, openCookieSettings, closeCookieModal, closeCookieConsent]
58
+ );
59
+
60
+ const cookieVisibility = useMemo(
61
+ () => ({ showCookieBanner, showCookieModal }),
62
+ [showCookieBanner, showCookieModal]
63
+ );
64
+
65
+ const contextValue = useMemo(
66
+ () => ({
67
+ translations: value.translations,
68
+ preview: value.isPreview,
69
+ language: value.language,
70
+ admin: value.admin,
71
+ setShowTranslationKeys,
72
+ showTranslationKeys,
73
+ topListContext,
74
+ cookieConsentContext,
75
+ }),
76
+ [
77
+ value.translations,
78
+ value.isPreview,
79
+ value.language,
80
+ value.admin,
81
+ showTranslationKeys,
82
+ topListContext,
83
+ cookieConsentContext,
84
+ ]
85
+ );
86
+
18
87
  return (
19
- <Context.Provider
20
- value={{
21
- translations: value.translations,
22
- preview: value.isPreview,
23
- language: value.language,
24
- admin: value.admin,
25
- setShowTranslationKeys,
26
- showTranslationKeys,
27
- topListContext,
28
- }}
29
- >
30
- {children}
88
+ <Context.Provider value={contextValue}>
89
+ <CookieVisibilityContext.Provider value={cookieVisibility}>
90
+ {children}
91
+ </CookieVisibilityContext.Provider>
31
92
  </Context.Provider>
32
93
  );
33
94
  };
@@ -19,3 +19,24 @@ export function getCookie(cname) {
19
19
  }
20
20
  return '';
21
21
  }
22
+
23
+ export const isCookieConsentEnabled = () =>
24
+ process.env.GATSBY_COOKIE_CONSENT_ENABLED === 'true';
25
+
26
+ export const hasLegacyAcceptAll = () => getCookie('CookieConsent') === 'true';
27
+
28
+ // Per-category cookie is authoritative once set. Only fall back to the legacy
29
+ // accept-all cookie when the per-category value is absent (pre-categories
30
+ // users), so an explicit "false" doesn't get re-granted by the legacy flag.
31
+ const isCategoryGranted = (cookieName) => {
32
+ if (!isCookieConsentEnabled()) return true;
33
+ const value = getCookie(cookieName);
34
+ if (value === 'true') return true;
35
+ if (value === 'false') return false;
36
+ return hasLegacyAcceptAll();
37
+ };
38
+
39
+ export const isAnalyticalGranted = () => isCategoryGranted('cookie_analytical');
40
+ export const isMarketingGranted = () => isCategoryGranted('cookie_marketing');
41
+ export const isFunctionalityGranted = () =>
42
+ isCategoryGranted('cookie_functionality');