Spaces:
Running
Running
| import { useState, useEffect } from 'react'; | |
| import { X, Cookie, Shield } from 'lucide-react'; | |
| interface ConsentSettings { | |
| essential: true; // niezmienne β wymagane do dziaΕania | |
| analytics: boolean; // opcjonalne | |
| marketing: boolean; // opcjonalne | |
| } | |
| const CONSENT_KEY = 'grantforge_cookie_consent'; | |
| const CONSENT_VERSION = '1.0'; // zmieΕ przy aktualizacji polityki β ponowny baner | |
| /** | |
| * Cookie Consent Banner β zgodny z RODO Art. 7 i DyrektywΔ ePrivacy. | |
| * | |
| * Zachowanie: | |
| * - Pojawia siΔ na dole ekranu przy pierwszej wizycie (lub po zmianie wersji polityki) | |
| * - "Akceptuj wszystkie" β zapisuje zgodΔ w localStorage | |
| * - "Tylko niezbΔdne" β tylko essential=true | |
| * - "Ustawienia" β rozwijane opcje szczegΓ³Εowe | |
| * - Nie blokuje korzystania z aplikacji | |
| * | |
| * Integracja z backendem: | |
| * Po zapisaniu zgody wywoΕaj POST /api/user/consent z payload { settings }. | |
| */ | |
| export function CookieConsentBanner() { | |
| const [visible, setVisible] = useState(false); | |
| const [expanded, setExpanded] = useState(false); | |
| const [settings, setSettings] = useState<ConsentSettings>({ | |
| essential: true, | |
| analytics: false, | |
| marketing: false, | |
| }); | |
| useEffect(() => { | |
| try { | |
| const stored = localStorage.getItem(CONSENT_KEY); | |
| if (stored) { | |
| const parsed = JSON.parse(stored); | |
| // Pokazuj ponownie jeΕli wersja siΔ zmieniΕa | |
| if (parsed.version !== CONSENT_VERSION) { | |
| setVisible(true); | |
| } | |
| } else { | |
| // Pierwsze wejΕcie | |
| setVisible(true); | |
| } | |
| } catch { | |
| setVisible(true); | |
| } | |
| }, []); | |
| const saveConsent = (chosen: ConsentSettings) => { | |
| const record = { version: CONSENT_VERSION, ...chosen, timestamp: new Date().toISOString() }; | |
| localStorage.setItem(CONSENT_KEY, JSON.stringify(record)); | |
| setVisible(false); | |
| // Opcjonalnie: wyΕlij do backendu | |
| try { | |
| fetch('/api/user/consent', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ consent: record }), | |
| }).catch(() => {}); // fire-and-forget | |
| } catch {} | |
| }; | |
| if (!visible) return null; | |
| return ( | |
| <div | |
| role="dialog" | |
| aria-modal="false" | |
| aria-label="Zgoda na pliki cookie" | |
| style={{ | |
| position: 'fixed', | |
| bottom: 0, | |
| left: 0, | |
| right: 0, | |
| zIndex: 9999, | |
| padding: '1rem', | |
| display: 'flex', | |
| justifyContent: 'center', | |
| pointerEvents: 'none', | |
| }} | |
| > | |
| <div | |
| style={{ | |
| background: 'rgba(15, 15, 25, 0.97)', | |
| backdropFilter: 'blur(16px)', | |
| border: '1px solid rgba(255,255,255,0.08)', | |
| borderRadius: '16px', | |
| padding: '1.5rem', | |
| maxWidth: '780px', | |
| width: '100%', | |
| boxShadow: '0 -4px 40px rgba(0,0,0,0.5)', | |
| pointerEvents: 'auto', | |
| animation: 'slideUp 0.3s ease-out', | |
| }} | |
| > | |
| {/* Header */} | |
| <div style={{ display: 'flex', alignItems: 'flex-start', gap: '0.75rem', marginBottom: '1rem' }}> | |
| <div style={{ | |
| width: 36, height: 36, borderRadius: '8px', | |
| background: 'rgba(139,92,246,0.15)', | |
| display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, | |
| }}> | |
| <Cookie size={18} color="#a78bfa" /> | |
| </div> | |
| <div style={{ flex: 1 }}> | |
| <h3 style={{ margin: 0, color: '#f1f5f9', fontSize: '0.95rem', fontWeight: 700 }}> | |
| Twoja prywatnoΕΔ ma znaczenie | |
| </h3> | |
| <p style={{ margin: '0.25rem 0 0', color: '#94a3b8', fontSize: '0.8rem', lineHeight: 1.5 }}> | |
| UΕΌywamy plikΓ³w cookie, aby zapewniΔ wΕaΕciwe dziaΕanie platformy. MoΕΌesz wybraΔ, | |
| ktΓ³re kategorie akceptujesz.{' '} | |
| <a href="/polityka-prywatnosci" style={{ color: '#a78bfa', textDecoration: 'none' }}> | |
| Polityka prywatnoΕci | |
| </a> | |
| </p> | |
| </div> | |
| <button | |
| onClick={() => saveConsent({ essential: true, analytics: false, marketing: false })} | |
| aria-label="Zamknij (tylko niezbΔdne)" | |
| style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#64748b', padding: 4 }} | |
| > | |
| <X size={16} /> | |
| </button> | |
| </div> | |
| {/* SzczegΓ³Εy rozwijane */} | |
| {expanded && ( | |
| <div style={{ | |
| borderTop: '1px solid rgba(255,255,255,0.06)', | |
| paddingTop: '1rem', | |
| marginBottom: '1rem', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| gap: '0.6rem', | |
| }}> | |
| {/* Essential β zawsze wΕΔ czone */} | |
| <ConsentRow | |
| label="NiezbΔdne" | |
| description="Sesja, uwierzytelnianie, bezpieczeΕstwo CSRF. Wymagane do dziaΕania platformy." | |
| icon={<Shield size={13} color="#34d399" />} | |
| checked={true} | |
| disabled={true} | |
| onChange={() => {}} | |
| /> | |
| {/* Analytics */} | |
| <ConsentRow | |
| label="Analityczne" | |
| description="Anonimowe statystyki uΕΌytkowania (np. LangSmith tracing β bez danych osobowych)." | |
| icon={<span style={{ fontSize: 13 }}>π</span>} | |
| checked={settings.analytics} | |
| disabled={false} | |
| onChange={(v) => setSettings(s => ({ ...s, analytics: v }))} | |
| /> | |
| {/* Marketing */} | |
| <ConsentRow | |
| label="Marketingowe" | |
| description="Personalizacja oferty i powiadomieΕ o nowych funkcjach. Opcjonalne." | |
| icon={<span style={{ fontSize: 13 }}>π£</span>} | |
| checked={settings.marketing} | |
| disabled={false} | |
| onChange={(v) => setSettings(s => ({ ...s, marketing: v }))} | |
| /> | |
| </div> | |
| )} | |
| {/* Przyciski */} | |
| <div style={{ | |
| display: 'flex', | |
| gap: '0.6rem', | |
| flexWrap: 'wrap', | |
| justifyContent: 'flex-end', | |
| }}> | |
| <button | |
| onClick={() => setExpanded(e => !e)} | |
| style={{ | |
| background: 'transparent', | |
| border: '1px solid rgba(255,255,255,0.1)', | |
| borderRadius: '8px', | |
| padding: '0.5rem 1rem', | |
| color: '#94a3b8', | |
| cursor: 'pointer', | |
| fontSize: '0.8rem', | |
| fontWeight: 500, | |
| }} | |
| > | |
| {expanded ? 'Ukryj ustawienia' : 'Ustawienia'} | |
| </button> | |
| <button | |
| onClick={() => saveConsent({ essential: true, analytics: false, marketing: false })} | |
| style={{ | |
| background: 'rgba(255,255,255,0.05)', | |
| border: '1px solid rgba(255,255,255,0.1)', | |
| borderRadius: '8px', | |
| padding: '0.5rem 1rem', | |
| color: '#cbd5e1', | |
| cursor: 'pointer', | |
| fontSize: '0.8rem', | |
| fontWeight: 500, | |
| }} | |
| > | |
| Tylko niezbΔdne | |
| </button> | |
| <button | |
| onClick={() => saveConsent({ essential: true, analytics: true, marketing: true })} | |
| style={{ | |
| background: 'linear-gradient(135deg, #7c3aed, #2563eb)', | |
| border: 'none', | |
| borderRadius: '8px', | |
| padding: '0.5rem 1.25rem', | |
| color: '#fff', | |
| cursor: 'pointer', | |
| fontSize: '0.8rem', | |
| fontWeight: 600, | |
| }} | |
| > | |
| Akceptuj wszystkie | |
| </button> | |
| </div> | |
| </div> | |
| <style>{` | |
| @keyframes slideUp { | |
| from { transform: translateY(20px); opacity: 0; } | |
| to { transform: translateY(0); opacity: 1; } | |
| } | |
| `}</style> | |
| </div> | |
| ); | |
| } | |
| // ββ Pomocniczy wiersz ustawienia βββββββββββββββββββββββββββββββββββββββββββββ | |
| function ConsentRow({ | |
| label, description, icon, checked, disabled, onChange | |
| }: { | |
| label: string; | |
| description: string; | |
| icon: React.ReactNode; | |
| checked: boolean; | |
| disabled: boolean; | |
| onChange: (v: boolean) => void; | |
| }) { | |
| return ( | |
| <div style={{ | |
| display: 'flex', | |
| alignItems: 'flex-start', | |
| gap: '0.75rem', | |
| padding: '0.6rem 0.75rem', | |
| borderRadius: '8px', | |
| background: 'rgba(255,255,255,0.03)', | |
| border: '1px solid rgba(255,255,255,0.05)', | |
| }}> | |
| <div style={{ marginTop: 2 }}>{icon}</div> | |
| <div style={{ flex: 1 }}> | |
| <div style={{ color: '#e2e8f0', fontSize: '0.82rem', fontWeight: 600 }}>{label}</div> | |
| <div style={{ color: '#64748b', fontSize: '0.75rem', marginTop: 2 }}>{description}</div> | |
| </div> | |
| <label style={{ position: 'relative', display: 'inline-flex', alignItems: 'center', cursor: disabled ? 'not-allowed' : 'pointer' }}> | |
| <input | |
| type="checkbox" | |
| checked={checked} | |
| disabled={disabled} | |
| onChange={e => onChange(e.target.checked)} | |
| style={{ opacity: 0, width: 0, height: 0 }} | |
| /> | |
| <span style={{ | |
| display: 'inline-block', | |
| width: 36, | |
| height: 20, | |
| borderRadius: 10, | |
| background: checked ? '#7c3aed' : 'rgba(255,255,255,0.1)', | |
| position: 'relative', | |
| transition: 'background 0.2s', | |
| opacity: disabled ? 0.5 : 1, | |
| }}> | |
| <span style={{ | |
| position: 'absolute', | |
| top: 2, | |
| left: checked ? 18 : 2, | |
| width: 16, | |
| height: 16, | |
| borderRadius: '50%', | |
| background: '#fff', | |
| transition: 'left 0.2s', | |
| }} /> | |
| </span> | |
| </label> | |
| </div> | |
| ); | |
| } | |
| export default CookieConsentBanner; | |