makefuture / games / accept-the-cookie / game / systems / consent.js
375 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { CONSENT_CATEGORIES } from './session.js?v=20260427c';

export function showConsentModal(session) {
    return new Promise((resolve) => {
        const startedAt = performance.now();
        const backdrop = document.createElement('div');
        backdrop.className = 'consent-backdrop';
        backdrop.setAttribute('role', 'dialog');
        backdrop.setAttribute('aria-modal', 'true');
        backdrop.setAttribute('aria-labelledby', 'cookie-title');

        const initial = session.replayPrivacyMode
            ? { analytics: false, personalization: false, marketing: false }
            : { analytics: true, personalization: true, marketing: true };

        const preferences = {
            necessary: true,
            analytics: initial.analytics,
            personalization: initial.personalization,
            marketing: initial.marketing,
        };

        backdrop.innerHTML = `
            <section class="consent-panel">
                <header class="consent-head">
                    <h1 class="consent-title" id="cookie-title">We use cookies to improve your experience</h1>
                    <p class="consent-copy">We use necessary cookies to make this game work. With your permission, we also use analytics, personalization, and marketing-style signals to adapt gameplay and show your privacy receipt at the end.</p>
                </header>
                <div class="consent-body" data-consent-body>
                    ${Object.entries(CONSENT_CATEGORIES).map(([key, category]) => categoryTemplate(key, category, preferences[key])).join('')}
                </div>
                <footer class="consent-actions">
                    <button class="link-button" type="button" data-action="manage">Manage Preferences</button>
                    <button class="secondary-button" type="button" data-action="reject">Reject Optional</button>
                    <button class="secondary-button" type="button" data-action="save">Save Choices</button>
                    <button class="primary-button" type="button" data-action="accept">Accept All</button>
                </footer>
            </section>
        `;

        const body = backdrop.querySelector('[data-consent-body]');
        const toggles = Array.from(backdrop.querySelectorAll('[data-toggle]'));
        const actions = Array.from(backdrop.querySelectorAll('[data-action]'));

        body.addEventListener('scroll', () => {
            const max = Math.max(1, body.scrollHeight - body.clientHeight);
            session.consentTelemetry.scrolled = true;
            session.consentTelemetry.maxScrollDepth = Math.max(
                session.consentTelemetry.maxScrollDepth,
                Math.round((body.scrollTop / max) * 100),
            );
        }, { passive: true });

        toggles.forEach((toggle) => {
            toggle.addEventListener('click', () => {
                const key = toggle.dataset.toggle;
                if (CONSENT_LOCKED_KEYS.has(key)) return;
                preferences[key] = !preferences[key];
                toggle.setAttribute('aria-pressed', String(preferences[key]));
                recordClick(session, `toggle:${key}:${preferences[key] ? 'on' : 'off'}`);
            });
        });

        actions.forEach((button) => {
            button.addEventListener('click', () => {
                const action = button.dataset.action;
                recordClick(session, action);
                if (action === 'manage') {
                    body.scrollTo({ top: 0, behavior: 'smooth' });
                    return;
                }
                if (action === 'accept') {
                    preferences.analytics = true;
                    preferences.personalization = true;
                    preferences.marketing = true;
                    finish('Accepted all');
                    return;
                }
                if (action === 'reject') {
                    preferences.analytics = false;
                    preferences.personalization = false;
                    preferences.marketing = false;
                    finish('Rejected optional');
                    return;
                }
                finish(choiceLabel(preferences));
            });
        });

        document.body.appendChild(backdrop);
        backdrop.querySelector('[data-action="accept"]').focus();

        function finish(choice) {
            session.consent = { ...preferences };
            session.consentTelemetry.choice = choice;
            session.consentTelemetry.decisionMs = Math.round(performance.now() - startedAt);
            backdrop.remove();
            resolve(session.consent);
        }
    });
}

const CONSENT_LOCKED_KEYS = new Set(['necessary']);

function categoryTemplate(key, category, enabled) {
    return `
        <article class="consent-category">
            <div>
                <h3>${category.label}</h3>
                <p>${category.description}</p>
            </div>
            <button class="toggle" type="button" data-toggle="${key}" aria-label="${category.label}" aria-pressed="${enabled}" ${category.required ? 'disabled' : ''}></button>
        </article>
    `;
}

function recordClick(session, label) {
    session.consentTelemetry.clickPath.push({
        label,
        t: Math.round(performance.now()),
    });
}

function choiceLabel(preferences) {
    const optional = ['analytics', 'personalization', 'marketing'];
    if (optional.every((key) => preferences[key])) return 'Accepted all';
    if (optional.every((key) => !preferences[key])) return 'Rejected optional';
    return 'Customized';
}

static mirror of HEAD · about · clone: git clone https://git.makefuture.ro/makefuture.git