gatsby-core-theme 44.22.4 → 44.23.0

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,30 @@
1
+ # [44.23.0](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/compare/v44.22.4...v44.23.0) (2026-04-29)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * per-category cookie is authoritative; legacy fallback only when unset ([4dd15ad](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/4dd15adbe30a688bf6b7a255e54cf190864ce958))
7
+ * revert ([76d3a86](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/76d3a86e475a75b8949ee75591c34d53165b86e3))
8
+
9
+
10
+ ### Code Refactoring
11
+
12
+ * extract consent gates into cookies helper for site-level reuse ([1be4643](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/1be4643e6917c44eeaaf650d1559172ea26b05ae))
13
+ * self-remove consent listener after all gated categories init ([e502ecd](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/e502ecdf7e5dcd004e7591c5f601c2a5d2ab4a72))
14
+
15
+
16
+ * Merge branch 'en-492-cookie-consent' into 'master' ([b7e3d5b](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/b7e3d5b20f264814d77b3409445be72aa903071e))
17
+ * Merge branch 'en-492-cookie-consent' into 'master' ([c7e21bd](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/c7e21bd7a3e98a38d3da6351ffccb8b9a68d8c55))
18
+
19
+
20
+ ### Features
21
+
22
+ * add functionality category, gate optinmonster on it ([41c2d47](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/41c2d4725424d621824412f50af555118703a7e8))
23
+ * enable cookie consent on demo ([7dc252f](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/7dc252fb1c8ca024fff6e32b4868400f39f7866e))
24
+ * per-category script gating with site-level toggle ([7f7230c](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/7f7230cfef830356342c8afca2c7724a515564b3))
25
+ * reopen cookie settings modal via context + link-list sentinel ([6e68856](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/6e68856451984653f3578f1552f45936d821f693))
26
+ * script loading gated by cookie consent ([09ce499](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/09ce499b5567acf10ccca4611f7027f88781639b))
27
+
1
28
  ## [44.22.4](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/compare/v44.22.3...v44.22.4) (2026-04-28)
2
29
 
3
30
 
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.0",
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,6 +1,6 @@
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
6
  import keygen from '~helpers/keygen';
@@ -8,6 +8,10 @@ import { imagePrettyUrl } from '~helpers/getters';
8
8
  import { getAltText } from '~helpers/image';
9
9
  import Link from '~hooks/link';
10
10
  import LazyImage from '~hooks/lazy-image';
11
+ import { Context } from '~context/MainProvider';
12
+
13
+ const COOKIE_SETTINGS_SENTINEL =
14
+ process.env.GATSBY_COOKIE_SETTINGS_SENTINEL || '#cookie-settings';
11
15
 
12
16
  /* eslint-disable react/jsx-no-target-blank */
13
17
 
@@ -25,6 +29,8 @@ const LinkList = ({
25
29
  gtmClass = '',
26
30
  showLinks = true,
27
31
  }) => {
32
+ const { openCookieSettings } = useContext(Context)?.cookieConsentContext || {};
33
+
28
34
  function renderLinkContent(item, index) {
29
35
  const icon = listIcon[index];
30
36
 
@@ -56,41 +62,59 @@ const LinkList = ({
56
62
  );
57
63
  }
58
64
 
59
- function renderItems(item, index) {
65
+ function renderLinkWrapper(item, index) {
60
66
  const link = item?.value || item?.url;
67
+ const content = renderLinkContent(item, index);
68
+ const ariaLabel = `${item.title || item.name || item.url} Link`;
69
+
70
+ if (link === COOKIE_SETTINGS_SENTINEL) {
71
+ return (
72
+ <button
73
+ type="button"
74
+ onClick={() => openCookieSettings && openCookieSettings()}
75
+ title={item.title || item.name}
76
+ className={gtmClass || ''}
77
+ aria-label={ariaLabel}
78
+ >
79
+ {content}
80
+ </button>
81
+ );
82
+ }
83
+
84
+ if (!link) return content;
85
+
86
+ if (link.includes('http') || link.includes('www')) {
87
+ return (
88
+ <a
89
+ href={link}
90
+ title={item.title || item.name}
91
+ rel={`noreferrer ${item.nofollow ? 'nofollow' : ''}`}
92
+ target="_blank"
93
+ className={gtmClass || ''}
94
+ aria-label={ariaLabel}
95
+ >
96
+ {content}
97
+ </a>
98
+ );
99
+ }
61
100
 
62
101
  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>
102
+ <Link
103
+ to={showLinks && link}
104
+ title={item.title || item.name}
105
+ className={gtmClass || ''}
106
+ rel={item.nofollow ? 'nofollow' : ''}
107
+ aria-label={ariaLabel}
108
+ >
109
+ {content}
110
+ </Link>
91
111
  );
92
112
  }
93
113
 
114
+ function renderItems(item, index) {
115
+ return <li key={keygen()}>{renderLinkWrapper(item, index)}</li>;
116
+ }
117
+
94
118
  const renderSingleList = () => {
95
119
  return (
96
120
  <ul className={classes}>
@@ -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,11 +2,12 @@
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 } from "~context/MainProvider";
10
11
  import styles from "./cookie-consent.module.scss";
11
12
 
12
13
  import cookiesContent from '../../../constants/cookies';
@@ -20,9 +21,14 @@ const CookieConsent = ({
20
21
  icon = null,
21
22
  showRejectButton = false,
22
23
  }) => {
23
- const [showModal, setShowModal] = useState(false);
24
-
25
- const [showCookieConsent, setShowCookieConsent] = useState(false);
24
+ const {
25
+ showCookieBanner = false,
26
+ showCookieModal = false,
27
+ openCookieBanner = () => {},
28
+ openCookieSettings = () => {},
29
+ closeCookieModal = () => {},
30
+ closeCookieConsent = () => {},
31
+ } = useContext(Context)?.cookieConsentContext || {};
26
32
 
27
33
  const CookieModal = lazy(() => import("../../molecules/cookie-modal"));
28
34
 
@@ -44,37 +50,100 @@ const CookieConsent = ({
44
50
  cookiesContent.text?.translationKey,
45
51
  cookiesContent.text?.label
46
52
  );
47
- // when user declines
53
+
54
+ const categories = cookiesContent.modal?.categories || [];
55
+
56
+ const dispatchConsentEvent = () => {
57
+ if (typeof window !== "undefined") {
58
+ window.dispatchEvent(new CustomEvent("consent:accepted"));
59
+ }
60
+ };
61
+
62
+ // Necessary categories (hasToggle === false) are always written as true.
63
+ // Toggleable categories are written from the provided state map.
64
+ const writeCategoryCookies = (stateByIndex) => {
65
+ categories.forEach((cat, idx) => {
66
+ if (!cat.cookieName) return;
67
+ const value = cat.hasToggle === false ? true : Boolean(stateByIndex[idx]);
68
+ setCookie(cat.cookieName, value, 365, "/");
69
+ });
70
+ };
71
+
72
+ // Detect a toggle going from on -> off, which requires a reload to unload
73
+ // tracking scripts that have already executed in this session.
74
+ const wasAnyCategoryRevoked = (newStateByIndex) =>
75
+ categories.some((cat, idx) => {
76
+ if (!cat.cookieName || cat.hasToggle === false) return false;
77
+ const wasEnabled = getCookie(cat.cookieName) === "true";
78
+ const willBeEnabled = Boolean(newStateByIndex[idx]);
79
+ return wasEnabled && !willBeEnabled;
80
+ });
81
+
82
+ const closeBanner = () => {
83
+ setCookie("showCookie", false, 365, "/");
84
+ closeCookieConsent();
85
+ };
86
+
87
+ // when user declines (reject non-necessary): all toggleable categories -> false.
88
+ // Still dispatches the consent event because `necessary` stays true and every
89
+ // tracking script is currently gated on `necessary`. Revisit when scripts get
90
+ // re-tagged to analytical/marketing — decline should then NOT fire the event.
48
91
  const handleDecline = () => {
92
+ const allFalse = {};
93
+ categories.forEach((_, idx) => {
94
+ allFalse[idx] = false;
95
+ });
96
+ const revoked = wasAnyCategoryRevoked(allFalse);
97
+ writeCategoryCookies(allFalse);
49
98
  setCookie(cookieName, false, 365, "/");
50
- setCookie("showCookie", false, 365, "/");
51
- setShowCookieConsent(false);
52
- setShowModal(false);
53
- document.body.style.overflow = "auto";
99
+ if (revoked) {
100
+ window.location.reload();
101
+ return;
102
+ }
103
+ closeBanner();
104
+ dispatchConsentEvent();
54
105
  };
55
106
 
56
- // when user accepts
107
+ // when user accepts all: every toggleable category -> true
57
108
  const handleAccept = () => {
109
+ const allTrue = {};
110
+ categories.forEach((_, idx) => {
111
+ allTrue[idx] = true;
112
+ });
113
+ writeCategoryCookies(allTrue);
58
114
  setCookie(cookieName, true, 365, "/");
59
- setCookie("showCookie", false, 365, "/");
60
- setShowCookieConsent(false);
61
- setShowModal(false);
62
- document.body.style.overflow = "auto";
115
+ closeBanner();
116
+ dispatchConsentEvent();
117
+ };
118
+
119
+ // when user confirms a custom mix from the modal toggles
120
+ const handleConfirmChoices = (categoryStates) => {
121
+ const revoked = wasAnyCategoryRevoked(categoryStates);
122
+ writeCategoryCookies(categoryStates);
123
+ setCookie(cookieName, true, 365, "/");
124
+ if (revoked) {
125
+ window.location.reload();
126
+ return;
127
+ }
128
+ closeBanner();
129
+ dispatchConsentEvent();
63
130
  };
64
131
 
65
132
  const handleShowModalClick = () => {
66
- setShowModal(!showModal);
67
- document.body.style.overflow = "hidden";
133
+ if (showCookieModal) {
134
+ closeCookieModal();
135
+ } else {
136
+ openCookieSettings();
137
+ }
68
138
  };
69
139
 
70
140
  const closeModal = () => {
71
- setShowModal(false);
72
- document.body.style.overflow = "auto";
141
+ closeCookieModal();
73
142
  };
74
143
 
75
144
  useEffect(() => {
76
145
  if (typeof window !== `undefined` && getCookie("showCookie") !== "false") {
77
- setShowCookieConsent(true);
146
+ openCookieBanner();
78
147
  }
79
148
  }, []);
80
149
 
@@ -82,7 +151,7 @@ const CookieConsent = ({
82
151
  <>
83
152
  <div
84
153
  className={`${styles.cookieConsent} ${
85
- (showCookieConsent && styles.show) || ""
154
+ (showCookieBanner && styles.show) || ""
86
155
  }`}
87
156
  >
88
157
  <div className={`${styles?.consent || ""}`}>
@@ -130,13 +199,14 @@ const CookieConsent = ({
130
199
  </div>
131
200
  </div>
132
201
  </div>
133
- {settingsCookie && showModal && (
202
+ {settingsCookie && showCookieModal && (
134
203
  <Suspense fallback={null}>
135
204
  <CookieModal
136
205
  logo={logo}
137
- hide={!showModal}
206
+ hide={!showCookieModal}
138
207
  handleAcceptCookies={handleAccept}
139
208
  handleDeclineCookies={handleDecline}
209
+ handleConfirmChoices={handleConfirmChoices}
140
210
  closeModal={closeModal}
141
211
  />
142
212
  </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,6 +1,11 @@
1
1
  /* eslint-disable react/prop-types */
2
2
  import React, { createContext, useState } from "react";
3
3
 
4
+ const setBodyScrollLock = (locked) => {
5
+ if (typeof document === "undefined") return;
6
+ document.body.style.overflow = locked ? "hidden" : "auto";
7
+ };
8
+
4
9
  export const Context = createContext();
5
10
 
6
11
  export default (props) => {
@@ -15,6 +20,29 @@ export default (props) => {
15
20
  topListFilters,
16
21
  setTopListFilters,
17
22
  };
23
+
24
+ const [showCookieBanner, setShowCookieBanner] = useState(false);
25
+ const [showCookieModal, setShowCookieModal] = useState(false);
26
+
27
+ const cookieConsentContext = {
28
+ showCookieBanner,
29
+ showCookieModal,
30
+ openCookieBanner: () => setShowCookieBanner(true),
31
+ openCookieSettings: () => {
32
+ setShowCookieModal(true);
33
+ setBodyScrollLock(true);
34
+ },
35
+ closeCookieModal: () => {
36
+ setShowCookieModal(false);
37
+ setBodyScrollLock(false);
38
+ },
39
+ closeCookieConsent: () => {
40
+ setShowCookieBanner(false);
41
+ setShowCookieModal(false);
42
+ setBodyScrollLock(false);
43
+ },
44
+ };
45
+
18
46
  return (
19
47
  <Context.Provider
20
48
  value={{
@@ -25,6 +53,7 @@ export default (props) => {
25
53
  setShowTranslationKeys,
26
54
  showTranslationKeys,
27
55
  topListContext,
56
+ cookieConsentContext,
28
57
  }}
29
58
  >
30
59
  {children}
@@ -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');