PageSourceSearch

https://financepartners.ai/js/cookie-consent.js

js financepartners.ai collected 2026-10-05 15:11:30 UTC 15,454 bytes, 367 lines download raw bytes

1/**
2 * Finance Partners — Cookie Consent Manager
3 *
4 * Compliant with GDPR + Provvedimento Garante Privacy 10/06/2021:
5 *  - Third-party scripts (Calendly) are NOT loaded until user opt-in.
6 *  - Closing or ignoring the banner = refusal (only technical cookies).
7 *  - Explicit "Solo necessari" button rejects non-essential cookies.
8 *  - User can revoke/change preferences at any time via footer link.
9 */
10(function () {
11    'use strict';
12
13    var STORAGE_KEY = 'fp_cookie_consent';
14    var CONSENT_VERSION = 1;
15    var CALENDLY_BOOKING_URL = 'https://calendly.com/m-nardini-financepartners/30min?hide_gdpr_banner=1';
16
17    var I18N = {
18        it: {
19            banner_title: 'Usiamo i cookie',
20            banner_text: 'Questo sito utilizza cookie tecnici necessari al funzionamento e — solo previo tuo consenso — cookie di terze parti (Calendly) per la prenotazione di call. Non usiamo cookie di profilazione o pubblicitari. Maggiori dettagli nella <a href="cookie-policy.html">Cookie Policy</a>.',
21            accept_all: 'Accetta tutti',
22            accept_necessary: 'Solo necessari',
23            customize: 'Personalizza',
24            modal_title: 'Preferenze cookie',
25            modal_intro: 'Scegli quali cookie autorizzare. Puoi modificare la scelta in qualsiasi momento dal link "Gestisci cookie" nel footer.',
26            cat_necessary_title: 'Cookie tecnici (sempre attivi)',
27            cat_necessary_desc: 'Necessari al funzionamento del sito: memorizzazione della lingua, sessione, sicurezza. Non richiedono consenso ai sensi dell\'Art. 122 del Codice Privacy.',
28            cat_necessary_badge: 'Sempre attivi',
29            cat_third_title: 'Cookie di terze parti — Calendly',
30            cat_third_desc: 'Permettono di prenotare una call direttamente dal sito tramite il widget Calendly. Calendly può impostare cookie propri e ricevere il tuo indirizzo IP. Senza il tuo consenso il widget non viene caricato.',
31            save: 'Salva preferenze',
32            booking_blocked_title: 'Servizio bloccato',
33            booking_blocked_text: 'Per prenotare una call online occorre accettare i cookie di terze parti (Calendly). In alternativa puoi scriverci a <a href="mailto:[email protected]">[email protected]</a>.',
34            booking_blocked_accept: 'Accetta e prenota',
35            booking_blocked_cancel: 'Annulla',
36            manage: 'Gestisci cookie'
37        },
38        en: {
39            banner_title: 'We use cookies',
40            banner_text: 'This site uses strictly necessary technical cookies and — only with your consent — third-party cookies (Calendly) to book a call. We do not use profiling or advertising cookies. Details in our <a href="cookie-policy.html">Cookie Policy</a>.',
41            accept_all: 'Accept all',
42            accept_necessary: 'Necessary only',
43            customize: 'Customize',
44            modal_title: 'Cookie preferences',
45            modal_intro: 'Choose which cookies to allow. You can change your choice at any time using the "Manage cookies" link in the footer.',
46            cat_necessary_title: 'Technical cookies (always on)',
47            cat_necessary_desc: 'Required for the site to function: language storage, session, security. No consent required under Art. 122 of the Italian Privacy Code.',
48            cat_necessary_badge: 'Always on',
49            cat_third_title: 'Third-party cookies — Calendly',
50            cat_third_desc: 'Let you book a call directly through the Calendly widget. Calendly may set its own cookies and receive your IP address. The widget is not loaded without your consent.',
51            save: 'Save preferences',
52            booking_blocked_title: 'Service blocked',
53            booking_blocked_text: 'To book a call online you need to accept third-party cookies (Calendly). Alternatively, email us at <a href="mailto:[email protected]">[email protected]</a>.',
54            booking_blocked_accept: 'Accept and book',
55            booking_blocked_cancel: 'Cancel',
56            manage: 'Manage cookies'
57        }
58    };
59
60    function currentLang() {
61        try {
62            var stored = localStorage.getItem('fp_lang') || localStorage.getItem('fp_language');
63            if (stored === 'en') return 'en';
64            return 'it';
65        } catch (e) { return 'it'; }
66    }
67
68    function t(key) {
69        var lang = currentLang();
70        return (I18N[lang] && I18N[lang][key]) || I18N.it[key] || key;
71    }
72
73    function readConsent() {
74        try {
75            var raw = localStorage.getItem(STORAGE_KEY);
76            if (!raw) return null;
77            var parsed = JSON.parse(raw);
78            if (!parsed || parsed.version !== CONSENT_VERSION) return null;
79            return parsed;
80        } catch (e) { return null; }
81    }
82
83    function writeConsent(thirdParty) {
84        var data = {
85            version: CONSENT_VERSION,
86            timestamp: new Date().toISOString(),
87            necessary: true,
88            thirdParty: !!thirdParty
89        };
90        try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch (e) { /* ignore */ }
91        return data;
92    }
93
94    function hasThirdPartyConsent() {
95        var c = readConsent();
96        return !!(c && c.thirdParty);
97    }
98
99    /* ────────── Calendly lazy-loader ────────── */
100    var calendlyLoaded = false;
101    var calendlyLoading = false;
102    var calendlyCallbacks = [];
103
104    function loadCalendly(onReady) {
105        if (calendlyLoaded) { onReady && onReady(); return; }
106        if (onReady) calendlyCallbacks.push(onReady);
107        if (calendlyLoading) return;
108        calendlyLoading = true;
109
110        var css = document.createElement('link');
111        css.rel = 'stylesheet';
112        css.href = 'https://assets.calendly.com/assets/external/widget.css';
113        document.head.appendChild(css);
114
115        var js = document.createElement('script');
116        js.src = 'https://assets.calendly.com/assets/external/widget.js';
117        js.async = true;
118        js.onload = function () {
119            calendlyLoaded = true;
120            calendlyLoading = false;
121            calendlyCallbacks.forEach(function (cb) { try { cb(); } catch (e) {} });
122            calendlyCallbacks = [];
123        };
124        document.head.appendChild(js);
125    }
126
127    function openCalendly(url) {
128        var target = url || CALENDLY_BOOKING_URL;
129        loadCalendly(function () {
130            if (window.Calendly && typeof window.Calendly.initPopupWidget === 'function') {
131                window.Calendly.initPopupWidget({ url: target });
132            }
133        });
134    }
135
136    /* ────────── DOM rendering ────────── */
137    function el(tag, attrs, children) {
138        var node = document.createElement(tag);
139        if (attrs) {
140            Object.keys(attrs).forEach(function (k) {
141                if (k === 'class') node.className = attrs[k];
142                else if (k === 'html') node.innerHTML = attrs[k];
143                else if (k.indexOf('on') === 0 && typeof attrs[k] === 'function') {
144                    node.addEventListener(k.slice(2), attrs[k]);
145                } else node.setAttribute(k, attrs[k]);
146            });
147        }
148        (children || []).forEach(function (c) {
149            if (c == null) return;
150            node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
151        });
152        return node;
153    }
154
155    var bannerNode = null;
156    var modalNode = null;
157
158    function buildBanner() {
159        if (bannerNode) return bannerNode;
160        bannerNode = el('div', { class: 'fp-cc-banner', role: 'dialog', 'aria-live': 'polite', 'aria-label': t('banner_title') }, [
161            el('h2', { class: 'fp-cc-title' }, [t('banner_title')]),
162            el('p', { class: 'fp-cc-text', html: t('banner_text') }),
163            el('div', { class: 'fp-cc-actions' }, [
164                el('button', {
165                    class: 'fp-cc-btn fp-cc-btn-primary',
166                    type: 'button',
167                    onclick: function () { acceptAll(); }
168                }, [t('accept_all')]),
169                el('button', {
170                    class: 'fp-cc-btn fp-cc-btn-secondary',
171                    type: 'button',
172                    onclick: function () { acceptNecessary(); }
173                }, [t('accept_necessary')]),
174                el('button', {
175                    class: 'fp-cc-btn fp-cc-btn-link',
176                    type: 'button',
177                    onclick: function () { hideBanner(); openPreferences(); }
178                }, [t('customize')])
179            ])
180        ]);
181        document.body.appendChild(bannerNode);
182        return bannerNode;
183    }
184
185    function showBanner() {
186        buildBanner();
187        requestAnimationFrame(function () {
188            bannerNode.classList.add('fp-cc-visible');
189        });
190    }
191
192    function hideBanner() {
193        if (!bannerNode) return;
194        bannerNode.classList.remove('fp-cc-visible');
195        setTimeout(function () {
196            if (bannerNode && bannerNode.parentNode) bannerNode.parentNode.removeChild(bannerNode);
197            bannerNode = null;
198        }, 400);
199    }
200
201    function buildModal() {
202        if (modalNode) return modalNode;
203
204        var thirdPartyChecked = hasThirdPartyConsent();
205
206        var toggle = el('label', { class: 'fp-cc-switch' }, []);
207        var input = el('input', { type: 'checkbox' });
208        if (thirdPartyChecked) input.setAttribute('checked', 'checked');
209        var slider = el('span', { class: 'fp-cc-switch-slider' }, []);
210        toggle.appendChild(input);
211        toggle.appendChild(slider);
212
213        var modalBox = el('div', { class: 'fp-cc-modal', role: 'dialog', 'aria-modal'
213: 'true', 'aria-label': t('modal_title') }, [
214            el('h2', null, [t('modal_title')]),
215            el('p', { class: 'fp-cc-modal-intro' }, [t('modal_intro')]),
216            el('div', { class: 'fp-cc-category' }, [
217                el('div', { class: 'fp-cc-category-head' }, [
218                    el('span', { class: 'fp-cc-category-title' }, [t('cat_necessary_title')]),
219                    el('span', { class: 'fp-cc-badge-always' }, [t('cat_necessary_badge')])
220                ]),
221                el('p', { class: 'fp-cc-category-desc' }, [t('cat_necessary_desc')])
222            ]),
223            el('div', { class: 'fp-cc-category' }, [
224                el('div', { class: 'fp-cc-category-head' }, [
225                    el('span', { class: 'fp-cc-category-title' }, [t('cat_third_title')]),
226                    toggle
227                ]),
228                el('p', { class: 'fp-cc-category-desc' }, [t('cat_third_desc')])
229            ]),
230            el('div', { class: 'fp-cc-modal-actions' }, [
231                el('button', {
232                    class: 'fp-cc-btn fp-cc-btn-secondary',
233                    type: 'button',
234                    onclick: function () { closePreferences(); }
235                }, [t('accept_necessary')]),
236                el('button', {
237                    class: 'fp-cc-btn fp-cc-btn-primary',
238                    type: 'button',
239                    onclick: function () {
240                        writeConsent(!!input.checked);
241                        closePreferences();
242                    }
243                }, [t('save')])
244            ])
245        ]);
246
247        modalNode = el('div', {
248            class: 'fp-cc-modal-overlay',
249            onclick: function (e) { if (e.target === modalNode) closePreferences(); }
250        }, [modalBox]);
251
252        document.body.appendChild(modalNode);
253        return modalNode;
254    }
255
256    function openPreferences() {
257        buildModal();
258        requestAnimationFrame(function () {
259            modalNode.classList.add('fp-cc-visible');
260        });
261    }
262
263    function closePreferences() {
264        if (!modalNode) return;
265        modalNode.classList.remove('fp-cc-visible');
266        setTimeout(function () {
267            if (modalNode && modalNode.parentNode) modalNode.parentNode.removeChild(modalNode);
268            modalNode = null;
269        }, 250);
270    }
271
272    function acceptAll() { writeConsent(true); hideBanner(); }
273    function acceptNecessary() { writeConsent(false); hideBanner(); }
274
275    /* ────────── Booking gate ────────── */
276    function bookCall(url) {
277        if (hasThirdPartyConsent()) {
278            openCalendly(url);
279            return;
280        }
281        showBookingGate(url);
282    }
283
284    var gateNode = null;
285    function showBookingGate(url) {
286        if (gateNode) return;
287        var box = el('div', { class: 'fp-cc-modal' }, [
288            el('h2', null, [t('booking_blocked_title')]),
289            el('p', { class: 'fp-cc-modal-intro', html: t('booking_blocked_text') }),
290            el('div', { class: 'fp-cc-modal-actions' }, [
291                el('button', {
292                    class: 'fp-cc-btn fp-cc-btn-secondary',
293                    type: 'button',
294                    onclick: function () { closeBookingGate(); }
295                }, [t('booking_blocked_cancel')]),
296                el('button', {
297                    class: 'fp-cc-btn fp-cc-btn-primary',
298                    type: 'button',
299                    onclick: function () {
300                        writeConsent(true);
301                        closeBookingGate();
302                        openCalendly(url);
303                    }
304                }, [t('booking_blocked_accept')])
305            ])
306        ]);
307        gateNode = el('div', {
308            class: 'fp-cc-modal-overlay',
309            onclick: function (e) { if (e.target === gateNode) closeBookingGate(); }
310        }, [box]);
311        document.body.appendChild(gateNode);
312        requestAnimationFrame(function () { gateNode.classList.add('fp-cc-visible'); });
313    }
314
315    function closeBookingGate() {
316        if (!gateNode) return;
317        gateNode.classList.remove('fp-cc-visible');
318        setTimeout(function () {
319            if (gateNode && gateNode.parentNode) gateNode.parentNode.removeChild(gateNode);
320            gateNode = null;
321        }, 250);
322    }
323
324    /* ────────── Footer link auto-wiring ────────── */
325    function wireFooterLinks() {
326        var nodes = document.querySelectorAll('[data-fp-cc-open]');
327        for (var i = 0; i < nodes.length; i++) {
328            (function (node) {
329                if (node.__fpCcWired) return;
330                node.__fpCcWired = true;
331                node.addEventListener('click', function (e) {
332                    e.preventDefault();
333                    openPreferences();
334                });
335            })(nodes[i]);
336        }
337    }
338
339    /* ────────── Boot ────────── */
340    function init() {
341        wireFooterLinks();
342        var consent = readConsent();
343        if (!consent) {
344            showBanner();
345        } else if (consent.thirdParty) {
346            // Pre-warm Calendly so the popup is instant when clicked
347            loadCalendly();
348        }
349    }
350
351    if (document.readyState === 'loading') {
352        document.addEventListener('DOMContentLoaded', init);
353    } else {
354        init();
355    }
356
357    /* ────────── Public API ────────── */
358    window.FPCookieConsent = {
359        bookCall: bookCall,
360        openPreferences: openPreferences,
361        hasThirdPartyConsent: hasThirdPartyConsent,
362        revoke: function () {
363            try { localStorage.removeItem(STORAGE_KEY); } catch (e) {}
364            showBanner();
365        }
366    };
367})();

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.