Tema ayarları eklendi
This commit is contained in:
parent
48cabcf293
commit
9a001197e1
29
.github/workflows/ci.yml
vendored
Normal file
29
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@ node_modules/
|
|||
.env
|
||||
dist/
|
||||
coverage/
|
||||
.next/
|
||||
|
|
|
|||
40
CHANGELOG.md
Normal file
40
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project are documented in this file.
|
||||
|
||||
## [1.1.0] - 2026-06-20
|
||||
|
||||
### Added
|
||||
|
||||
- `theme` prop on `CookieConsent` for brand color customization via CSS variables
|
||||
- `className` prop on `CookieConsent`
|
||||
- Optional `cookies` prop with expandable cookie inventory tables per category
|
||||
- New label keys for cookie table headers and storage error messages
|
||||
- `onConsentSaved` and `onConsentError` callbacks on `CookieConsent`
|
||||
- `PersistResult` return values from `setConsent`, `acceptAll`, and `rejectAll`
|
||||
- `configureConsent({ onStorageError })` global error callback
|
||||
- `debug: true` console warnings for invalid or failed consent writes
|
||||
- `ads_data_redaction` and `url_passthrough` in bootstrap default script
|
||||
- Multi-tab consent sync via the `storage` event
|
||||
- Keyboard support: `Escape` closes preferences or returns to banner
|
||||
- Focus management when the banner opens
|
||||
- TypeScript definitions for `@pdntechnology/cookie-consent/react`
|
||||
- Cookie schema helpers exported from the main package
|
||||
- Vitest test suite and GitHub Actions CI
|
||||
- Next.js demo app in `examples/next-app`
|
||||
- Turkish documentation in `README.tr.md`
|
||||
- `CONTRIBUTING.md`
|
||||
|
||||
### Changed
|
||||
|
||||
- Banner styles now use CSS custom properties instead of hardcoded colors
|
||||
- Bootstrap script includes advanced Consent Mode settings by default
|
||||
|
||||
## [1.0.0] - 2026-06-20
|
||||
|
||||
### Added
|
||||
|
||||
- Initial release with Google Consent Mode v2 integration
|
||||
- React banner and Next.js bootstrap components
|
||||
- Optional GTM and Meta Pixel loaders
|
||||
- Framework-agnostic consent API
|
||||
72
CONTRIBUTING.md
Normal file
72
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Contributing
|
||||
|
||||
Thank you for considering a contribution to `@pdntechnology/cookie-consent`.
|
||||
|
||||
## Before you start
|
||||
|
||||
- Open an issue for large changes so we can agree on the approach first.
|
||||
- Keep pull requests focused. One feature or fix per PR is easier to review.
|
||||
- Match the existing code style: small modules, clear names, minimal dependencies.
|
||||
|
||||
## Development setup
|
||||
|
||||
```bash
|
||||
git clone https://git.pdntechnology.com.tr/pdn/cookie-consent.git
|
||||
cd cookie-consent
|
||||
npm install
|
||||
npm test
|
||||
```
|
||||
|
||||
### Try the Next.js demo
|
||||
|
||||
```bash
|
||||
cd examples/next-app
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The example uses a local file dependency on the parent package.
|
||||
|
||||
## Tests
|
||||
|
||||
All changes should pass the test suite:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Add or update tests when you change behaviour in:
|
||||
|
||||
- `src/preferences.js`
|
||||
- `src/storage.js`
|
||||
- `src/consent-api.js`
|
||||
- `src/google/*`
|
||||
- `src/cookie-schema.js`
|
||||
- `src/react/theme.js`
|
||||
|
||||
## Pull request checklist
|
||||
|
||||
- [ ] `npm test` passes locally
|
||||
- [ ] Public API changes are reflected in `src/index.d.ts` and/or `src/react/index.d.ts`
|
||||
- [ ] User-facing changes are documented in `README.md`
|
||||
- [ ] Turkish docs updated in `README.tr.md` when behaviour or public API changes
|
||||
- [ ] `CHANGELOG.md` updated under `[Unreleased]` or the next version
|
||||
|
||||
## Commit messages
|
||||
|
||||
Use clear, imperative messages:
|
||||
|
||||
- `Add theme prop to CookieConsent`
|
||||
- `Fix bootstrap script advanced settings`
|
||||
- `Document cookie inventory API`
|
||||
|
||||
## Code guidelines
|
||||
|
||||
- Prefer framework-agnostic logic in `src/` and React-specific UI in `src/react/`
|
||||
- Do not add runtime dependencies without a strong reason
|
||||
- Fail safely: never throw from public consent APIs in the browser
|
||||
- Surface storage failures via return values, callbacks, or debug logging
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).
|
||||
187
README.md
187
README.md
|
|
@ -29,12 +29,16 @@ This library implements that flow in ~15 KB of readable JavaScript.
|
|||
|
||||
- Google Consent Mode v2 (all 7 signals)
|
||||
- Granular categories: Necessary / Analytics / Marketing
|
||||
- `ads_data_redaction` + `url_passthrough` when marketing is denied
|
||||
- Customizable banner text, theme colors, and cookie inventory
|
||||
- `ads_data_redaction` + `url_passthrough` in bootstrap and on update
|
||||
- Regional default consent (EEA, etc.)
|
||||
- React banner + Next.js bootstrap component
|
||||
- Optional GTM and Meta Pixel loaders (consent-gated)
|
||||
- Framework-agnostic core API (vanilla JS supported)
|
||||
- TypeScript definitions included
|
||||
- TypeScript definitions for core API and React components
|
||||
- Error callbacks when consent cannot be saved
|
||||
|
||||
> Turkish documentation: [README.tr.md](README.tr.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -81,7 +85,7 @@ Import this file in your root layout **before** `ConsentBootstrap`.
|
|||
// app/layout.jsx
|
||||
import "@pdntechnology/cookie-consent/styles.css";
|
||||
import "@/lib/consent-config";
|
||||
import ConsentBootstrap from "@pdntechnology/cookie-consent/react/ConsentBootstrap";
|
||||
import { ConsentBootstrap } from "@pdntechnology/cookie-consent/react";
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
|
|
@ -136,7 +140,142 @@ import { openPreferences } from "@pdntechnology/cookie-consent";
|
|||
|
||||
---
|
||||
|
||||
## How categories map to Google signals
|
||||
## Customization
|
||||
|
||||
### Text and policy URL
|
||||
|
||||
Every visible string can be overridden via the `labels` prop. The policy link uses `policyHref`.
|
||||
|
||||
```jsx
|
||||
<CookieConsent
|
||||
policyHref="/privacy"
|
||||
labels={{
|
||||
title: "Cookie preferences",
|
||||
message: "We use cookies to improve your experience.",
|
||||
policyLink: "Privacy policy",
|
||||
accept: "Accept all",
|
||||
reject: "Reject all",
|
||||
manage: "Manage preferences",
|
||||
save: "Save preferences",
|
||||
necessaryTitle: "Necessary",
|
||||
necessaryDescription: "Required for core site functionality.",
|
||||
analyticsTitle: "Analytics",
|
||||
analyticsDescription: "Google Analytics (_ga, _gid).",
|
||||
marketingTitle: "Marketing",
|
||||
marketingDescription: "Google Ads and Meta Pixel.",
|
||||
alwaysOn: "Always on",
|
||||
storageError: "Your preferences could not be saved. Please try again.",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
| Label key | Default |
|
||||
|-----------|---------|
|
||||
| `title` | Cookie preferences |
|
||||
| `message` | We use cookies to improve your experience… |
|
||||
| `policyLink` | Privacy policy |
|
||||
| `accept` | Accept all |
|
||||
| `reject` | Reject all |
|
||||
| `manage` | Manage preferences |
|
||||
| `save` | Save preferences |
|
||||
| `necessaryTitle` | Necessary |
|
||||
| `necessaryDescription` | Required for core site functionality… |
|
||||
| `analyticsTitle` | Analytics |
|
||||
| `analyticsDescription` | Helps us understand how visitors use the site… |
|
||||
| `marketingTitle` | Marketing |
|
||||
| `marketingDescription` | Used for ads and remarketing… |
|
||||
| `alwaysOn` | Always on |
|
||||
| `cookiesHeading` | View cookies |
|
||||
| `cookieName` | Name |
|
||||
| `cookiePurpose` | Purpose |
|
||||
| `cookieDuration` | Duration |
|
||||
| `cookieProvider` | Provider |
|
||||
| `storageError` | Your preferences could not be saved… |
|
||||
|
||||
### Theme colors
|
||||
|
||||
Pass a `theme` object to override CSS variables without writing custom CSS:
|
||||
|
||||
```jsx
|
||||
<CookieConsent
|
||||
theme={{
|
||||
background: "rgba(15, 23, 42, 0.97)",
|
||||
text: "#f8fafc",
|
||||
textMuted: "rgba(248, 250, 252, 0.82)",
|
||||
link: "#93c5fd",
|
||||
primary: "#2563eb",
|
||||
primaryText: "#ffffff",
|
||||
secondaryBackground: "rgba(255, 255, 255, 0.1)",
|
||||
border: "rgba(255, 255, 255, 0.14)",
|
||||
accent: "#2563eb",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
You can also pass `className` for additional styling hooks.
|
||||
|
||||
### Cookie inventory table
|
||||
|
||||
Provide a `cookies` array to show an expandable table inside each category in the preferences panel:
|
||||
|
||||
```jsx
|
||||
<CookieConsent
|
||||
cookies={[
|
||||
{
|
||||
category: "necessary",
|
||||
name: "my_site_consent",
|
||||
purpose: "Stores your consent choice",
|
||||
duration: "1 year",
|
||||
provider: "This site",
|
||||
},
|
||||
{
|
||||
category: "analytics",
|
||||
name: "_ga",
|
||||
purpose: "Visitor statistics",
|
||||
duration: "2 years",
|
||||
provider: "Google Analytics",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
If `cookies` is omitted, the banner keeps the simpler three-category view.
|
||||
|
||||
### Error handling
|
||||
|
||||
Consent write functions return a result object:
|
||||
|
||||
```js
|
||||
const result = acceptAll();
|
||||
if (!result.ok) {
|
||||
console.error(result.reason); // "invalid" | "storage"
|
||||
}
|
||||
```
|
||||
|
||||
In React, use callbacks on the banner:
|
||||
|
||||
```jsx
|
||||
<CookieConsent
|
||||
onConsentSaved={(consent) => console.log("saved", consent)}
|
||||
onConsentError={({ reason }) => console.warn("failed", reason)}
|
||||
/>
|
||||
```
|
||||
|
||||
Enable `debug: true` in `configureConsent()` to log warnings to the console, or use `onStorageError` for global handling.
|
||||
|
||||
---
|
||||
|
||||
## Example app
|
||||
|
||||
See [`examples/next-app`](examples/next-app) for a working Next.js demo with Turkish labels, custom theme colors, and a cookie inventory table.
|
||||
|
||||
```bash
|
||||
cd examples/next-app
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
| Banner category | User choice | Google Consent Mode signals |
|
||||
|-----------------|-------------|----------------------------|
|
||||
|
|
@ -161,6 +300,7 @@ src/
|
|||
├── preferences.js Parse / serialize user choices
|
||||
├── storage.js localStorage read/write
|
||||
├── consent-api.js acceptAll, rejectAll, getConsent, …
|
||||
├── cookie-schema.js Cookie inventory helpers
|
||||
├── google/
|
||||
│ ├── gtag.js dataLayer helpers
|
||||
│ ├── consent-signals.js Category → signal mapping
|
||||
|
|
@ -169,6 +309,8 @@ src/
|
|||
└── react/
|
||||
├── ConsentBootstrap.jsx
|
||||
├── CookieConsent.jsx
|
||||
├── CookieList.jsx
|
||||
├── theme.js
|
||||
├── GoogleTagManager.jsx
|
||||
└── FacebookPixel.jsx
|
||||
```
|
||||
|
|
@ -189,9 +331,9 @@ src/
|
|||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `getConsent()` | Read stored preferences |
|
||||
| `setConsent({ analytics, marketing })` | Save custom choice |
|
||||
| `acceptAll()` | Grant all categories |
|
||||
| `rejectAll()` | Deny optional categories |
|
||||
| `setConsent({ analytics, marketing })` | Save custom choice; returns `{ ok, consent \| reason }` |
|
||||
| `acceptAll()` | Grant all categories; returns `{ ok, consent \| reason }` |
|
||||
| `rejectAll()` | Deny optional categories; returns `{ ok, consent \| reason }` |
|
||||
| `hasAnalyticsConsent()` | Check analytics opt-in |
|
||||
| `hasMarketingConsent()` | Check marketing opt-in |
|
||||
| `openPreferences()` | Re-open banner panel |
|
||||
|
|
@ -214,7 +356,18 @@ src/
|
|||
| `EVENT_OPEN_PREFERENCES` | Preferences panel should open |
|
||||
| `GTM_CONSENT_EVENT` | Pushed to dataLayer (`cookie_consent_update`) |
|
||||
|
||||
Legacy aliases (`getCookieConsent`, `acceptAllCookies`, …) remain available.
|
||||
### `CookieConsent` props
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `labels` | `object` | Override banner text |
|
||||
| `theme` | `object` | Override banner colors |
|
||||
| `cookies` | `CookieEntry[]` | Optional cookie inventory table |
|
||||
| `policyHref` | `string` | Privacy / cookie policy URL |
|
||||
| `LinkComponent` | `component` | Custom link component (default: Next.js `Link`) |
|
||||
| `className` | `string` | Extra CSS class on the banner root |
|
||||
| `onConsentSaved` | `(consent) => void` | Called after a successful save |
|
||||
| `onConsentError` | `({ reason }) => void` | Called when save fails |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -278,7 +431,17 @@ configureConsent({
|
|||
|
||||
---
|
||||
|
||||
## Local development (link to another project)
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| Banner never appears | Consent already stored | Clear `localStorage` key or call `openPreferences()` |
|
||||
| GTM fires before consent | Bootstrap missing or loaded too late | Add `<ConsentBootstrap />` with `beforeInteractive` before GTM |
|
||||
| Consent not saved | Storage blocked (private mode, Safari ITP) | Handle `onConsentError`; show inline error from banner |
|
||||
| Tags still fire after reject | GTM tags not using Consent Mode | Enable Consent Mode in GTM and set tag consent requirements |
|
||||
| TypeScript errors on `./react` | Missing types path | Import from `@pdntechnology/cookie-consent/react` (types included since v1.1) |
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
# In this repo
|
||||
|
|
@ -312,11 +475,7 @@ npm publish --access public
|
|||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. Please open an issue before large changes.
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Submit a pull request
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports and pull requests are welcome.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
344
README.tr.md
Normal file
344
README.tr.md
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
# @pdntechnology/cookie-consent
|
||||
|
||||
[](LICENSE)
|
||||
[](https://www.npmjs.com/package/@pdntechnology/cookie-consent)
|
||||
|
||||
React ve Next.js için hafif, açık kaynak bir **Çerez Yönetim Platformu (CMP)**. [Google Consent Mode v2](https://developers.google.com/tag-platform/security/guides/consent) akışını sertifikalı araçlarla aynı şekilde uygular.
|
||||
|
||||
Ağır bağımlılık yok. Vendor lock-in yok. Google Tag Manager, GA4, Google Ads ve Meta Pixel ile uyumludur.
|
||||
|
||||
> English documentation: [README.md](README.md)
|
||||
|
||||
---
|
||||
|
||||
## Neden var?
|
||||
|
||||
Google sertifikalı CMP'ler (Cookiebot, OneTrust, CookieYes, …) aynı teknik akışı izler:
|
||||
|
||||
1. Google etiketlerinden **önce** varsayılanları **reddedilmiş** olarak ayarla
|
||||
2. GTM / gtag.js yükle
|
||||
3. Çerez banner'ını göster
|
||||
4. Kullanıcı seçimi → `gtag('consent', 'update', …)` + dataLayer olayı
|
||||
5. Reklamlar reddedildiğinde → `ads_data_redaction` ve `url_passthrough` etkinleştir
|
||||
|
||||
Bu kütüphane bu akışı okunabilir, yaklaşık 15 KB JavaScript ile uygular.
|
||||
|
||||
> **Not:** Sertifikalı CMP'ler ayrıca IAB TCF v2.2 sinyalleri sağlar ve Google'ın sertifikalı satıcı listesinde yer alır. Bu kütüphane **Consent Mode v2 teknik entegrasyonunu** kapsar. Gizlilik politikası metni ve yasal uyumluluk sizin sorumluluğunuzdadır.
|
||||
|
||||
---
|
||||
|
||||
## Özellikler
|
||||
|
||||
- Google Consent Mode v2 (7 sinyalin tamamı)
|
||||
- Granüler kategoriler: Zorunlu / Analitik / Pazarlama
|
||||
- Özelleştirilebilir banner metni, tema renkleri ve çerez envanteri
|
||||
- Bootstrap ve güncellemede `ads_data_redaction` + `url_passthrough`
|
||||
- Bölgesel varsayılan onay (EEA vb.)
|
||||
- React banner + Next.js bootstrap bileşeni
|
||||
- İsteğe bağlı GTM ve Meta Pixel yükleyicileri (onay kapılı)
|
||||
- Framework'ten bağımsız çekirdek API (vanilla JS desteklenir)
|
||||
- Çekirdek API ve React bileşenleri için TypeScript tanımları
|
||||
- Onay kaydedilemediğinde hata callback'leri
|
||||
|
||||
---
|
||||
|
||||
## Kurulum
|
||||
|
||||
```bash
|
||||
npm install @pdntechnology/cookie-consent
|
||||
```
|
||||
|
||||
### Next.js ayarı
|
||||
|
||||
`next.config.mjs` dosyasına ekleyin:
|
||||
|
||||
```js
|
||||
const nextConfig = {
|
||||
transpilePackages: ["@pdntechnology/cookie-consent"],
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hızlı başlangıç
|
||||
|
||||
### 1. Yapılandırma (isteğe bağlı)
|
||||
|
||||
```js
|
||||
// lib/consent-config.js
|
||||
import { configureConsent } from "@pdntechnology/cookie-consent";
|
||||
|
||||
configureConsent({
|
||||
storageKey: "site_cerez_tercihi",
|
||||
waitForUpdateMs: 500,
|
||||
adsDataRedaction: true,
|
||||
urlPassthrough: true,
|
||||
debug: false,
|
||||
});
|
||||
```
|
||||
|
||||
Bu dosyayı root layout'ta `ConsentBootstrap`'ten **önce** import edin.
|
||||
|
||||
### 2. Consent varsayılanlarını bootstrap et (GTM'den önce)
|
||||
|
||||
```jsx
|
||||
// app/layout.jsx
|
||||
import "@pdntechnology/cookie-consent/styles.css";
|
||||
import "@/lib/consent-config";
|
||||
import { ConsentBootstrap } from "@pdntechnology/cookie-consent/react";
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="tr">
|
||||
<body>
|
||||
<ConsentBootstrap />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Banner + etiketler
|
||||
|
||||
```jsx
|
||||
// app/providers.jsx
|
||||
import Link from "next/link";
|
||||
import {
|
||||
CookieConsent,
|
||||
GoogleTagManager,
|
||||
} from "@pdntechnology/cookie-consent/react";
|
||||
|
||||
export function Providers({ children }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<CookieConsent
|
||||
policyHref="/cerez-politikasi"
|
||||
LinkComponent={Link}
|
||||
labels={{
|
||||
title: "Çerez tercihleri",
|
||||
message: "Deneyiminizi iyileştirmek için çerez kullanıyoruz.",
|
||||
policyLink: "Çerez politikası",
|
||||
accept: "Tümünü kabul et",
|
||||
reject: "Tümünü reddet",
|
||||
manage: "Tercihleri yönet",
|
||||
}}
|
||||
theme={{
|
||||
primary: "#2563eb",
|
||||
background: "rgba(15, 23, 42, 0.97)",
|
||||
}}
|
||||
/>
|
||||
<GoogleTagManager gtmId="GTM-XXXXXXX" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Tercihleri yeniden aç (footer linki)
|
||||
|
||||
```jsx
|
||||
import { openPreferences } from "@pdntechnology/cookie-consent";
|
||||
|
||||
<button type="button" onClick={openPreferences}>
|
||||
Çerez ayarları
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Özelleştirme
|
||||
|
||||
### Metin ve politika URL'si
|
||||
|
||||
Görünen tüm metinler `labels` prop'u ile değiştirilebilir. Politika linki `policyHref` ile ayarlanır.
|
||||
|
||||
| Label anahtarı | Varsayılan (EN) |
|
||||
|----------------|-----------------|
|
||||
| `title` | Cookie preferences |
|
||||
| `message` | We use cookies to improve your experience… |
|
||||
| `policyLink` | Privacy policy |
|
||||
| `accept` | Accept all |
|
||||
| `reject` | Reject all |
|
||||
| `manage` | Manage preferences |
|
||||
| `save` | Save preferences |
|
||||
| `necessaryTitle` | Necessary |
|
||||
| `necessaryDescription` | Required for core site functionality… |
|
||||
| `analyticsTitle` | Analytics |
|
||||
| `analyticsDescription` | Helps us understand how visitors use the site… |
|
||||
| `marketingTitle` | Marketing |
|
||||
| `marketingDescription` | Used for ads and remarketing… |
|
||||
| `alwaysOn` | Always on |
|
||||
| `cookiesHeading` | View cookies |
|
||||
| `cookieName` | Name |
|
||||
| `cookiePurpose` | Purpose |
|
||||
| `cookieDuration` | Duration |
|
||||
| `cookieProvider` | Provider |
|
||||
| `storageError` | Your preferences could not be saved… |
|
||||
|
||||
### Tema renkleri
|
||||
|
||||
`theme` prop'u ile CSS değişkenlerini override edin:
|
||||
|
||||
```jsx
|
||||
<CookieConsent
|
||||
theme={{
|
||||
background: "rgba(15, 23, 42, 0.97)",
|
||||
text: "#f8fafc",
|
||||
link: "#93c5fd",
|
||||
primary: "#2563eb",
|
||||
accent: "#2563eb",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
Desteklenen anahtarlar: `background`, `text`, `textMuted`, `link`, `primary`, `primaryText`, `secondaryBackground`, `border`, `accent`.
|
||||
|
||||
### Çerez envanteri tablosu
|
||||
|
||||
Hangi çerezlerin kullanıldığını tercihler panelinde göstermek için `cookies` dizisi verin:
|
||||
|
||||
```jsx
|
||||
<CookieConsent
|
||||
cookies={[
|
||||
{
|
||||
category: "necessary",
|
||||
name: "site_cerez_tercihi",
|
||||
purpose: "Çerez tercihlerinizi saklar",
|
||||
duration: "1 yıl",
|
||||
provider: "Bu site",
|
||||
},
|
||||
{
|
||||
category: "analytics",
|
||||
name: "_ga",
|
||||
purpose: "Ziyaretçi istatistikleri",
|
||||
duration: "2 yıl",
|
||||
provider: "Google Analytics",
|
||||
},
|
||||
{
|
||||
category: "marketing",
|
||||
name: "_fbp",
|
||||
purpose: "Reklam hedefleme",
|
||||
duration: "3 ay",
|
||||
provider: "Meta",
|
||||
},
|
||||
]}
|
||||
labels={{
|
||||
cookiesHeading: "Kullanılan çerezleri gör",
|
||||
cookieName: "Ad",
|
||||
cookiePurpose: "Amaç",
|
||||
cookieDuration: "Süre",
|
||||
cookieProvider: "Sağlayıcı",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
`cookies` verilmezse banner mevcut üç kategori görünümünü korur.
|
||||
|
||||
### Hata yönetimi
|
||||
|
||||
```js
|
||||
const result = acceptAll();
|
||||
if (!result.ok) {
|
||||
console.error(result.reason); // "invalid" | "storage"
|
||||
}
|
||||
```
|
||||
|
||||
React'te callback kullanın:
|
||||
|
||||
```jsx
|
||||
<CookieConsent
|
||||
onConsentSaved={(consent) => console.log("kaydedildi", consent)}
|
||||
onConsentError={({ reason }) => console.warn("hata", reason)}
|
||||
/>
|
||||
```
|
||||
|
||||
`configureConsent({ debug: true })` ile konsola uyarı yazılır. Global işlem için `onStorageError` kullanılabilir.
|
||||
|
||||
---
|
||||
|
||||
## Örnek uygulama
|
||||
|
||||
[`examples/next-app`](examples/next-app) dizininde Türkçe metinler, özel tema ve çerez envanteri içeren çalışan bir Next.js demosu vardır.
|
||||
|
||||
```bash
|
||||
cd examples/next-app
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kategoriler → Google sinyalleri
|
||||
|
||||
| Banner kategorisi | Kullanıcı seçimi | Google Consent Mode sinyalleri |
|
||||
|-------------------|------------------|--------------------------------|
|
||||
| **Zorunlu** | Her zaman açık | `security_storage: granted` |
|
||||
| **Analitik** | Opt-in | `analytics_storage` |
|
||||
| **Pazarlama** | Opt-in | `ad_storage`, `ad_user_data`, `ad_personalization`, `functionality_storage`, `personalization_storage` |
|
||||
|
||||
Pazarlama **reddedildiğinde** kütüphane ayrıca şunları ayarlar:
|
||||
|
||||
- `ads_data_redaction: true`
|
||||
- `url_passthrough: true`
|
||||
|
||||
---
|
||||
|
||||
## API referansı
|
||||
|
||||
### Yapılandırma
|
||||
|
||||
| Fonksiyon | Açıklama |
|
||||
|-----------|----------|
|
||||
| `configureConsent(config)` | Depolama anahtarı, bekleme süresi, bölgesel varsayılanlar |
|
||||
| `getConsentConfig()` | Aktif yapılandırmayı oku |
|
||||
|
||||
### Kullanıcı onayı
|
||||
|
||||
| Fonksiyon | Açıklama |
|
||||
|-----------|----------|
|
||||
| `getConsent()` | Kayıtlı tercihleri oku |
|
||||
| `setConsent({ analytics, marketing })` | Özel seçim kaydet; `{ ok, consent \| reason }` döner |
|
||||
| `acceptAll()` | Tüm kategorilere izin ver |
|
||||
| `rejectAll()` | Opsiyonel kategorileri reddet |
|
||||
| `hasAnalyticsConsent()` | Analitik onayını kontrol et |
|
||||
| `hasMarketingConsent()` | Pazarlama onayını kontrol et |
|
||||
| `openPreferences()` | Tercih panelini yeniden aç |
|
||||
| `syncConsentFromStorage()` | GTM yüklendikten sonra kayıtlı onayı uygula |
|
||||
|
||||
### `CookieConsent` prop'ları
|
||||
|
||||
| Prop | Tip | Açıklama |
|
||||
|------|-----|----------|
|
||||
| `labels` | `object` | Banner metinlerini override et |
|
||||
| `theme` | `object` | Banner renklerini override et |
|
||||
| `cookies` | `CookieEntry[]` | Opsiyonel çerez envanteri tablosu |
|
||||
| `policyHref` | `string` | Gizlilik / çerez politikası URL'si |
|
||||
| `LinkComponent` | `component` | Özel link bileşeni (varsayılan: Next.js `Link`) |
|
||||
| `className` | `string` | Banner kök elemanına ek CSS sınıfı |
|
||||
| `onConsentSaved` | `(consent) => void` | Başarılı kayıttan sonra çağrılır |
|
||||
| `onConsentError` | `({ reason }) => void` | Kayıt başarısız olursa çağrılır |
|
||||
|
||||
---
|
||||
|
||||
## Sorun giderme
|
||||
|
||||
| Sorun | Olası neden | Çözüm |
|
||||
|-------|-------------|-------|
|
||||
| Banner hiç görünmüyor | Onay zaten kayıtlı | `localStorage` anahtarını temizleyin veya `openPreferences()` çağırın |
|
||||
| GTM onaydan önce tetikleniyor | Bootstrap eksik veya geç yükleniyor | GTM'den önce `<ConsentBootstrap />` ekleyin |
|
||||
| Onay kaydedilmiyor | Depolama engelli (gizli mod, Safari ITP) | `onConsentError` ile yakalayın; banner inline hata gösterir |
|
||||
| Reddetmeden sonra etiketler çalışıyor | GTM etiketleri Consent Mode kullanmıyor | GTM'de Consent Mode'u etkinleştirin |
|
||||
| `./react` TypeScript hatası | Eski sürüm | v1.1+ ile `@pdntechnology/cookie-consent/react` tipleri dahildir |
|
||||
|
||||
---
|
||||
|
||||
## Katkıda bulunma
|
||||
|
||||
Bkz. [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
---
|
||||
|
||||
## Lisans
|
||||
|
||||
[MIT](LICENSE) © [PDN Technology](https://pdntechnology.com)
|
||||
15
examples/next-app/README.md
Normal file
15
examples/next-app/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Next.js Demo
|
||||
|
||||
Minimal demo for `@pdntechnology/cookie-consent`.
|
||||
|
||||
## Run locally
|
||||
|
||||
```bash
|
||||
cd examples/next-app
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
The demo uses a local file dependency on the parent package, Turkish labels, a custom theme, and a cookie inventory table.
|
||||
20
examples/next-app/app/layout.jsx
Normal file
20
examples/next-app/app/layout.jsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import "@pdntechnology/cookie-consent/styles.css";
|
||||
import "@/lib/consent-config";
|
||||
import { ConsentBootstrap } from "@pdntechnology/cookie-consent/react";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
export const metadata = {
|
||||
title: "Cookie Consent Demo",
|
||||
description: "Next.js demo for @pdntechnology/cookie-consent",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="tr">
|
||||
<body>
|
||||
<ConsentBootstrap />
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
19
examples/next-app/app/page.jsx
Normal file
19
examples/next-app/app/page.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { CookieSettingsButton } from "./providers";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main style={{ maxWidth: 720, margin: "48px auto", padding: "0 24px", fontFamily: "system-ui, sans-serif" }}>
|
||||
<h1>Cookie Consent Demo</h1>
|
||||
<p>
|
||||
Bu örnek uygulama, <code>@pdntechnology/cookie-consent</code> paketinin Next.js
|
||||
ile nasıl kullanılacağını gösterir. Türkçe metinler, özel tema renkleri ve çerez
|
||||
envanteri tablosu etkinleştirilmiştir.
|
||||
</p>
|
||||
<p>
|
||||
Sayfayı ilk kez açtığınızda banner görünür. Tercih kaydedildikten sonra aşağıdaki
|
||||
buton ile paneli yeniden açabilirsiniz.
|
||||
</p>
|
||||
<CookieSettingsButton />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
93
examples/next-app/app/providers.jsx
Normal file
93
examples/next-app/app/providers.jsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
CookieConsent,
|
||||
openPreferences,
|
||||
} from "@pdntechnology/cookie-consent/react";
|
||||
|
||||
const labels = {
|
||||
title: "Çerez tercihleri",
|
||||
message: "Deneyiminizi iyileştirmek ve site trafiğini analiz etmek için çerez kullanıyoruz.",
|
||||
policyLink: "Çerez politikası",
|
||||
accept: "Tümünü kabul et",
|
||||
reject: "Tümünü reddet",
|
||||
manage: "Tercihleri yönet",
|
||||
save: "Tercihleri kaydet",
|
||||
necessaryTitle: "Zorunlu",
|
||||
necessaryDescription: "Sitenin temel işlevleri için gereklidir. Her zaman aktiftir.",
|
||||
analyticsTitle: "Analitik",
|
||||
analyticsDescription: "Google Analytics (_ga, _gid) ile ziyaretçi trafiğini ölçer.",
|
||||
marketingTitle: "Pazarlama",
|
||||
marketingDescription: "Google Ads ve Meta Pixel ile reklam performansını ölçer.",
|
||||
alwaysOn: "Her zaman açık",
|
||||
cookiesHeading: "Kullanılan çerezleri gör",
|
||||
cookieName: "Ad",
|
||||
cookiePurpose: "Amaç",
|
||||
cookieDuration: "Süre",
|
||||
cookieProvider: "Sağlayıcı",
|
||||
storageError: "Tercihleriniz kaydedilemedi. Lütfen tekrar deneyin.",
|
||||
};
|
||||
|
||||
const cookies = [
|
||||
{
|
||||
category: "necessary",
|
||||
name: "demo_cookie_consent",
|
||||
purpose: "Çerez tercihlerinizi saklar",
|
||||
duration: "1 yıl",
|
||||
provider: "Bu site",
|
||||
},
|
||||
{
|
||||
category: "analytics",
|
||||
name: "_ga",
|
||||
purpose: "Ziyaretçi istatistikleri",
|
||||
duration: "2 yıl",
|
||||
provider: "Google Analytics",
|
||||
},
|
||||
{
|
||||
category: "analytics",
|
||||
name: "_gid",
|
||||
purpose: "Oturum ayırma",
|
||||
duration: "24 saat",
|
||||
provider: "Google Analytics",
|
||||
},
|
||||
{
|
||||
category: "marketing",
|
||||
name: "_fbp",
|
||||
purpose: "Reklam hedefleme",
|
||||
duration: "3 ay",
|
||||
provider: "Meta",
|
||||
},
|
||||
];
|
||||
|
||||
export function Providers({ children }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<CookieConsent
|
||||
policyHref="/cerez-politikasi"
|
||||
LinkComponent={Link}
|
||||
labels={labels}
|
||||
cookies={cookies}
|
||||
theme={{
|
||||
primary: "#2563eb",
|
||||
background: "rgba(15, 23, 42, 0.97)",
|
||||
text: "#f8fafc",
|
||||
link: "#93c5fd",
|
||||
accent: "#2563eb",
|
||||
}}
|
||||
onConsentError={({ reason }) => {
|
||||
console.warn("[demo] consent error:", reason);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function CookieSettingsButton() {
|
||||
return (
|
||||
<button type="button" onClick={openPreferences}>
|
||||
Çerez ayarları
|
||||
</button>
|
||||
);
|
||||
}
|
||||
7
examples/next-app/jsconfig.json
Normal file
7
examples/next-app/jsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
examples/next-app/lib/consent-config.js
Normal file
9
examples/next-app/lib/consent-config.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { configureConsent } from "@pdntechnology/cookie-consent";
|
||||
|
||||
configureConsent({
|
||||
storageKey: "demo_cookie_consent",
|
||||
waitForUpdateMs: 500,
|
||||
adsDataRedaction: true,
|
||||
urlPassthrough: true,
|
||||
debug: false,
|
||||
});
|
||||
7
examples/next-app/next.config.mjs
Normal file
7
examples/next-app/next.config.mjs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ["@pdntechnology/cookie-consent"],
|
||||
outputFileTracingRoot: import.meta.dirname,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
928
examples/next-app/package-lock.json
generated
Normal file
928
examples/next-app/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,928 @@
|
|||
{
|
||||
"name": "cookie-consent-next-example",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cookie-consent-next-example",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@pdntechnology/cookie-consent": "file:../..",
|
||||
"next": "^15.3.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"../..": {
|
||||
"name": "@pdntechnology/cookie-consent",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": ">=14.0.0",
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"next": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.19.tgz",
|
||||
"integrity": "sha512-sWWluFvcv5v3Fxznmf2ZfjyoVQt/64oCnYqS90inQWGzMPK1VjvekPiz3OPHKmFT30EnHrjlbyaHLt3M0vWabw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.19.tgz",
|
||||
"integrity": "sha512-jx9wWlTKueHKPvVOndyr7WuaevWCkuYqsQ8gC0TMPKAVWG3MhcdMrjfo9tvIZNXd0QOUYXXvAcZ325y8Uq7uzg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.19.tgz",
|
||||
"integrity": "sha512-291KFcsIQ3OenRdiUDFOR6W3wezzH4auENXm1gbm1Bjd4ANMMRgxPrWTUztQN43BnVoVuMnHCrLeECIMwgFKbA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.19.tgz",
|
||||
"integrity": "sha512-WeH+nelQyyMeE2f8FxBRZNrGipya5zHZV2vjzfCOAYyiI6am+NbnWAAldOBFQBB2w0DjJcsvrKqoFT2b7+5YoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.19.tgz",
|
||||
"integrity": "sha512-5xTOE0lDlDCSSfp+BAif7j17VRRCjWp//ZPZy6NI0QpdrhxtQnsZguSx0xAAZ0c9XZLrLLwCe/XVe5YPrRilKw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.19.tgz",
|
||||
"integrity": "sha512-LTxRmMgqqMv05Had879W00Fm53quiJd3Zuz8h1JSNJ3nGSlbZ/7Tjs1tKyScgN3Au3t3MyPsjPlq60fMmSHLsg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.19.tgz",
|
||||
"integrity": "sha512-eoNQSpA5PQfB9wBO4RA47MTDXWz1fizy9Y3Z6e4DetYIF3dvjuu8sj7aIGn/bFCU6lnFzTK34NtCaffP4NsQ7Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.19.tgz",
|
||||
"integrity": "sha512-6UNt2dFuCHOe446sm/Kp69nUe8/wIhnh9bm6Xcqw4qEWCOppLMOvhTBVgvM7invVUNr4SPpP6NOQsACtn2IN9Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.19.tgz",
|
||||
"integrity": "sha512-PhmojAHyqMne56HBLGu9dhDnHPuFmEjrXSQMM/nW0J6j849lk3ESrVtqNJcCk8CKOV7brpTTbaYAjwKPzKM69w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdntechnology/cookie-consent": {
|
||||
"resolved": "../..",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001799",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
|
||||
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.13",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.13.tgz",
|
||||
"integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.5.19",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.19.tgz",
|
||||
"integrity": "sha512-xNOW6tYshGX1/Oi3F8uuk4gpDeWsSUE/1Z0G5uUMekIxaQ0xc03UXd9II0VQHYMWviMeA0OHpJFAKsHf8bTYVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.5.19",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
"styled-jsx": "5.1.6"
|
||||
},
|
||||
"bin": {
|
||||
"next": "dist/bin/next"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.5.19",
|
||||
"@next/swc-darwin-x64": "15.5.19",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.19",
|
||||
"@next/swc-linux-arm64-musl": "15.5.19",
|
||||
"@next/swc-linux-x64-gnu": "15.5.19",
|
||||
"@next/swc-linux-x64-musl": "15.5.19",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.19",
|
||||
"@next/swc-win32-x64-msvc": "15.5.19",
|
||||
"sharp": "^0.34.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"babel-plugin-react-compiler": "*",
|
||||
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
|
||||
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
|
||||
"sass": "^1.3.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@playwright/test": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-react-compiler": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.7"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"client-only": "0.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/core": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
examples/next-app/package.json
Normal file
16
examples/next-app/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "cookie-consent-next-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pdntechnology/cookie-consent": "file:../..",
|
||||
"next": "^15.3.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
}
|
||||
}
|
||||
1602
package-lock.json
generated
Normal file
1602
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@pdntechnology/cookie-consent",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Lightweight Google Consent Mode v2 CMP for React and Next.js",
|
||||
"type": "module",
|
||||
"main": "./src/index.js",
|
||||
|
|
@ -9,18 +9,22 @@
|
|||
"sideEffects": ["./src/styles.css"],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.d.ts",
|
||||
"import": "./src/index.js",
|
||||
"default": "./src/index.js"
|
||||
},
|
||||
"./react": {
|
||||
"types": "./src/react/index.d.ts",
|
||||
"import": "./src/react/index.js",
|
||||
"default": "./src/react/index.js"
|
||||
},
|
||||
"./styles.css": "./src/styles.css"
|
||||
},
|
||||
"files": ["src", "README.md", "LICENSE"],
|
||||
"files": ["src", "README.md", "README.tr.md", "LICENSE", "CHANGELOG.md", "CONTRIBUTING.md"],
|
||||
"scripts": {
|
||||
"prepublishOnly": "test -f README.md && test -f LICENSE"
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"prepublishOnly": "npm test && test -f README.md && test -f LICENSE"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": ">=14.0.0",
|
||||
|
|
@ -32,6 +36,9 @@
|
|||
"react": { "optional": true },
|
||||
"react-dom": { "optional": true }
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"keywords": [
|
||||
"cookie-consent",
|
||||
"consent-mode",
|
||||
|
|
@ -47,12 +54,12 @@
|
|||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pdntechnology/cookie-consent.git"
|
||||
"url": "https://git.pdntechnology.com.tr/pdn/cookie-consent.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/pdntechnology/cookie-consent/issues"
|
||||
"url": "https://git.pdntechnology.com.tr/pdn/cookie-consent/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pdntechnology/cookie-consent#readme",
|
||||
"homepage": "https://git.pdntechnology.com.tr/pdn/cookie-consent#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { getConsentConfig } from "./config.js";
|
||||
import { EVENT_CONSENT_UPDATED, EVENT_OPEN_PREFERENCES } from "./constants.js";
|
||||
import { applyConsentUpdate } from "./google/consent-lifecycle.js";
|
||||
import { parseUserConsent } from "./preferences.js";
|
||||
|
|
@ -23,26 +24,63 @@ export function hasMarketingConsent() {
|
|||
|
||||
// ─── Write ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {unknown} reason
|
||||
* @param {unknown} [detail]
|
||||
*/
|
||||
function reportConsentError(reason, detail) {
|
||||
const config = getConsentConfig();
|
||||
|
||||
if (config.debug) {
|
||||
console.warn("[cookie-consent]", reason, detail ?? "");
|
||||
}
|
||||
|
||||
if (typeof config.onStorageError === "function") {
|
||||
config.onStorageError({ reason, detail });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("./types.js").UserConsent | Partial<import("./types.js").UserConsent> | "accepted" | "rejected"} preferences
|
||||
* @returns {import("./types.js").PersistResult}
|
||||
*/
|
||||
function persistConsent(preferences) {
|
||||
const consent = parseUserConsent(preferences);
|
||||
if (!consent) return;
|
||||
if (!consent) {
|
||||
reportConsentError("invalid", preferences);
|
||||
return { ok: false, reason: "invalid" };
|
||||
}
|
||||
|
||||
if (!writeStoredConsent(consent)) return;
|
||||
if (!writeStoredConsent(consent)) {
|
||||
reportConsentError("storage", consent);
|
||||
return { ok: false, reason: "storage" };
|
||||
}
|
||||
|
||||
applyConsentUpdate(consent);
|
||||
window.dispatchEvent(new CustomEvent(EVENT_CONSENT_UPDATED, { detail: consent }));
|
||||
return { ok: true, consent };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("./types.js").UserConsent | Partial<import("./types.js").UserConsent>} preferences
|
||||
* @returns {import("./types.js").PersistResult}
|
||||
*/
|
||||
export function setConsent(preferences) {
|
||||
persistConsent(preferences);
|
||||
return persistConsent(preferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import("./types.js").PersistResult}
|
||||
*/
|
||||
export function acceptAll() {
|
||||
persistConsent({ analytics: true, marketing: true });
|
||||
return persistConsent({ analytics: true, marketing: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import("./types.js").PersistResult}
|
||||
*/
|
||||
export function rejectAll() {
|
||||
persistConsent({ analytics: false, marketing: false });
|
||||
return persistConsent({ analytics: false, marketing: false });
|
||||
}
|
||||
|
||||
// ─── Sync & UI ──────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
61
src/consent-api.test.js
Normal file
61
src/consent-api.test.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { configureConsent } from "./config.js";
|
||||
import { EVENT_CONSENT_UPDATED } from "./constants.js";
|
||||
import { acceptAll, rejectAll, setConsent } from "./consent-api.js";
|
||||
|
||||
const storage = new Map();
|
||||
const dataLayer = [];
|
||||
|
||||
describe("consent-api", () => {
|
||||
beforeEach(() => {
|
||||
storage.clear();
|
||||
dataLayer.length = 0;
|
||||
configureConsent({ storageKey: "test_consent", debug: false });
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
localStorage: {
|
||||
getItem: (key) => storage.get(key) ?? null,
|
||||
setItem: (key, value) => {
|
||||
storage.set(key, value);
|
||||
},
|
||||
},
|
||||
dataLayer,
|
||||
dispatchEvent: vi.fn((event) => {
|
||||
window.__lastEvent = event;
|
||||
}),
|
||||
});
|
||||
|
||||
vi.stubGlobal("dataLayer", dataLayer);
|
||||
window.dataLayer = dataLayer;
|
||||
window.gtag = (...args) => {
|
||||
dataLayer.push(args);
|
||||
};
|
||||
});
|
||||
|
||||
it("acceptAll persists consent and dispatches event", () => {
|
||||
const result = acceptAll();
|
||||
|
||||
expect(result).toEqual({ ok: true, consent: { analytics: true, marketing: true } });
|
||||
expect(window.__lastEvent.type).toBe(EVENT_CONSENT_UPDATED);
|
||||
expect(dataLayer.some((entry) => entry[0] === "consent" && entry[1] === "update")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejectAll denies optional categories", () => {
|
||||
const result = rejectAll();
|
||||
expect(result).toEqual({ ok: true, consent: { analytics: false, marketing: false } });
|
||||
});
|
||||
|
||||
it("returns invalid when preferences cannot be parsed", () => {
|
||||
const result = setConsent(null);
|
||||
expect(result).toEqual({ ok: false, reason: "invalid" });
|
||||
});
|
||||
|
||||
it("returns storage error when localStorage write fails", () => {
|
||||
window.localStorage.setItem = () => {
|
||||
throw new Error("blocked");
|
||||
};
|
||||
|
||||
const result = setConsent({ analytics: true, marketing: true });
|
||||
expect(result).toEqual({ ok: false, reason: "storage" });
|
||||
});
|
||||
});
|
||||
57
src/cookie-schema.js
Normal file
57
src/cookie-schema.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/** @typedef {"necessary"|"analytics"|"marketing"} CookieCategory */
|
||||
|
||||
/**
|
||||
* @typedef {Object} CookieEntry
|
||||
* @property {CookieCategory} category
|
||||
* @property {string} name
|
||||
* @property {string} purpose
|
||||
* @property {string} duration
|
||||
* @property {string} provider
|
||||
*/
|
||||
|
||||
export const COOKIE_CATEGORIES = ["necessary", "analytics", "marketing"];
|
||||
|
||||
/**
|
||||
* @param {unknown} entry
|
||||
* @returns {entry is CookieEntry}
|
||||
*/
|
||||
export function isValidCookieEntry(entry) {
|
||||
if (!entry || typeof entry !== "object") return false;
|
||||
const { category, name, purpose, duration, provider } = entry;
|
||||
return (
|
||||
COOKIE_CATEGORIES.includes(category) &&
|
||||
typeof name === "string" &&
|
||||
name.length > 0 &&
|
||||
typeof purpose === "string" &&
|
||||
typeof duration === "string" &&
|
||||
typeof provider === "string"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown[]} cookies
|
||||
* @returns {CookieEntry[]}
|
||||
*/
|
||||
export function normalizeCookieEntries(cookies) {
|
||||
if (!Array.isArray(cookies)) return [];
|
||||
return cookies.filter(isValidCookieEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CookieEntry[]} cookies
|
||||
* @returns {Record<CookieCategory, CookieEntry[]>}
|
||||
*/
|
||||
export function groupCookiesByCategory(cookies) {
|
||||
/** @type {Record<CookieCategory, CookieEntry[]>} */
|
||||
const grouped = {
|
||||
necessary: [],
|
||||
analytics: [],
|
||||
marketing: [],
|
||||
};
|
||||
|
||||
for (const entry of cookies) {
|
||||
grouped[entry.category].push(entry);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
61
src/cookie-schema.test.js
Normal file
61
src/cookie-schema.test.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
groupCookiesByCategory,
|
||||
isValidCookieEntry,
|
||||
normalizeCookieEntries,
|
||||
} from "./cookie-schema.js";
|
||||
|
||||
describe("cookie-schema", () => {
|
||||
it("validates cookie entries", () => {
|
||||
expect(
|
||||
isValidCookieEntry({
|
||||
category: "analytics",
|
||||
name: "_ga",
|
||||
purpose: "Analytics",
|
||||
duration: "2 years",
|
||||
provider: "Google",
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(isValidCookieEntry({ category: "unknown", name: "_ga" })).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes invalid entries out of the list", () => {
|
||||
const cookies = normalizeCookieEntries([
|
||||
{
|
||||
category: "marketing",
|
||||
name: "_fbp",
|
||||
purpose: "Ads",
|
||||
duration: "3 months",
|
||||
provider: "Meta",
|
||||
},
|
||||
{ category: "invalid", name: "bad" },
|
||||
]);
|
||||
|
||||
expect(cookies).toHaveLength(1);
|
||||
expect(cookies[0].name).toBe("_fbp");
|
||||
});
|
||||
|
||||
it("groups cookies by category", () => {
|
||||
const grouped = groupCookiesByCategory([
|
||||
{
|
||||
category: "analytics",
|
||||
name: "_ga",
|
||||
purpose: "Analytics",
|
||||
duration: "2 years",
|
||||
provider: "Google",
|
||||
},
|
||||
{
|
||||
category: "necessary",
|
||||
name: "ccm_consent",
|
||||
purpose: "Preferences",
|
||||
duration: "1 year",
|
||||
provider: "Site",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(grouped.analytics).toHaveLength(1);
|
||||
expect(grouped.necessary).toHaveLength(1);
|
||||
expect(grouped.marketing).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
20
src/google/bootstrap-script.js
vendored
20
src/google/bootstrap-script.js
vendored
|
|
@ -1,24 +1,38 @@
|
|||
import { getConsentConfig } from "../config.js";
|
||||
import { getDefaultDeniedState } from "./consent-signals.js";
|
||||
|
||||
function buildBootstrapDefaults(config) {
|
||||
const wait = Number(config.waitForUpdateMs) || 500;
|
||||
const defaults = getDefaultDeniedState(wait);
|
||||
|
||||
if (config.adsDataRedaction !== false) {
|
||||
defaults.ads_data_redaction = true;
|
||||
}
|
||||
if (config.urlPassthrough !== false) {
|
||||
defaults.url_passthrough = true;
|
||||
}
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline script for `<head>` — must execute before GTM snippet.
|
||||
* Used by ConsentBootstrap and vanilla HTML integrations.
|
||||
*/
|
||||
export function getConsentBootstrapScript(overrides = {}) {
|
||||
const config = { ...getConsentConfig(), ...overrides };
|
||||
const wait = Number(config.waitForUpdateMs) || 500;
|
||||
const bootstrapDefaults = buildBootstrapDefaults(config);
|
||||
|
||||
const regionLines = (config.regionDefaults || [])
|
||||
.filter((block) => Array.isArray(block.regions) && block.regions.length > 0)
|
||||
.map((block) => {
|
||||
const { regions, ...signals } = block;
|
||||
const payload = JSON.stringify({ ...getDefaultDeniedState(wait), ...signals, region: regions });
|
||||
const payload = JSON.stringify({ ...bootstrapDefaults, ...signals, region: regions });
|
||||
return `gtag('consent','default',${payload});`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const globalDefault = JSON.stringify(getDefaultDeniedState(wait));
|
||||
const globalDefault = JSON.stringify(bootstrapDefaults);
|
||||
|
||||
return `
|
||||
window.dataLayer=window.dataLayer||[];
|
||||
|
|
|
|||
23
src/google/bootstrap-script.test.js
vendored
Normal file
23
src/google/bootstrap-script.test.js
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { configureConsent } from "../config.js";
|
||||
import { getConsentBootstrapScript } from "./bootstrap-script.js";
|
||||
|
||||
describe("getConsentBootstrapScript", () => {
|
||||
it("includes denied defaults and advanced settings", () => {
|
||||
configureConsent({
|
||||
waitForUpdateMs: 500,
|
||||
adsDataRedaction: true,
|
||||
urlPassthrough: true,
|
||||
regionDefaults: [],
|
||||
});
|
||||
|
||||
const script = getConsentBootstrapScript();
|
||||
|
||||
expect(script).toContain("gtag('consent','default'");
|
||||
expect(script).toContain('"analytics_storage":"denied"');
|
||||
expect(script).toContain('"security_storage":"granted"');
|
||||
expect(script).toContain('"ads_data_redaction":true');
|
||||
expect(script).toContain('"url_passthrough":true');
|
||||
expect(script).toContain('"wait_for_update":500');
|
||||
});
|
||||
});
|
||||
64
src/google/consent-signals.test.js
Normal file
64
src/google/consent-signals.test.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { buildConsentSignals, buildAdvancedSettings, getDefaultDeniedState } from "./consent-signals.js";
|
||||
|
||||
describe("buildConsentSignals", () => {
|
||||
it("denies optional categories by default", () => {
|
||||
expect(buildConsentSignals(null)).toEqual({
|
||||
analytics_storage: "denied",
|
||||
ad_storage: "denied",
|
||||
ad_user_data: "denied",
|
||||
ad_personalization: "denied",
|
||||
functionality_storage: "denied",
|
||||
personalization_storage: "denied",
|
||||
security_storage: "granted",
|
||||
});
|
||||
});
|
||||
|
||||
it("grants analytics signals when analytics is true", () => {
|
||||
const signals = buildConsentSignals({ analytics: true, marketing: false });
|
||||
expect(signals.analytics_storage).toBe("granted");
|
||||
expect(signals.ad_storage).toBe("denied");
|
||||
});
|
||||
|
||||
it("grants marketing signals when marketing is true", () => {
|
||||
const signals = buildConsentSignals({ analytics: false, marketing: true });
|
||||
expect(signals.analytics_storage).toBe("denied");
|
||||
expect(signals.ad_storage).toBe("granted");
|
||||
expect(signals.ad_user_data).toBe("granted");
|
||||
expect(signals.ad_personalization).toBe("granted");
|
||||
expect(signals.functionality_storage).toBe("granted");
|
||||
expect(signals.personalization_storage).toBe("granted");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAdvancedSettings", () => {
|
||||
it("enables redaction when marketing is denied", () => {
|
||||
expect(
|
||||
buildAdvancedSettings({ marketing: false }, {
|
||||
adsDataRedaction: true,
|
||||
urlPassthrough: true,
|
||||
}),
|
||||
).toEqual({
|
||||
ads_data_redaction: true,
|
||||
url_passthrough: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("disables redaction when marketing is granted", () => {
|
||||
expect(
|
||||
buildAdvancedSettings({ marketing: true }, {
|
||||
adsDataRedaction: true,
|
||||
urlPassthrough: true,
|
||||
}),
|
||||
).toEqual({
|
||||
ads_data_redaction: false,
|
||||
url_passthrough: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDefaultDeniedState", () => {
|
||||
it("includes wait_for_update", () => {
|
||||
expect(getDefaultDeniedState(750).wait_for_update).toBe(750);
|
||||
});
|
||||
});
|
||||
41
src/index.d.ts
vendored
41
src/index.d.ts
vendored
|
|
@ -21,8 +21,21 @@ export interface ConsentConfig {
|
|||
urlPassthrough?: boolean;
|
||||
regionDefaults?: RegionDefault[];
|
||||
debug?: boolean;
|
||||
onStorageError?: (error: { reason: "invalid" | "storage"; detail?: unknown }) => void;
|
||||
}
|
||||
|
||||
export interface ConsentResult {
|
||||
ok: true;
|
||||
consent: UserConsent;
|
||||
}
|
||||
|
||||
export interface ConsentError {
|
||||
ok: false;
|
||||
reason: "invalid" | "storage";
|
||||
}
|
||||
|
||||
export type PersistResult = ConsentResult | ConsentError;
|
||||
|
||||
export const DEFAULT_STORAGE_KEY: string;
|
||||
export const EVENT_CONSENT_UPDATED: string;
|
||||
export const EVENT_OPEN_PREFERENCES: string;
|
||||
|
|
@ -34,9 +47,9 @@ export function getConsentConfig(): ConsentConfig;
|
|||
export function defaultConfig: ConsentConfig;
|
||||
|
||||
export function getConsent(): UserConsent | null;
|
||||
export function setConsent(preferences: Partial<UserConsent>): void;
|
||||
export function acceptAll(): void;
|
||||
export function rejectAll(): void;
|
||||
export function setConsent(preferences: Partial<UserConsent>): PersistResult;
|
||||
export function acceptAll(): PersistResult;
|
||||
export function rejectAll(): PersistResult;
|
||||
export function hasAnalyticsConsent(): boolean;
|
||||
export function hasMarketingConsent(): boolean;
|
||||
export function hasConsentChoice(): boolean;
|
||||
|
|
@ -48,15 +61,31 @@ export function applyConsentUpdate(consent: UserConsent | null | undefined): voi
|
|||
export function buildGoogleConsentState(consent: UserConsent | null | undefined): Record<string, "granted" | "denied">;
|
||||
export function getConsentBootstrapScript(options?: Partial<ConsentConfig>): string;
|
||||
|
||||
export type CookieCategory = "necessary" | "analytics" | "marketing";
|
||||
|
||||
export interface CookieEntry {
|
||||
category: CookieCategory;
|
||||
name: string;
|
||||
purpose: string;
|
||||
duration: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export const COOKIE_CATEGORIES: readonly CookieCategory[];
|
||||
|
||||
export function isValidCookieEntry(entry: unknown): entry is CookieEntry;
|
||||
export function normalizeCookieEntries(cookies: unknown[]): CookieEntry[];
|
||||
export function groupCookiesByCategory(cookies: CookieEntry[]): Record<CookieCategory, CookieEntry[]>;
|
||||
|
||||
// Legacy aliases
|
||||
export const COOKIE_CONSENT_KEY: string;
|
||||
export const COOKIE_CONSENT_EVENT: string;
|
||||
export const COOKIE_CONSENT_OPEN_EVENT: string;
|
||||
export const CONSENT_TYPES: readonly string[];
|
||||
export function getCookieConsent(): UserConsent | null;
|
||||
export function setCookieConsent(preferences: Partial<UserConsent>): void;
|
||||
export function acceptAllCookies(): void;
|
||||
export function rejectAllCookies(): void;
|
||||
export function setCookieConsent(preferences: Partial<UserConsent>): PersistResult;
|
||||
export function acceptAllCookies(): PersistResult;
|
||||
export function rejectAllCookies(): PersistResult;
|
||||
export function hasCookieConsentChoice(): boolean;
|
||||
export function syncGoogleConsentFromStorage(): void;
|
||||
export function openCookiePreferences(): void;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,14 @@ export {
|
|||
openCookiePreferences,
|
||||
} from "./consent-api.js";
|
||||
|
||||
// Cookie inventory helpers
|
||||
export {
|
||||
COOKIE_CATEGORIES,
|
||||
groupCookiesByCategory,
|
||||
isValidCookieEntry,
|
||||
normalizeCookieEntries,
|
||||
} from "./cookie-schema.js";
|
||||
|
||||
// Google Consent Mode
|
||||
export { applyConsentDefaults, applyConsentUpdate } from "./google/consent-lifecycle.js";
|
||||
export { buildConsentSignals as buildGoogleConsentState } from "./google/consent-signals.js";
|
||||
|
|
|
|||
34
src/preferences.test.js
Normal file
34
src/preferences.test.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { parseUserConsent, serializeUserConsent } from "./preferences.js";
|
||||
|
||||
describe("parseUserConsent", () => {
|
||||
it("parses legacy accepted/rejected strings", () => {
|
||||
expect(parseUserConsent("accepted")).toEqual({ analytics: true, marketing: true });
|
||||
expect(parseUserConsent("rejected")).toEqual({ analytics: false, marketing: false });
|
||||
});
|
||||
|
||||
it("normalises partial objects", () => {
|
||||
expect(parseUserConsent({ analytics: 1, marketing: 0 })).toEqual({
|
||||
analytics: true,
|
||||
marketing: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null for invalid values", () => {
|
||||
expect(parseUserConsent(null)).toBeNull();
|
||||
expect(parseUserConsent(undefined)).toBeNull();
|
||||
expect(parseUserConsent("maybe")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("serializeUserConsent", () => {
|
||||
it("stores version and timestamp metadata", () => {
|
||||
const raw = serializeUserConsent({ analytics: true, marketing: false });
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
expect(parsed.analytics).toBe(true);
|
||||
expect(parsed.marketing).toBe(false);
|
||||
expect(parsed.version).toBe(1);
|
||||
expect(typeof parsed.updatedAt).toBe("string");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { groupCookiesByCategory, normalizeCookieEntries } from "../cookie-schema.js";
|
||||
import { getStorageKey } from "../config.js";
|
||||
import { EVENT_OPEN_PREFERENCES } from "../constants.js";
|
||||
import {
|
||||
acceptAll,
|
||||
|
|
@ -10,6 +12,8 @@ import {
|
|||
rejectAll,
|
||||
setConsent,
|
||||
} from "../consent-api.js";
|
||||
import CookieList from "./CookieList.jsx";
|
||||
import { buildThemeStyle } from "./theme.js";
|
||||
|
||||
const DEFAULT_LABELS = {
|
||||
title: "Cookie preferences",
|
||||
|
|
@ -26,6 +30,12 @@ const DEFAULT_LABELS = {
|
|||
marketingTitle: "Marketing",
|
||||
marketingDescription: "Used for ads and remarketing (Google Ads, Meta Pixel).",
|
||||
alwaysOn: "Always on",
|
||||
cookiesHeading: "View cookies",
|
||||
cookieName: "Name",
|
||||
cookiePurpose: "Purpose",
|
||||
cookieDuration: "Duration",
|
||||
cookieProvider: "Provider",
|
||||
storageError: "Your preferences could not be saved. Please try again.",
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -34,16 +44,29 @@ const DEFAULT_LABELS = {
|
|||
*/
|
||||
export default function CookieConsent({
|
||||
labels: labelOverrides,
|
||||
theme,
|
||||
cookies,
|
||||
policyHref = "/privacy",
|
||||
LinkComponent = Link,
|
||||
className = "",
|
||||
onConsentSaved,
|
||||
onConsentError,
|
||||
}) {
|
||||
const labels = { ...DEFAULT_LABELS, ...labelOverrides };
|
||||
const PolicyLink = LinkComponent;
|
||||
const themeStyle = useMemo(() => buildThemeStyle(theme), [theme]);
|
||||
const groupedCookies = useMemo(
|
||||
() => groupCookiesByCategory(normalizeCookieEntries(cookies)),
|
||||
[cookies],
|
||||
);
|
||||
const hasCookieInventory = Boolean(cookies?.length);
|
||||
const bannerRef = useRef(null);
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [mode, setMode] = useState("banner");
|
||||
const [analytics, setAnalytics] = useState(false);
|
||||
const [marketing, setMarketing] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setVisible(!hasConsentChoice());
|
||||
|
|
@ -56,42 +79,110 @@ export default function CookieConsent({
|
|||
setMarketing(current?.marketing ?? false);
|
||||
setMode("preferences");
|
||||
setVisible(true);
|
||||
setErrorMessage("");
|
||||
}
|
||||
|
||||
window.addEventListener(EVENT_OPEN_PREFERENCES, handleOpenPreferences);
|
||||
return () => window.removeEventListener(EVENT_OPEN_PREFERENCES, handleOpenPreferences);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function handleStorage(event) {
|
||||
if (event.key !== getStorageKey()) return;
|
||||
|
||||
if (event.newValue) {
|
||||
setVisible(false);
|
||||
setErrorMessage("");
|
||||
return;
|
||||
}
|
||||
|
||||
setMode("banner");
|
||||
setVisible(true);
|
||||
}
|
||||
|
||||
window.addEventListener("storage", handleStorage);
|
||||
return () => window.removeEventListener("storage", handleStorage);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
||||
const firstFocusable = bannerRef.current?.querySelector(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
||||
);
|
||||
firstFocusable?.focus();
|
||||
}, [visible, mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.key !== "Escape") return;
|
||||
|
||||
if (mode === "preferences" && !hasConsentChoice()) {
|
||||
setMode("banner");
|
||||
setErrorMessage("");
|
||||
return;
|
||||
}
|
||||
|
||||
setVisible(false);
|
||||
setErrorMessage("");
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [visible, mode]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
function close() {
|
||||
setVisible(false);
|
||||
setErrorMessage("");
|
||||
}
|
||||
|
||||
function handlePersistResult(result) {
|
||||
if (result.ok) {
|
||||
setErrorMessage("");
|
||||
onConsentSaved?.(result.consent);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage(labels.storageError);
|
||||
onConsentError?.({ reason: result.reason });
|
||||
}
|
||||
|
||||
function handleAcceptAll() {
|
||||
acceptAll();
|
||||
close();
|
||||
handlePersistResult(acceptAll());
|
||||
}
|
||||
|
||||
function handleRejectAll() {
|
||||
rejectAll();
|
||||
close();
|
||||
handlePersistResult(rejectAll());
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
setConsent({ analytics, marketing });
|
||||
close();
|
||||
handlePersistResult(setConsent({ analytics, marketing }));
|
||||
}
|
||||
|
||||
const bannerClassName = ["ccm-banner", className].filter(Boolean).join(" ");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ccm-banner"
|
||||
ref={bannerRef}
|
||||
className={bannerClassName}
|
||||
style={themeStyle}
|
||||
role="dialog"
|
||||
aria-live="polite"
|
||||
aria-label={labels.title}
|
||||
aria-modal="true"
|
||||
>
|
||||
<div className="ccm-banner__inner">
|
||||
{errorMessage ? (
|
||||
<p className="ccm-banner__error" role="alert">
|
||||
{errorMessage}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{mode === "banner" ? (
|
||||
<BannerView
|
||||
labels={labels}
|
||||
|
|
@ -99,7 +190,10 @@ export default function CookieConsent({
|
|||
policyHref={policyHref}
|
||||
onAccept={handleAcceptAll}
|
||||
onReject={handleRejectAll}
|
||||
onManage={() => setMode("preferences")}
|
||||
onManage={() => {
|
||||
setMode("preferences");
|
||||
setErrorMessage("");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<PreferencesView
|
||||
|
|
@ -108,6 +202,8 @@ export default function CookieConsent({
|
|||
policyHref={policyHref}
|
||||
analytics={analytics}
|
||||
marketing={marketing}
|
||||
groupedCookies={groupedCookies}
|
||||
hasCookieInventory={hasCookieInventory}
|
||||
onAnalyticsChange={setAnalytics}
|
||||
onMarketingChange={setMarketing}
|
||||
onSave={handleSave}
|
||||
|
|
@ -148,6 +244,8 @@ function PreferencesView({
|
|||
policyHref,
|
||||
analytics,
|
||||
marketing,
|
||||
groupedCookies,
|
||||
hasCookieInventory,
|
||||
onAnalyticsChange,
|
||||
onMarketingChange,
|
||||
onSave,
|
||||
|
|
@ -170,18 +268,24 @@ function PreferencesView({
|
|||
description={labels.necessaryDescription}
|
||||
locked
|
||||
badge={labels.alwaysOn}
|
||||
cookies={hasCookieInventory ? groupedCookies.necessary : null}
|
||||
labels={labels}
|
||||
/>
|
||||
<CategoryCard
|
||||
title={labels.analyticsTitle}
|
||||
description={labels.analyticsDescription}
|
||||
checked={analytics}
|
||||
onChange={onAnalyticsChange}
|
||||
cookies={hasCookieInventory ? groupedCookies.analytics : null}
|
||||
labels={labels}
|
||||
/>
|
||||
<CategoryCard
|
||||
title={labels.marketingTitle}
|
||||
description={labels.marketingDescription}
|
||||
checked={marketing}
|
||||
onChange={onMarketingChange}
|
||||
cookies={hasCookieInventory ? groupedCookies.marketing : null}
|
||||
labels={labels}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -200,7 +304,7 @@ function PreferencesView({
|
|||
);
|
||||
}
|
||||
|
||||
function CategoryCard({ title, description, checked, onChange, locked, badge }) {
|
||||
function CategoryCard({ title, description, checked, onChange, locked, badge, cookies, labels }) {
|
||||
if (locked) {
|
||||
return (
|
||||
<div className="ccm-banner__category ccm-banner__category--locked">
|
||||
|
|
@ -209,6 +313,7 @@ function CategoryCard({ title, description, checked, onChange, locked, badge })
|
|||
<span className="ccm-banner__badge">{badge}</span>
|
||||
</div>
|
||||
<p>{description}</p>
|
||||
{cookies ? <CookieList cookies={cookies} labels={labels} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -220,6 +325,7 @@ function CategoryCard({ title, description, checked, onChange, locked, badge })
|
|||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
||||
</div>
|
||||
<p>{description}</p>
|
||||
{cookies ? <CookieList cookies={cookies} labels={labels} /> : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
32
src/react/CookieList.jsx
Normal file
32
src/react/CookieList.jsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Expandable cookie inventory table for a single category.
|
||||
*/
|
||||
export default function CookieList({ cookies, labels }) {
|
||||
if (!cookies?.length) return null;
|
||||
|
||||
return (
|
||||
<details className="ccm-banner__cookies">
|
||||
<summary>{labels.cookiesHeading}</summary>
|
||||
<table className="ccm-banner__cookie-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{labels.cookieName}</th>
|
||||
<th scope="col">{labels.cookiePurpose}</th>
|
||||
<th scope="col">{labels.cookieDuration}</th>
|
||||
<th scope="col">{labels.cookieProvider}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cookies.map((cookie) => (
|
||||
<tr key={`${cookie.category}-${cookie.name}`}>
|
||||
<td>{cookie.name}</td>
|
||||
<td>{cookie.purpose}</td>
|
||||
<td>{cookie.duration}</td>
|
||||
<td>{cookie.provider}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
89
src/react/index.d.ts
vendored
Normal file
89
src/react/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import type { ComponentType, CSSProperties } from "react";
|
||||
import type {
|
||||
ConsentConfig,
|
||||
CookieEntry,
|
||||
PersistResult,
|
||||
UserConsent,
|
||||
} from "../index.js";
|
||||
|
||||
export interface CookieConsentLabels {
|
||||
title?: string;
|
||||
message?: string;
|
||||
policyLink?: string;
|
||||
accept?: string;
|
||||
reject?: string;
|
||||
manage?: string;
|
||||
save?: string;
|
||||
necessaryTitle?: string;
|
||||
necessaryDescription?: string;
|
||||
analyticsTitle?: string;
|
||||
analyticsDescription?: string;
|
||||
marketingTitle?: string;
|
||||
marketingDescription?: string;
|
||||
alwaysOn?: string;
|
||||
cookiesHeading?: string;
|
||||
cookieName?: string;
|
||||
cookiePurpose?: string;
|
||||
cookieDuration?: string;
|
||||
cookieProvider?: string;
|
||||
storageError?: string;
|
||||
}
|
||||
|
||||
export interface CookieConsentTheme {
|
||||
background?: string;
|
||||
text?: string;
|
||||
textMuted?: string;
|
||||
link?: string;
|
||||
primary?: string;
|
||||
primaryText?: string;
|
||||
secondaryBackground?: string;
|
||||
border?: string;
|
||||
accent?: string;
|
||||
}
|
||||
|
||||
export interface CookieConsentProps {
|
||||
labels?: Partial<CookieConsentLabels>;
|
||||
theme?: CookieConsentTheme;
|
||||
cookies?: CookieEntry[];
|
||||
policyHref?: string;
|
||||
LinkComponent?: ComponentType<{ href: string; children: React.ReactNode }>;
|
||||
className?: string;
|
||||
onConsentSaved?: (consent: UserConsent) => void;
|
||||
onConsentError?: (error: { reason: "invalid" | "storage" }) => void;
|
||||
}
|
||||
|
||||
export interface ConsentBootstrapProps {
|
||||
overrides?: Partial<ConsentConfig>;
|
||||
}
|
||||
|
||||
export interface GoogleTagManagerProps {
|
||||
gtmId: string;
|
||||
}
|
||||
|
||||
export interface FacebookPixelProps {
|
||||
pixelId: string;
|
||||
}
|
||||
|
||||
export const CookieConsent: ComponentType<CookieConsentProps>;
|
||||
export const ConsentBootstrap: ComponentType<ConsentBootstrapProps>;
|
||||
export const GoogleTagManager: ComponentType<GoogleTagManagerProps>;
|
||||
export const FacebookPixel: ComponentType<FacebookPixelProps>;
|
||||
|
||||
export function configureConsent(config?: ConsentConfig): ConsentConfig;
|
||||
export function getConsent(): UserConsent | null;
|
||||
export function setConsent(preferences: Partial<UserConsent>): PersistResult;
|
||||
export function acceptAll(): PersistResult;
|
||||
export function rejectAll(): PersistResult;
|
||||
export function hasAnalyticsConsent(): boolean;
|
||||
export function hasMarketingConsent(): boolean;
|
||||
export function openPreferences(): void;
|
||||
export function syncConsentFromStorage(): void;
|
||||
|
||||
export function getCookieConsent(): UserConsent | null;
|
||||
export function setCookieConsent(preferences: Partial<UserConsent>): PersistResult;
|
||||
export function acceptAllCookies(): PersistResult;
|
||||
export function rejectAllCookies(): PersistResult;
|
||||
export function openCookiePreferences(): void;
|
||||
export function syncGoogleConsentFromStorage(): void;
|
||||
|
||||
export function buildThemeStyle(theme?: CookieConsentTheme): CSSProperties;
|
||||
|
|
@ -2,6 +2,7 @@ export { default as CookieConsent } from "./CookieConsent.jsx";
|
|||
export { default as ConsentBootstrap } from "./ConsentBootstrap.jsx";
|
||||
export { default as GoogleTagManager } from "./GoogleTagManager.jsx";
|
||||
export { default as FacebookPixel } from "./FacebookPixel.jsx";
|
||||
export { buildThemeStyle, THEME_VAR_MAP } from "./theme.js";
|
||||
|
||||
export { configureConsent } from "../config.js";
|
||||
export {
|
||||
|
|
|
|||
29
src/react/theme.js
Normal file
29
src/react/theme.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/** Maps theme prop keys to CSS custom property names. */
|
||||
export const THEME_VAR_MAP = {
|
||||
background: "--ccm-bg",
|
||||
text: "--ccm-text",
|
||||
textMuted: "--ccm-text-muted",
|
||||
link: "--ccm-link",
|
||||
primary: "--ccm-primary",
|
||||
primaryText: "--ccm-primary-text",
|
||||
secondaryBackground: "--ccm-secondary-bg",
|
||||
border: "--ccm-border",
|
||||
accent: "--ccm-accent",
|
||||
};
|
||||
|
||||
/**
|
||||
* Build inline style object for CSS variable overrides.
|
||||
* @param {Record<string, string>|undefined} theme
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
export function buildThemeStyle(theme) {
|
||||
if (!theme || typeof theme !== "object") return {};
|
||||
|
||||
const style = {};
|
||||
for (const [key, value] of Object.entries(theme)) {
|
||||
if (value == null || value === "") continue;
|
||||
const cssVar = THEME_VAR_MAP[key];
|
||||
if (cssVar) style[cssVar] = value;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
24
src/react/theme.test.js
Normal file
24
src/react/theme.test.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { buildThemeStyle, THEME_VAR_MAP } from "./theme.js";
|
||||
|
||||
describe("buildThemeStyle", () => {
|
||||
it("maps theme keys to CSS custom properties", () => {
|
||||
expect(
|
||||
buildThemeStyle({
|
||||
primary: "#2563eb",
|
||||
background: "#0f172a",
|
||||
}),
|
||||
).toEqual({
|
||||
[THEME_VAR_MAP.primary]: "#2563eb",
|
||||
[THEME_VAR_MAP.background]: "#0f172a",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores empty values", () => {
|
||||
expect(buildThemeStyle({ primary: "", text: null })).toEqual({});
|
||||
});
|
||||
|
||||
it("returns empty object when theme is missing", () => {
|
||||
expect(buildThemeStyle()).toEqual({});
|
||||
});
|
||||
});
|
||||
41
src/storage.test.js
Normal file
41
src/storage.test.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { configureConsent } from "./config.js";
|
||||
import { readStoredConsent, writeStoredConsent } from "./storage.js";
|
||||
|
||||
const storage = new Map();
|
||||
|
||||
describe("storage", () => {
|
||||
beforeEach(() => {
|
||||
storage.clear();
|
||||
configureConsent({ storageKey: "test_consent" });
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
localStorage: {
|
||||
getItem: (key) => storage.get(key) ?? null,
|
||||
setItem: (key, value) => {
|
||||
storage.set(key, value);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when nothing is stored", () => {
|
||||
expect(readStoredConsent()).toBeNull();
|
||||
});
|
||||
|
||||
it("reads and writes consent objects", () => {
|
||||
writeStoredConsent({ analytics: true, marketing: false });
|
||||
expect(readStoredConsent()).toEqual({ analytics: true, marketing: false });
|
||||
});
|
||||
|
||||
it("falls back to legacy plain string values", () => {
|
||||
storage.set("test_consent", "accepted");
|
||||
expect(readStoredConsent()).toEqual({ analytics: true, marketing: true });
|
||||
});
|
||||
|
||||
it("returns null on SSR", () => {
|
||||
vi.stubGlobal("window", undefined);
|
||||
expect(readStoredConsent()).toBeNull();
|
||||
expect(writeStoredConsent({ analytics: true, marketing: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
101
src/styles.css
101
src/styles.css
|
|
@ -1,14 +1,24 @@
|
|||
/* Cookie Consent Module — banner styles */
|
||||
|
||||
.ccm-banner {
|
||||
--ccm-bg: rgba(5, 22, 52, 0.96);
|
||||
--ccm-text: #ffffff;
|
||||
--ccm-text-muted: rgba(255, 255, 255, 0.82);
|
||||
--ccm-link: #ffffff;
|
||||
--ccm-primary: #4a90d9;
|
||||
--ccm-primary-text: #ffffff;
|
||||
--ccm-secondary-bg: rgba(255, 255, 255, 0.1);
|
||||
--ccm-border: rgba(255, 255, 255, 0.14);
|
||||
--ccm-accent: #4a90d9;
|
||||
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10050;
|
||||
padding: 16px;
|
||||
background: rgba(5, 22, 52, 0.96);
|
||||
color: #fff;
|
||||
background: var(--ccm-bg);
|
||||
color: var(--ccm-text);
|
||||
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
|
@ -30,7 +40,7 @@
|
|||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: var(--ccm-text);
|
||||
}
|
||||
|
||||
.ccm-banner__text {
|
||||
|
|
@ -42,10 +52,22 @@
|
|||
}
|
||||
|
||||
.ccm-banner__text a {
|
||||
color: #fff;
|
||||
color: var(--ccm-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ccm-banner__error {
|
||||
flex: 1 1 100%;
|
||||
margin: 0;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
color: #fecaca;
|
||||
background: rgba(220, 38, 38, 0.18);
|
||||
border: 1px solid rgba(248, 113, 113, 0.35);
|
||||
}
|
||||
|
||||
.ccm-banner__categories {
|
||||
flex: 1 1 100%;
|
||||
display: grid;
|
||||
|
|
@ -56,7 +78,7 @@
|
|||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border: 1px solid var(--ccm-border);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -70,7 +92,7 @@
|
|||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
color: var(--ccm-text-muted);
|
||||
}
|
||||
|
||||
.ccm-banner__category-head {
|
||||
|
|
@ -88,7 +110,7 @@
|
|||
.ccm-banner__category-head input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: #4a90d9;
|
||||
accent-color: var(--ccm-accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +125,54 @@
|
|||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.ccm-banner__cookies {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ccm-banner__cookies summary {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--ccm-link);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.ccm-banner__cookies summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ccm-banner__cookies summary::before {
|
||||
content: "▸ ";
|
||||
}
|
||||
|
||||
.ccm-banner__cookies[open] summary::before {
|
||||
content: "▾ ";
|
||||
}
|
||||
|
||||
.ccm-banner__cookie-table {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ccm-banner__cookie-table th,
|
||||
.ccm-banner__cookie-table td {
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--ccm-border);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.ccm-banner__cookie-table th {
|
||||
font-weight: 600;
|
||||
color: var(--ccm-text);
|
||||
}
|
||||
|
||||
.ccm-banner__cookie-table td {
|
||||
color: var(--ccm-text-muted);
|
||||
}
|
||||
|
||||
.ccm-banner__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
@ -123,9 +193,9 @@
|
|||
}
|
||||
|
||||
.ccm-banner__btn--primary {
|
||||
background: #4a90d9;
|
||||
border: 1px solid #4a90d9;
|
||||
color: #fff;
|
||||
background: var(--ccm-primary);
|
||||
border: 1px solid var(--ccm-primary);
|
||||
color: var(--ccm-primary-text);
|
||||
}
|
||||
|
||||
.ccm-banner__btn--primary:hover {
|
||||
|
|
@ -133,9 +203,9 @@
|
|||
}
|
||||
|
||||
.ccm-banner__btn--secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: var(--ccm-secondary-bg);
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
color: #fff;
|
||||
color: var(--ccm-text);
|
||||
}
|
||||
|
||||
.ccm-banner__btn--secondary:hover {
|
||||
|
|
@ -145,7 +215,7 @@
|
|||
.ccm-banner__btn--ghost {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
color: #fff;
|
||||
color: var(--ccm-text);
|
||||
}
|
||||
|
||||
.ccm-banner__btn--ghost:hover {
|
||||
|
|
@ -161,4 +231,9 @@
|
|||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ccm-banner__cookie-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
46
src/types.js
Normal file
46
src/types.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* @typedef {Object} UserConsent
|
||||
* @property {boolean} analytics
|
||||
* @property {boolean} marketing
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} RegionDefault
|
||||
* @property {string[]} regions
|
||||
* @property {"granted"|"denied"} [ad_storage]
|
||||
* @property {"granted"|"denied"} [analytics_storage]
|
||||
* @property {"granted"|"denied"} [ad_user_data]
|
||||
* @property {"granted"|"denied"} [ad_personalization]
|
||||
* @property {"granted"|"denied"} [functionality_storage]
|
||||
* @property {"granted"|"denied"} [personalization_storage]
|
||||
* @property {"granted"|"denied"} [security_storage]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ConsentConfig
|
||||
* @property {string} [storageKey]
|
||||
* @property {number} [waitForUpdateMs]
|
||||
* @property {boolean} [adsDataRedaction]
|
||||
* @property {boolean} [urlPassthrough]
|
||||
* @property {RegionDefault[]} [regionDefaults]
|
||||
* @property {boolean} [debug]
|
||||
* @property {(error: { reason: string, detail?: unknown }) => void} [onStorageError]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ConsentResult
|
||||
* @property {true} ok
|
||||
* @property {UserConsent} consent
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ConsentError
|
||||
* @property {false} ok
|
||||
* @property {"invalid"|"storage"} reason
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {ConsentResult|ConsentError} PersistResult
|
||||
*/
|
||||
|
||||
export {};
|
||||
8
vitest.config.js
Normal file
8
vitest.config.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
include: ["src/**/*.test.js"],
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user