Session 9: api-football + FootApi + Tank01 adapters, grace period middleware, cookie consent, /pricing page, OOM fix documented (1240 tests)
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
const STORAGE_KEY = 'vyndr_cookie_consent';
|
||||
|
||||
/**
|
||||
* Cookie consent — thin bottom bar shown on first visit. Single line,
|
||||
* dark, dismissable. "Accept" writes a flag to localStorage so the
|
||||
* banner never appears again on this device.
|
||||
*
|
||||
* SSR-safe: we render nothing until the client mounts and the
|
||||
* localStorage check completes. That prevents a hydration mismatch
|
||||
* (server has no `window.localStorage`, so it can't know the user's
|
||||
* prior choice) and avoids the brief banner flash on every refresh
|
||||
* for users who already accepted.
|
||||
*
|
||||
* GDPR posture: VYNDR's cookies are essential (auth + read counter)
|
||||
* plus analytics (anonymized PostHog). We disclose; we don't pre-tick
|
||||
* checkboxes for non-essential analytics. The "Accept" button only
|
||||
* acknowledges that you saw the disclosure.
|
||||
*/
|
||||
export default function CookieConsent() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (window.localStorage.getItem(STORAGE_KEY) !== 'accepted') {
|
||||
setVisible(true);
|
||||
}
|
||||
} catch {
|
||||
// Storage may be unavailable in private mode — fail closed: show
|
||||
// the banner. Cheaper than tracking sessions for these users.
|
||||
setVisible(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function accept() {
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, 'accepted');
|
||||
} catch {
|
||||
/* private mode — banner will reappear next visit; acceptable. */
|
||||
}
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="region"
|
||||
aria-label="Cookie notice"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 60,
|
||||
background: 'rgba(10, 10, 15, 0.96)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
WebkitBackdropFilter: 'blur(12px)',
|
||||
borderTop: '1px solid var(--border)',
|
||||
padding: '12px 16px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 1100,
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 16,
|
||||
flexWrap: 'wrap',
|
||||
fontSize: 13,
|
||||
color: 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
We use cookies for authentication and anonymized analytics.{' '}
|
||||
<Link
|
||||
href="/privacy"
|
||||
style={{ color: 'var(--grade-a)', textDecoration: 'underline', textUnderlineOffset: 2 }}
|
||||
>
|
||||
Privacy policy
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={accept}
|
||||
className="btn-primary"
|
||||
style={{ padding: '6px 14px', fontSize: 12 }}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user