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
|
.env
|
||||||
dist/
|
dist/
|
||||||
coverage/
|
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)
|
- Google Consent Mode v2 (all 7 signals)
|
||||||
- Granular categories: Necessary / Analytics / Marketing
|
- 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.)
|
- Regional default consent (EEA, etc.)
|
||||||
- React banner + Next.js bootstrap component
|
- React banner + Next.js bootstrap component
|
||||||
- Optional GTM and Meta Pixel loaders (consent-gated)
|
- Optional GTM and Meta Pixel loaders (consent-gated)
|
||||||
- Framework-agnostic core API (vanilla JS supported)
|
- 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
|
// app/layout.jsx
|
||||||
import "@pdntechnology/cookie-consent/styles.css";
|
import "@pdntechnology/cookie-consent/styles.css";
|
||||||
import "@/lib/consent-config";
|
import "@/lib/consent-config";
|
||||||
import ConsentBootstrap from "@pdntechnology/cookie-consent/react/ConsentBootstrap";
|
import { ConsentBootstrap } from "@pdntechnology/cookie-consent/react";
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({ children }) {
|
||||||
return (
|
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 |
|
| Banner category | User choice | Google Consent Mode signals |
|
||||||
|-----------------|-------------|----------------------------|
|
|-----------------|-------------|----------------------------|
|
||||||
|
|
@ -161,6 +300,7 @@ src/
|
||||||
├── preferences.js Parse / serialize user choices
|
├── preferences.js Parse / serialize user choices
|
||||||
├── storage.js localStorage read/write
|
├── storage.js localStorage read/write
|
||||||
├── consent-api.js acceptAll, rejectAll, getConsent, …
|
├── consent-api.js acceptAll, rejectAll, getConsent, …
|
||||||
|
├── cookie-schema.js Cookie inventory helpers
|
||||||
├── google/
|
├── google/
|
||||||
│ ├── gtag.js dataLayer helpers
|
│ ├── gtag.js dataLayer helpers
|
||||||
│ ├── consent-signals.js Category → signal mapping
|
│ ├── consent-signals.js Category → signal mapping
|
||||||
|
|
@ -169,6 +309,8 @@ src/
|
||||||
└── react/
|
└── react/
|
||||||
├── ConsentBootstrap.jsx
|
├── ConsentBootstrap.jsx
|
||||||
├── CookieConsent.jsx
|
├── CookieConsent.jsx
|
||||||
|
├── CookieList.jsx
|
||||||
|
├── theme.js
|
||||||
├── GoogleTagManager.jsx
|
├── GoogleTagManager.jsx
|
||||||
└── FacebookPixel.jsx
|
└── FacebookPixel.jsx
|
||||||
```
|
```
|
||||||
|
|
@ -189,9 +331,9 @@ src/
|
||||||
| Function | Description |
|
| Function | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| `getConsent()` | Read stored preferences |
|
| `getConsent()` | Read stored preferences |
|
||||||
| `setConsent({ analytics, marketing })` | Save custom choice |
|
| `setConsent({ analytics, marketing })` | Save custom choice; returns `{ ok, consent \| reason }` |
|
||||||
| `acceptAll()` | Grant all categories |
|
| `acceptAll()` | Grant all categories; returns `{ ok, consent \| reason }` |
|
||||||
| `rejectAll()` | Deny optional categories |
|
| `rejectAll()` | Deny optional categories; returns `{ ok, consent \| reason }` |
|
||||||
| `hasAnalyticsConsent()` | Check analytics opt-in |
|
| `hasAnalyticsConsent()` | Check analytics opt-in |
|
||||||
| `hasMarketingConsent()` | Check marketing opt-in |
|
| `hasMarketingConsent()` | Check marketing opt-in |
|
||||||
| `openPreferences()` | Re-open banner panel |
|
| `openPreferences()` | Re-open banner panel |
|
||||||
|
|
@ -214,7 +356,18 @@ src/
|
||||||
| `EVENT_OPEN_PREFERENCES` | Preferences panel should open |
|
| `EVENT_OPEN_PREFERENCES` | Preferences panel should open |
|
||||||
| `GTM_CONSENT_EVENT` | Pushed to dataLayer (`cookie_consent_update`) |
|
| `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
|
```bash
|
||||||
# In this repo
|
# In this repo
|
||||||
|
|
@ -312,11 +475,7 @@ npm publish --access public
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome. Please open an issue before large changes.
|
See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports and pull requests are welcome.
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create a feature branch
|
|
||||||
3. Submit a pull request
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
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",
|
"name": "@pdntechnology/cookie-consent",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "Lightweight Google Consent Mode v2 CMP for React and Next.js",
|
"description": "Lightweight Google Consent Mode v2 CMP for React and Next.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
|
|
@ -9,18 +9,22 @@
|
||||||
"sideEffects": ["./src/styles.css"],
|
"sideEffects": ["./src/styles.css"],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
|
"types": "./src/index.d.ts",
|
||||||
"import": "./src/index.js",
|
"import": "./src/index.js",
|
||||||
"default": "./src/index.js"
|
"default": "./src/index.js"
|
||||||
},
|
},
|
||||||
"./react": {
|
"./react": {
|
||||||
|
"types": "./src/react/index.d.ts",
|
||||||
"import": "./src/react/index.js",
|
"import": "./src/react/index.js",
|
||||||
"default": "./src/react/index.js"
|
"default": "./src/react/index.js"
|
||||||
},
|
},
|
||||||
"./styles.css": "./src/styles.css"
|
"./styles.css": "./src/styles.css"
|
||||||
},
|
},
|
||||||
"files": ["src", "README.md", "LICENSE"],
|
"files": ["src", "README.md", "README.tr.md", "LICENSE", "CHANGELOG.md", "CONTRIBUTING.md"],
|
||||||
"scripts": {
|
"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": {
|
"peerDependencies": {
|
||||||
"next": ">=14.0.0",
|
"next": ">=14.0.0",
|
||||||
|
|
@ -32,6 +36,9 @@
|
||||||
"react": { "optional": true },
|
"react": { "optional": true },
|
||||||
"react-dom": { "optional": true }
|
"react-dom": { "optional": true }
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cookie-consent",
|
"cookie-consent",
|
||||||
"consent-mode",
|
"consent-mode",
|
||||||
|
|
@ -47,12 +54,12 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/pdntechnology/cookie-consent.git"
|
"url": "https://git.pdntechnology.com.tr/pdn/cookie-consent.git"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"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": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getConsentConfig } from "./config.js";
|
||||||
import { EVENT_CONSENT_UPDATED, EVENT_OPEN_PREFERENCES } from "./constants.js";
|
import { EVENT_CONSENT_UPDATED, EVENT_OPEN_PREFERENCES } from "./constants.js";
|
||||||
import { applyConsentUpdate } from "./google/consent-lifecycle.js";
|
import { applyConsentUpdate } from "./google/consent-lifecycle.js";
|
||||||
import { parseUserConsent } from "./preferences.js";
|
import { parseUserConsent } from "./preferences.js";
|
||||||
|
|
@ -23,26 +24,63 @@ export function hasMarketingConsent() {
|
||||||
|
|
||||||
// ─── Write ──────────────────────────────────────────────────────────────────
|
// ─── 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) {
|
function persistConsent(preferences) {
|
||||||
const consent = parseUserConsent(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);
|
applyConsentUpdate(consent);
|
||||||
window.dispatchEvent(new CustomEvent(EVENT_CONSENT_UPDATED, { detail: 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) {
|
export function setConsent(preferences) {
|
||||||
persistConsent(preferences);
|
return persistConsent(preferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("./types.js").PersistResult}
|
||||||
|
*/
|
||||||
export function acceptAll() {
|
export function acceptAll() {
|
||||||
persistConsent({ analytics: true, marketing: true });
|
return persistConsent({ analytics: true, marketing: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("./types.js").PersistResult}
|
||||||
|
*/
|
||||||
export function rejectAll() {
|
export function rejectAll() {
|
||||||
persistConsent({ analytics: false, marketing: false });
|
return persistConsent({ analytics: false, marketing: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Sync & UI ──────────────────────────────────────────────────────────────
|
// ─── 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 { getConsentConfig } from "../config.js";
|
||||||
import { getDefaultDeniedState } from "./consent-signals.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.
|
* Inline script for `<head>` — must execute before GTM snippet.
|
||||||
* Used by ConsentBootstrap and vanilla HTML integrations.
|
* Used by ConsentBootstrap and vanilla HTML integrations.
|
||||||
*/
|
*/
|
||||||
export function getConsentBootstrapScript(overrides = {}) {
|
export function getConsentBootstrapScript(overrides = {}) {
|
||||||
const config = { ...getConsentConfig(), ...overrides };
|
const config = { ...getConsentConfig(), ...overrides };
|
||||||
const wait = Number(config.waitForUpdateMs) || 500;
|
const bootstrapDefaults = buildBootstrapDefaults(config);
|
||||||
|
|
||||||
const regionLines = (config.regionDefaults || [])
|
const regionLines = (config.regionDefaults || [])
|
||||||
.filter((block) => Array.isArray(block.regions) && block.regions.length > 0)
|
.filter((block) => Array.isArray(block.regions) && block.regions.length > 0)
|
||||||
.map((block) => {
|
.map((block) => {
|
||||||
const { regions, ...signals } = 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});`;
|
return `gtag('consent','default',${payload});`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
const globalDefault = JSON.stringify(getDefaultDeniedState(wait));
|
const globalDefault = JSON.stringify(bootstrapDefaults);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
window.dataLayer=window.dataLayer||[];
|
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;
|
urlPassthrough?: boolean;
|
||||||
regionDefaults?: RegionDefault[];
|
regionDefaults?: RegionDefault[];
|
||||||
debug?: boolean;
|
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 DEFAULT_STORAGE_KEY: string;
|
||||||
export const EVENT_CONSENT_UPDATED: string;
|
export const EVENT_CONSENT_UPDATED: string;
|
||||||
export const EVENT_OPEN_PREFERENCES: string;
|
export const EVENT_OPEN_PREFERENCES: string;
|
||||||
|
|
@ -34,9 +47,9 @@ export function getConsentConfig(): ConsentConfig;
|
||||||
export function defaultConfig: ConsentConfig;
|
export function defaultConfig: ConsentConfig;
|
||||||
|
|
||||||
export function getConsent(): UserConsent | null;
|
export function getConsent(): UserConsent | null;
|
||||||
export function setConsent(preferences: Partial<UserConsent>): void;
|
export function setConsent(preferences: Partial<UserConsent>): PersistResult;
|
||||||
export function acceptAll(): void;
|
export function acceptAll(): PersistResult;
|
||||||
export function rejectAll(): void;
|
export function rejectAll(): PersistResult;
|
||||||
export function hasAnalyticsConsent(): boolean;
|
export function hasAnalyticsConsent(): boolean;
|
||||||
export function hasMarketingConsent(): boolean;
|
export function hasMarketingConsent(): boolean;
|
||||||
export function hasConsentChoice(): 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 buildGoogleConsentState(consent: UserConsent | null | undefined): Record<string, "granted" | "denied">;
|
||||||
export function getConsentBootstrapScript(options?: Partial<ConsentConfig>): string;
|
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
|
// Legacy aliases
|
||||||
export const COOKIE_CONSENT_KEY: string;
|
export const COOKIE_CONSENT_KEY: string;
|
||||||
export const COOKIE_CONSENT_EVENT: string;
|
export const COOKIE_CONSENT_EVENT: string;
|
||||||
export const COOKIE_CONSENT_OPEN_EVENT: string;
|
export const COOKIE_CONSENT_OPEN_EVENT: string;
|
||||||
export const CONSENT_TYPES: readonly string[];
|
export const CONSENT_TYPES: readonly string[];
|
||||||
export function getCookieConsent(): UserConsent | null;
|
export function getCookieConsent(): UserConsent | null;
|
||||||
export function setCookieConsent(preferences: Partial<UserConsent>): void;
|
export function setCookieConsent(preferences: Partial<UserConsent>): PersistResult;
|
||||||
export function acceptAllCookies(): void;
|
export function acceptAllCookies(): PersistResult;
|
||||||
export function rejectAllCookies(): void;
|
export function rejectAllCookies(): PersistResult;
|
||||||
export function hasCookieConsentChoice(): boolean;
|
export function hasCookieConsentChoice(): boolean;
|
||||||
export function syncGoogleConsentFromStorage(): void;
|
export function syncGoogleConsentFromStorage(): void;
|
||||||
export function openCookiePreferences(): void;
|
export function openCookiePreferences(): void;
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,14 @@ export {
|
||||||
openCookiePreferences,
|
openCookiePreferences,
|
||||||
} from "./consent-api.js";
|
} from "./consent-api.js";
|
||||||
|
|
||||||
|
// Cookie inventory helpers
|
||||||
|
export {
|
||||||
|
COOKIE_CATEGORIES,
|
||||||
|
groupCookiesByCategory,
|
||||||
|
isValidCookieEntry,
|
||||||
|
normalizeCookieEntries,
|
||||||
|
} from "./cookie-schema.js";
|
||||||
|
|
||||||
// Google Consent Mode
|
// Google Consent Mode
|
||||||
export { applyConsentDefaults, applyConsentUpdate } from "./google/consent-lifecycle.js";
|
export { applyConsentDefaults, applyConsentUpdate } from "./google/consent-lifecycle.js";
|
||||||
export { buildConsentSignals as buildGoogleConsentState } from "./google/consent-signals.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";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import Link from "next/link";
|
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 { EVENT_OPEN_PREFERENCES } from "../constants.js";
|
||||||
import {
|
import {
|
||||||
acceptAll,
|
acceptAll,
|
||||||
|
|
@ -10,6 +12,8 @@ import {
|
||||||
rejectAll,
|
rejectAll,
|
||||||
setConsent,
|
setConsent,
|
||||||
} from "../consent-api.js";
|
} from "../consent-api.js";
|
||||||
|
import CookieList from "./CookieList.jsx";
|
||||||
|
import { buildThemeStyle } from "./theme.js";
|
||||||
|
|
||||||
const DEFAULT_LABELS = {
|
const DEFAULT_LABELS = {
|
||||||
title: "Cookie preferences",
|
title: "Cookie preferences",
|
||||||
|
|
@ -26,6 +30,12 @@ const DEFAULT_LABELS = {
|
||||||
marketingTitle: "Marketing",
|
marketingTitle: "Marketing",
|
||||||
marketingDescription: "Used for ads and remarketing (Google Ads, Meta Pixel).",
|
marketingDescription: "Used for ads and remarketing (Google Ads, Meta Pixel).",
|
||||||
alwaysOn: "Always on",
|
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({
|
export default function CookieConsent({
|
||||||
labels: labelOverrides,
|
labels: labelOverrides,
|
||||||
|
theme,
|
||||||
|
cookies,
|
||||||
policyHref = "/privacy",
|
policyHref = "/privacy",
|
||||||
LinkComponent = Link,
|
LinkComponent = Link,
|
||||||
|
className = "",
|
||||||
|
onConsentSaved,
|
||||||
|
onConsentError,
|
||||||
}) {
|
}) {
|
||||||
const labels = { ...DEFAULT_LABELS, ...labelOverrides };
|
const labels = { ...DEFAULT_LABELS, ...labelOverrides };
|
||||||
const PolicyLink = LinkComponent;
|
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 [visible, setVisible] = useState(false);
|
||||||
const [mode, setMode] = useState("banner");
|
const [mode, setMode] = useState("banner");
|
||||||
const [analytics, setAnalytics] = useState(false);
|
const [analytics, setAnalytics] = useState(false);
|
||||||
const [marketing, setMarketing] = useState(false);
|
const [marketing, setMarketing] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVisible(!hasConsentChoice());
|
setVisible(!hasConsentChoice());
|
||||||
|
|
@ -56,42 +79,110 @@ export default function CookieConsent({
|
||||||
setMarketing(current?.marketing ?? false);
|
setMarketing(current?.marketing ?? false);
|
||||||
setMode("preferences");
|
setMode("preferences");
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
|
setErrorMessage("");
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener(EVENT_OPEN_PREFERENCES, handleOpenPreferences);
|
window.addEventListener(EVENT_OPEN_PREFERENCES, handleOpenPreferences);
|
||||||
return () => window.removeEventListener(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;
|
if (!visible) return null;
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
|
setErrorMessage("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePersistResult(result) {
|
||||||
|
if (result.ok) {
|
||||||
|
setErrorMessage("");
|
||||||
|
onConsentSaved?.(result.consent);
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorMessage(labels.storageError);
|
||||||
|
onConsentError?.({ reason: result.reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAcceptAll() {
|
function handleAcceptAll() {
|
||||||
acceptAll();
|
handlePersistResult(acceptAll());
|
||||||
close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRejectAll() {
|
function handleRejectAll() {
|
||||||
rejectAll();
|
handlePersistResult(rejectAll());
|
||||||
close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
setConsent({ analytics, marketing });
|
handlePersistResult(setConsent({ analytics, marketing }));
|
||||||
close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bannerClassName = ["ccm-banner", className].filter(Boolean).join(" ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="ccm-banner"
|
ref={bannerRef}
|
||||||
|
className={bannerClassName}
|
||||||
|
style={themeStyle}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
aria-label={labels.title}
|
aria-label={labels.title}
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
<div className="ccm-banner__inner">
|
<div className="ccm-banner__inner">
|
||||||
|
{errorMessage ? (
|
||||||
|
<p className="ccm-banner__error" role="alert">
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{mode === "banner" ? (
|
{mode === "banner" ? (
|
||||||
<BannerView
|
<BannerView
|
||||||
labels={labels}
|
labels={labels}
|
||||||
|
|
@ -99,7 +190,10 @@ export default function CookieConsent({
|
||||||
policyHref={policyHref}
|
policyHref={policyHref}
|
||||||
onAccept={handleAcceptAll}
|
onAccept={handleAcceptAll}
|
||||||
onReject={handleRejectAll}
|
onReject={handleRejectAll}
|
||||||
onManage={() => setMode("preferences")}
|
onManage={() => {
|
||||||
|
setMode("preferences");
|
||||||
|
setErrorMessage("");
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PreferencesView
|
<PreferencesView
|
||||||
|
|
@ -108,6 +202,8 @@ export default function CookieConsent({
|
||||||
policyHref={policyHref}
|
policyHref={policyHref}
|
||||||
analytics={analytics}
|
analytics={analytics}
|
||||||
marketing={marketing}
|
marketing={marketing}
|
||||||
|
groupedCookies={groupedCookies}
|
||||||
|
hasCookieInventory={hasCookieInventory}
|
||||||
onAnalyticsChange={setAnalytics}
|
onAnalyticsChange={setAnalytics}
|
||||||
onMarketingChange={setMarketing}
|
onMarketingChange={setMarketing}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
|
|
@ -148,6 +244,8 @@ function PreferencesView({
|
||||||
policyHref,
|
policyHref,
|
||||||
analytics,
|
analytics,
|
||||||
marketing,
|
marketing,
|
||||||
|
groupedCookies,
|
||||||
|
hasCookieInventory,
|
||||||
onAnalyticsChange,
|
onAnalyticsChange,
|
||||||
onMarketingChange,
|
onMarketingChange,
|
||||||
onSave,
|
onSave,
|
||||||
|
|
@ -170,18 +268,24 @@ function PreferencesView({
|
||||||
description={labels.necessaryDescription}
|
description={labels.necessaryDescription}
|
||||||
locked
|
locked
|
||||||
badge={labels.alwaysOn}
|
badge={labels.alwaysOn}
|
||||||
|
cookies={hasCookieInventory ? groupedCookies.necessary : null}
|
||||||
|
labels={labels}
|
||||||
/>
|
/>
|
||||||
<CategoryCard
|
<CategoryCard
|
||||||
title={labels.analyticsTitle}
|
title={labels.analyticsTitle}
|
||||||
description={labels.analyticsDescription}
|
description={labels.analyticsDescription}
|
||||||
checked={analytics}
|
checked={analytics}
|
||||||
onChange={onAnalyticsChange}
|
onChange={onAnalyticsChange}
|
||||||
|
cookies={hasCookieInventory ? groupedCookies.analytics : null}
|
||||||
|
labels={labels}
|
||||||
/>
|
/>
|
||||||
<CategoryCard
|
<CategoryCard
|
||||||
title={labels.marketingTitle}
|
title={labels.marketingTitle}
|
||||||
description={labels.marketingDescription}
|
description={labels.marketingDescription}
|
||||||
checked={marketing}
|
checked={marketing}
|
||||||
onChange={onMarketingChange}
|
onChange={onMarketingChange}
|
||||||
|
cookies={hasCookieInventory ? groupedCookies.marketing : null}
|
||||||
|
labels={labels}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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) {
|
if (locked) {
|
||||||
return (
|
return (
|
||||||
<div className="ccm-banner__category ccm-banner__category--locked">
|
<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>
|
<span className="ccm-banner__badge">{badge}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
|
{cookies ? <CookieList cookies={cookies} labels={labels} /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -220,6 +325,7 @@ function CategoryCard({ title, description, checked, onChange, locked, badge })
|
||||||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
||||||
</div>
|
</div>
|
||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
|
{cookies ? <CookieList cookies={cookies} labels={labels} /> : null}
|
||||||
</label>
|
</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 ConsentBootstrap } from "./ConsentBootstrap.jsx";
|
||||||
export { default as GoogleTagManager } from "./GoogleTagManager.jsx";
|
export { default as GoogleTagManager } from "./GoogleTagManager.jsx";
|
||||||
export { default as FacebookPixel } from "./FacebookPixel.jsx";
|
export { default as FacebookPixel } from "./FacebookPixel.jsx";
|
||||||
|
export { buildThemeStyle, THEME_VAR_MAP } from "./theme.js";
|
||||||
|
|
||||||
export { configureConsent } from "../config.js";
|
export { configureConsent } from "../config.js";
|
||||||
export {
|
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 */
|
/* Cookie Consent Module — banner styles */
|
||||||
|
|
||||||
.ccm-banner {
|
.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;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 10050;
|
z-index: 10050;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: rgba(5, 22, 52, 0.96);
|
background: var(--ccm-bg);
|
||||||
color: #fff;
|
color: var(--ccm-text);
|
||||||
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,7 +40,7 @@
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: var(--ccm-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ccm-banner__text {
|
.ccm-banner__text {
|
||||||
|
|
@ -42,10 +52,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ccm-banner__text a {
|
.ccm-banner__text a {
|
||||||
color: #fff;
|
color: var(--ccm-link);
|
||||||
text-decoration: underline;
|
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 {
|
.ccm-banner__categories {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -56,7 +78,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border-radius: 10px;
|
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);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +92,7 @@
|
||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
color: rgba(255, 255, 255, 0.82);
|
color: var(--ccm-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ccm-banner__category-head {
|
.ccm-banner__category-head {
|
||||||
|
|
@ -88,7 +110,7 @@
|
||||||
.ccm-banner__category-head input[type="checkbox"] {
|
.ccm-banner__category-head input[type="checkbox"] {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
accent-color: #4a90d9;
|
accent-color: var(--ccm-accent);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,6 +125,54 @@
|
||||||
color: rgba(255, 255, 255, 0.9);
|
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 {
|
.ccm-banner__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
@ -123,9 +193,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ccm-banner__btn--primary {
|
.ccm-banner__btn--primary {
|
||||||
background: #4a90d9;
|
background: var(--ccm-primary);
|
||||||
border: 1px solid #4a90d9;
|
border: 1px solid var(--ccm-primary);
|
||||||
color: #fff;
|
color: var(--ccm-primary-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ccm-banner__btn--primary:hover {
|
.ccm-banner__btn--primary:hover {
|
||||||
|
|
@ -133,9 +203,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ccm-banner__btn--secondary {
|
.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);
|
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||||
color: #fff;
|
color: var(--ccm-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ccm-banner__btn--secondary:hover {
|
.ccm-banner__btn--secondary:hover {
|
||||||
|
|
@ -145,7 +215,7 @@
|
||||||
.ccm-banner__btn--ghost {
|
.ccm-banner__btn--ghost {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||||
color: #fff;
|
color: var(--ccm-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ccm-banner__btn--ghost:hover {
|
.ccm-banner__btn--ghost:hover {
|
||||||
|
|
@ -161,4 +231,9 @@
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
text-align: center;
|
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