PageSourceSearch

https://utwpolska.pl/zgody.js?v=5

js utwpolska.pl collected 2026-10-06 12:54:03 UTC 16,252 bytes, 339 lines download raw bytes

1/* Zgody na cookies — jeden mechanizm dla serwisu (React) i landingów (statyczny HTML).
2 *
3 * Zasada nadrzędna: przed zgodą NIE ładujemy żadnego skryptu śledzącego.
4 * Baner, który tylko wyświetla komunikat, a tagi i tak lecą, jest gorszy niż brak
5 * banera — daje złudzenie zgodności. Dlatego Google Analytics, Clarity i Meta
6 * Pixel są wstrzykiwane dopiero po decyzji użytkownika, a Google dostaje
7 * dodatkowo Consent Mode v2 z domyślną odmową.
8 *
9 * Plik jest serwowany z /zgody.js (public/), więc korzystają z niego zarówno
10 * strony Reacta, jak i landingi /lp/*. Jedna implementacja = jedna prawda.
11 *
12 * API dla reszty strony:
13 *   window.UTWZgody.ma('analityczne')  -> boolean
14 *   window.UTWZgody.otworz()           -> otwiera okno ustawień
15 */
16(function () {
17  'use strict'
18
19  var COOKIE = 'utw_zgody'
20  var WERSJA = 1
21  var DNI = 365
22
23  var TAGI = window.__UTW_TAGI || {}
24  var GA_ID = TAGI.ga || ''
25  var CLARITY_ID = TAGI.clarity || ''
26  var PIXEL_ID = TAGI.pixel || ''
27
28  // ── Consent Mode v2: domyślnie odmowa, zanim cokolwiek się załaduje ──────
29  window.dataLayer = window.dataLayer || []
30  function gtag() { window.dataLayer.push(arguments) }
31  window.gtag = window.gtag || gtag
32
33  gtag('consent', 'default', {
34    ad_storage: 'denied',
35    ad_user_data: 'denied',
36    ad_personalization: 'denied',
37    analytics_storage: 'denied',
38    functionality_storage: 'granted',
39    security_storage: 'granted',
40    wait_for_update: 500
41  })
42
43  // ── Zapis decyzji ────────────────────────────────────────────────────────
44  function czytaj() {
45    try {
46      var m = document.cookie.match(new RegExp('(?:^|; )' + COOKIE + '=([^;]*)'))
47      if (!m) return null
48      var dane = JSON.parse(decodeURIComponent(m[1]))
49      if (!dane || dane.v !== WERSJA) return null
50      return dane
51    } catch (e) { return null }
52  }
53
54  function zapisz(stan) {
55    var dane = { v: WERSJA, analityczne: !!stan.analityczne, marketingowe: !!stan.marketingowe, data: new Date().toISOString().slice(0, 10) }
56    try {
57      var wygasa = new Date(Date.now() + DNI * 864e5).toUTCString()
58      document.cookie = COOKIE + '=' + encodeURIComponent(JSON.stringify(dane)) +
59        ';expires=' + wygasa + ';path=/;SameSite=Lax' + (location.protocol === 'https:' ? ';Secure' : '')
60    } catch (e) { /* przeglądarka blokuje cookies — decyzja zadziała na tę sesję */ }
61    return dane
62  }
63
64  var stan = czytaj()
65
66  // ── Ładowanie narzędzi (dopiero po zgodzie) ─────────────────────────────
67  var zaladowane = { ga: false, clarity: false, pixel: false }
68
69  function dodajSkrypt(src) {
70    var s = document.createElement('script')
71    s.async = true
72    s.src = src
73    s.setAttribute('data-cfasync', 'false')
74    document.head.appendChild(s)
75    return s
76  }
77
78  function wlaczAnalityke() {
79    if (GA_ID && !zaladowane.ga) {
80      zaladowane.ga = true
81      dodajSkrypt('https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(GA_ID))
82      gtag('js', new Date())
83      gtag('config', GA_ID)
84    }
85    if (CLARITY_ID && !zaladowane.clarity) {
86      zaladowane.clarity = true
87      ;(function (c, l, a, r, i, t, y) {
88        c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments) }
89        t = l.createElement(r); t.async = 1; t.src = 'https://www.clarity.ms/tag/' + i
90        t.setAttribute('data-cfasync', 'false')
91        y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y)
92      })(window, document, 'clarity', 'script', CLARITY_ID)
93    }
94  }
95
96  function wlaczMarketing() {
97    if (!PIXEL_ID || zaladowane.pixel) return
98    zaladowane.pixel = true
99    ;(function (f) {
100      if (f.fbq) return
101      var n = f.fbq = function () { n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments) }
102      if (!f._fbq) f._fbq = n
103      n.push = n; n.loaded = !0; n.version = '2.0'; n.queue = []
104    })(window)
105    dodajSkrypt('https://connect.facebook.net/en_US/fbevents.js')
106    window.fbq('init', PIXEL_ID)
107    window.fbq('track', 'PageView')
108  }
109
110  function zastosuj(s, pierwszeWejscie) {
111    // Bez zgody marketingowej Google i tak wysyła bezciasteczkowe pingi (taki jest
112    // Consent Mode). ads_data_redaction usuwa z nich identyfikatory reklamowe —
113    // to oficjalny mechanizm Google i maksimum, co da się zrobić, nie rezygnując
114    // z analityki (GA4 i Google Ads dzielą jeden skrypt gtag.js).
115    if (!s.marketingowe) gtag('set', 'ads_data_redaction', true)
116
117    gtag('consent', 'update', {
118      ad_storage: s.marketingowe ? 'granted' : 'denied',
119      ad_user_data: s.marketingowe ? 'granted' : 'denied',
120      ad_personalization: s.marketingowe ? 'granted' : 'denied',
121      analytics_storage: s.analityczne ? 'granted' : 'denied'
122    })
123    if (s.analityczne) wlaczAnalityke()
124    if (s.marketingowe) wlaczMarketing()
125
126    // Cofnięcie zgody po tym, jak skrypty już wstały: samo `consent update` nie
127    // usuwa działających nasłuchów, więc odświeżamy stronę — to jedyny uczciwy
128    // sposób, żeby naprawdę przestać zbierać dane.
129    if (!pierwszeWejscie && ((zaladowane.ga && !s.analityczne) || (zaladowane.pixel && !s.marketingowe))) {
130      location.reload()
131    }
132  }
133
134  if (stan) zastosuj(stan, true)
135
136  // ── Interfejs ────────────────────────────────────────────────────────────
137  // Karta w rogu, nie okno modalne: nie ma przyciemnienia tła, nie blokuje
138  // przewijania ani klikania. Strona jest w pełni używalna z widocznym banerem,
139  // a decyzja i tak musi być świadomym kliknięciem — brak decyzji nigdy nie
140  // oznacza zgody (art. 4 pkt 11 RODO, wyrok TSUE C-673/17 Planet49).
141  var STYLE = [
142    '.utwz-karta{position:fixed;z-index:2147483000;left:0;right:0;bottom:0;background:#fff;color:#0B2447;',
143      'font-family:Outfit,system-ui,-apple-system,sans-serif;line-height:1.55;box-shadow:0 -8px 40px -8px rgba(11,36,71,.28);',
144      'border-top:1px solid rgba(11,36,71,.1);padding:20px 20px calc(20px + env(safe-area-inset-bottom,0px));max-height:70vh;overflow-y:auto}',
145    '@media(min-width:900px){.utwz-karta{left:24px;right:auto;bottom:24px;width:400px;border-radius:20px;border:1px solid rgba(11,36,71,.1);padding:24px}}',
146    '.utwz-karta h2{font-family:Georgia,serif;font-size:20px;line-height:1.3;margin:0 0 8px;font-weight:700;padding-right:28px}',
147    '.utwz-karta p{font-size:15.5px;margin:0 0 12px;color:rgba(11,36,71,.75)}',
148    '.utwz-karta a{color:#B8932A;font-weight:700}',
149    '.utwz-zamknij{position:absolute;top:12px;right:12px;width:34px;height:34px;border:0;background:transparent;',
150      'font-size:24px;line-height:1;color:rgba(11,36,71,.45);cursor:pointer;border-radius:10px}',
151    '.utwz-zamknij:hover{background:rgba(11,36,71,.06);color:#0B2447}',
152    '.utwz-btny{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}',
153    '.utwz-btn{flex:1 1 100%;font-family:inherit;font-size:15.5px;font-weight:700;padding:13px 18px;border-radius:12px;',
154      'border:2px solid transparent;cursor:pointer;text-align:center;transition:transform .15s,background .15s}',
155    '.utwz-btn:active{transform:scale(.98)}',
156    '.utwz-btn:focus-visible,.utwz-zamknij:focus-visible{outline:3px solid #D4AF37;outline-offset:2px}',
157    '.utwz-akceptuj{background:#D4AF37;color:#0B2447}',
158    '.utwz-akceptuj:hover{background:#C6A230}',
159    '.utwz-odrzuc{background:#fff;color:#0B2447;border-color:rgba(11,36,71,.22)}',
160    '.utwz-odrzuc:hover{border-color:#0B2447}',
161    '.utwz-ustawienia{flex:1 1 100%;background:transparent;color:rgba(11,36,71,.7);text-decoration:underline;padding:8px;font-size:14.5px}',
162    '.utwz-kat{border:1.5px solid rgba(11,36,71,.12);border-radius:12px;padding:13px 15px;margin-bottom:8px}',
163    '.utwz-kat-gora{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}',
164    '.utwz-kat b{font-size:15.5px;display:block;margin-bottom:2px}',
165    '.utwz-kat p{font-size:14px;margin:0;color:rgba(11,36,71,.65)}',
166    '.utwz-kat small{display:block;font-size:12.5px;color:rgba(11,36,71,.5);margin-top:5px}',
167    '.utwz-przel{position:relative;flex:none;width:52px;height:30px}',
168    '.utwz-przel input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer}',
169    '.utwz-przel span{position:absolute;inset:0;background:rgba(11,36,71,.2);border-radius:999px;transition:background .2s;pointer-events:none}',
170    '.utwz-przel span::after{content:"";position:absolute;top:4px;left:4px;width:22px;height:22px;background:#fff;border-radius:50%;transition:transform .2s}',
171    '.utwz-przel input:checked+span{background:#1F7A4D}',
172    '.utwz-przel input:checked+span::after{transform:translateX(22px)}',
173    '.utwz-przel input:disabled+span{background:#1F7A4D;opacity:.45}',
174    '.utwz-przel input:focus-visible+span{outline:3px solid #D4AF37;outline-offset:3px}',
175    // Przyklejony pasek CTA i baner zgody obydwa siedzą na dole — dopóki trwa
176    // decyzja, pasek chowamy, żeby nie zasłaniały się nawzajem.
177    'html[data-zgody="oczekuje"] .utw-pasek-cta{display:none}'
178  ].join('')
179
180  var okno = null
181
182  function stylKiedyTrzeba() {
183    if (document.getElementById('utwz-style')) return
184    var st = document.createElement('style')
185    st.id = 'utwz-style'
186    st.textContent = STYLE
187    document.head.appendChild(st)
188  }
189
190  function zamknij() {
191    if (!okno) return
192    okno.remove()
193    okno = null
194    document.documentElement.removeAttribute('data-zgody')
195  }
196
197  function decyzja(analityczne, marketingowe) {
198    var wybor = zapisz({ analityczne: analityczne, marketingowe: marketingowe })
199    zamknij()
200    zastosuj(wybor, false)
201  }
202
203  /**
204   * Zamknięcie krzyżykiem NIE jest ani zgodą, ani odmową — chowa kartę na czas
205   * tej sesji przeglądarki, żeby nie wracała na każdej podstronie. Nic się wtedy
206   * nie ładuje, a przy kolejnej wizycie pytamy ponownie. Milczenie nie może
207   * zastąpić zgody, więc nie zapisujemy go jako decyzji.
208   */
209  function odloz() {
210    try { sessionStorage.setItem('utw_zgody_odlozone', '1') } catch (e) {}
211    zamknij()
212  }
213
214  function odlozone() {
215    try { return sessionStorage.getItem('utw_zgody_odlozone') === '1' } catch (e) { return false }
216  }
217
218  function kategoria(id, tytul, opis, narzedzia, zaznaczone, zablokowane) {
219    return '<div class="utwz-kat"><div class="utwz-kat-gora"><div>' +
220        '<b>' + tytul + '</b>' +
221        '<p>' + opis + '</p>' +
222        '<small>' + narzedzia + '</small>' +
223      '</div>' +
224      '<label class="utwz-przel">' +
225        '<input type="checkbox" id="utwz-' + id + '"' + (zaznaczone ? ' checked' : '') + (zablokowane ? ' disabled' : '') +
226        ' aria-label="Zgoda: ' + tytul + '">' +
227        '<span></span>' +
228      '</label></div></div>'
229  }
230
231  function pokaz(tylkoUstawienia) {
232    stylKiedyTrzeba()
233    zamknij()
234
235    var biezacy = czytaj() || { analityczne: false, marketingowe: false }
236    var pierwszePytanie = !czytaj()
237
238    okno = document.createElement('div')
239    okno.className = 'utwz-karta'
240    okno.setAttribute('role', 'region')
241    okno.setAttribute('aria-label', 'Zgoda na pliki cookies')
242    okno.innerHTML =
243      '<button type="button" class="utwz-zamknij" data-akcja="odloz" aria-label="Zamknij — zapytamy przy następnej wizycie">×</button>' +
244      '<h2>Czy możemy używać plików cookies?</h2>' +
245      '<p>Pliki niezbędne są zawsze włączone. Pozostałe pomagają nam sprawdzić, co na stronie działa dobrze, i pokazywać nasze zajęcia osobom, które mogą być nimi zainteresowane. Strona działa tak samo niezależnie od Twojego wyboru.</p>' +
246      '<div id="utwz-kategorie" hidden>' +
247        kategoria('niezbedne', 'Niezbędne', 'Logowanie, formularze i zapamiętanie Twojej decyzji o cookies.', 'Zawsze włączone', true, true) +
248        kategoria('analityczne', 'Analityczne', 'Pokazują, które strony są odwiedzane i gdzie ludzie się gubią.', 'Google Analytics, Microsoft Clarity', biezacy.analityczne, false) +
249        kategoria('marketingowe', 'Marketingowe', 'Pozwalają pokazywać nasze zajęcia w reklamach i mierzyć ich skuteczność.', 'Meta Pixel (Facebook), Google Ads', biezacy.marketingowe, false) +
250      '</div>' +
251      '<div class="utwz-btny">' +
252        '<button type="button" class="utwz-btn utwz-akceptuj" data-akcja="wszystkie">Akceptuję wszystkie</button>' +
253        '<button type="button" class="utwz-btn utwz-odrzuc" data-akcja="zadne">Tylko niezbędne</button>' +
254        '<button type="button" class="utwz-btn utwz-ustawienia" data-akcja="ustawienia">Wybierz kategorie</button>' +
255      '</div>' +
256      '<p style="font-size:13px;margin:12px 0 0;color:rgba(11,36,71,.55)">' +
257        'Zmienisz to w każdej chwili — „Ustawienia cookies” w stopce. ' +
258        '<a href="/polityka-prywatnosci">Polityka prywatności</a>' +
259      '</p>'
260
261    document.body.appendChild(okno)
262    if (pierwszePytanie) document.documentElement.setAttribute('data-zgody', 'oczekuje')
263
264    var kategorie = okno.querySelector('#utwz-kategorie')
265    var przyciskUstawien = okno.querySelector('[data-akcja="ustawienia"]')
266
267    function pokazKategorie() {
268      kategorie.hidden = false
269      przyciskUstawien.textContent = 'Zapisz wybór'
270      przyciskUstawien.dataset.akcja = 'zapisz'
271      przyciskUstawien.className = 'utwz-btn utwz-odrzuc'
272      przyciskUstawien.style.flex = '1 1 100%'
273    }
274
275    if (tylkoUstawienia) pokazKategorie()
276
277    okno.addEventListener('click', function (e) {
278      var btn = e.target.closest ? e.target.closest('[data-akcja]') : null
279      if (!btn) return
280      var akcja = btn.dataset.akcja
281      if (akcja === 'wszystkie') return decyzja(true, true)
282      if (akcja === 'zadne') return decyzja(false, false)
283      if (akcja === 'odloz') return odloz()
284      if (akcja === 'ustawienia') return pokazKategorie()
285      if (akcja === 'zapisz') {
286        return decyzja(
287          okno.querySelector('#utwz-analityczne').checked,
288          okno.querySelector('#utwz-marketingowe').checked
289        )
290      }
291    })
292
293    // Escape chowa kartę (jak krzyżyk) — nie zapisuje żadnej zgody.
294    okno.addEventListener('keydown', function (e) {
295      if (e.key === 'Escape') odloz()
296    })
297  }
298  // ── Link „Ustawienia cookies" ────────────────────────────────────────────
299  // Serwis ma go w stopce Reacta. Landingi to statyczny HTML, więc żeby nikt nie
300  // musiał o nim pamiętać przy kolejnym landingu, dokładamy go automatycznie —
301  // wycofanie zgody musi być tak samo dostępne jak jej udzielenie.
302  function podepnijLinki() {
303    var recznie = document.querySelectorAll('[data-utw-zgody]')
304    for (var i = 0; i < recznie.length; i++) {
305      recznie[i].addEventListener('click', function (e) { e.preventDefault(); pokaz(true) })
306    }
307    if (recznie.length) return
308
309    var stopki = document.getElementsByTagName('footer')
310    if (!stopki.length) return
311    var cel = stopki[stopki.length - 1].querySelector('.legal') || stopki[stopki.length - 1]
312    var a = document.createElement('a')
313    a.href = '#'
314    a.textContent = 'Ustawienia cookies'
315    a.style.cssText = 'display:inline-block;margin-top:8px;text-decoration:underline;cursor:pointer'
316    a.addEventListener('click', function (e) { e.preventDefault(); pokaz(true) })
317    var sep = document.createElement('span')
318    sep.textContent = ' · '
319    cel.appendChild(sep)
320    cel.appendChild(a)
321  }
322
323  // ── Start ────────────────────────────────────────────────────────────────
324  function start() {
325    podepnijLinki()
326    if (!czytaj() && !odlozone()) pokaz(false)
327  }
328
329  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start)
330  else start()
331
332  window.UTWZgody = {
333    ma: function (kategoria) {
334      var s = czytaj()
335      return !!(s && s[kategoria])
336    },
337    otworz: function () { pokaz(true) }
338  }
339})()

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.