Add cookie-banner React example app
Interactive playground with themed banner, headless components, and debug tabs demonstrating programmatic consent access via getConsent() across separate bundles. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
96
examples/cookie-banner-react/src/tabs/ConfigTab.tsx
Normal file
96
examples/cookie-banner-react/src/tabs/ConfigTab.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useState } from "react";
|
||||
import { useConfig } from "../hooks/useConfig";
|
||||
|
||||
export function ConfigTab() {
|
||||
const [config, setConfig] = useConfig();
|
||||
const [bannerId, setBannerId] = useState(config.bannerId);
|
||||
const [baseUrl, setBaseUrl] = useState(config.baseUrl);
|
||||
|
||||
const dirty =
|
||||
bannerId !== config.bannerId || baseUrl !== config.baseUrl;
|
||||
|
||||
const save = () => {
|
||||
setConfig({ bannerId, baseUrl });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Configuration</h2>
|
||||
<p style={{ color: "#666", marginBottom: 16 }}>
|
||||
Set the banner ID and base URL for the cookie banner API. These values
|
||||
are persisted to localStorage and used by every other tab.
|
||||
</p>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: "block", marginBottom: 4, fontWeight: "bold" }}>
|
||||
Banner ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={bannerId}
|
||||
onChange={(e) => setBannerId(e.target.value)}
|
||||
placeholder="e.g. cm9xkz5ab000208jx1yy99abc"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: 500,
|
||||
padding: "6px 8px",
|
||||
fontFamily: "monospace",
|
||||
fontSize: 14,
|
||||
border: "1px solid #ccc",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: "block", marginBottom: 4, fontWeight: "bold" }}>
|
||||
Base URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
placeholder="e.g. https://cookie-banner.getprobo.com/v1/banners/"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: 500,
|
||||
padding: "6px 8px",
|
||||
fontFamily: "monospace",
|
||||
fontSize: 14,
|
||||
border: "1px solid #ccc",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={save}
|
||||
disabled={!dirty || !bannerId || !baseUrl}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
fontWeight: "bold",
|
||||
cursor: dirty && bannerId && baseUrl ? "pointer" : "not-allowed",
|
||||
opacity: dirty && bannerId && baseUrl ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
|
||||
{config.bannerId && config.baseUrl && (
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<h3>Current saved config</h3>
|
||||
<pre
|
||||
style={{
|
||||
background: "#f5f5f5",
|
||||
padding: 12,
|
||||
border: "1px solid #ddd",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(config, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
195
examples/cookie-banner-react/src/tabs/DebugTab.tsx
Normal file
195
examples/cookie-banner-react/src/tabs/DebugTab.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getConsent } from "@probo/cookie-banner/consent";
|
||||
import type { ConsentData } from "@probo/cookie-banner/consent";
|
||||
import { useConfig } from "../hooks/useConfig";
|
||||
|
||||
interface ConsentSnapshot {
|
||||
ready: boolean;
|
||||
hasResponse: boolean;
|
||||
data: ConsentData;
|
||||
}
|
||||
|
||||
function readSnapshot(): ConsentSnapshot {
|
||||
const mgr = getConsent();
|
||||
return {
|
||||
ready: mgr.ready,
|
||||
hasResponse: mgr.hasResponse,
|
||||
data: mgr.getAll(),
|
||||
};
|
||||
}
|
||||
|
||||
function readVisitorId(bannerId: string): string | null {
|
||||
if (!bannerId) return null;
|
||||
try {
|
||||
return localStorage.getItem(`probo_consent:${bannerId}:vid`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readCookie(): string | null {
|
||||
try {
|
||||
const prefix = "probo_consent=";
|
||||
const entry = document.cookie
|
||||
.split("; ")
|
||||
.find((c) => c.startsWith(prefix));
|
||||
if (!entry) return null;
|
||||
return decodeURIComponent(entry.substring(prefix.length));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function DebugTab() {
|
||||
const [config] = useConfig();
|
||||
const [snapshot, setSnapshot] = useState<ConsentSnapshot>(readSnapshot);
|
||||
const [visitorId, setVisitorId] = useState<string | null>(() =>
|
||||
readVisitorId(config.bannerId),
|
||||
);
|
||||
const [cookie, setCookie] = useState<string | null>(readCookie);
|
||||
|
||||
useEffect(() => {
|
||||
const mgr = getConsent();
|
||||
return mgr.subscribe(() => {
|
||||
setSnapshot(readSnapshot());
|
||||
setVisitorId(readVisitorId(config.bannerId));
|
||||
setCookie(readCookie());
|
||||
});
|
||||
}, [config.bannerId]);
|
||||
|
||||
useEffect(() => {
|
||||
setVisitorId(readVisitorId(config.bannerId));
|
||||
setCookie(readCookie());
|
||||
}, [config.bannerId]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Debug</h2>
|
||||
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #ccc",
|
||||
padding: 12,
|
||||
background: "#fafafa",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginTop: 0 }}>getConsent() State</h3>
|
||||
<pre
|
||||
style={{
|
||||
background: "#f0f0f0",
|
||||
padding: 8,
|
||||
border: "1px solid #ddd",
|
||||
overflow: "auto",
|
||||
margin: "0 0 12px 0",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(
|
||||
{ ready: snapshot.ready, hasResponse: snapshot.hasResponse },
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
|
||||
{Object.keys(snapshot.data).length === 0 ? (
|
||||
<p style={{ color: "#999", margin: 0 }}>
|
||||
No consent data yet. Interact with the banner to generate consent.
|
||||
</p>
|
||||
) : (
|
||||
<table
|
||||
style={{
|
||||
borderCollapse: "collapse",
|
||||
fontFamily: "monospace",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "4px 16px 4px 0",
|
||||
borderBottom: "1px solid #ccc",
|
||||
}}
|
||||
>
|
||||
Category
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "4px 0",
|
||||
borderBottom: "1px solid #ccc",
|
||||
}}
|
||||
>
|
||||
has()
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(snapshot.data).map(([cat, granted]) => (
|
||||
<tr key={cat}>
|
||||
<td style={{ padding: "4px 16px 4px 0" }}>{cat}</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "4px 0",
|
||||
color: granted ? "green" : "red",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{String(granted)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #ccc",
|
||||
padding: 12,
|
||||
background: "#fafafa",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginTop: 0 }}>Storage</h3>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<strong>Visitor ID</strong>{" "}
|
||||
<span style={{ fontFamily: "monospace", fontSize: 13, color: "#666" }}>
|
||||
(localStorage: probo_consent:{config.bannerId || "?"}:vid)
|
||||
</span>
|
||||
<pre
|
||||
style={{
|
||||
background: "#f0f0f0",
|
||||
padding: 8,
|
||||
border: "1px solid #ddd",
|
||||
overflow: "auto",
|
||||
margin: "4px 0 0 0",
|
||||
}}
|
||||
>
|
||||
{visitorId ?? "(not set)"}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>probo_consent cookie</strong>
|
||||
<pre
|
||||
style={{
|
||||
background: "#f0f0f0",
|
||||
padding: 8,
|
||||
border: "1px solid #ddd",
|
||||
overflow: "auto",
|
||||
margin: "4px 0 0 0",
|
||||
}}
|
||||
>
|
||||
{cookie
|
||||
? JSON.stringify(JSON.parse(cookie), null, 2)
|
||||
: "(not set)"}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
140
examples/cookie-banner-react/src/tabs/HeadlessTab.tsx
Normal file
140
examples/cookie-banner-react/src/tabs/HeadlessTab.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { registerHeadlessComponents } from "@probo/cookie-banner/headless";
|
||||
import { useConfig } from "../hooks/useConfig";
|
||||
import type { EventEntry } from "../App";
|
||||
|
||||
let registered = false;
|
||||
|
||||
interface HeadlessTabProps {
|
||||
events: EventEntry[];
|
||||
pushEvent: (type: string, detail: unknown) => void;
|
||||
}
|
||||
|
||||
export function HeadlessTab({ events, pushEvent }: HeadlessTabProps) {
|
||||
const [config] = useConfig();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!registered) {
|
||||
registerHeadlessComponents();
|
||||
registered = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container || !config.bannerId || !config.baseUrl) return;
|
||||
|
||||
container.innerHTML = "";
|
||||
|
||||
container.innerHTML = `
|
||||
<style>probo-banner, probo-preference-panel { display: block !important; }</style>
|
||||
<probo-cookie-banner-root banner-id="${config.bannerId}" base-url="${config.baseUrl}">
|
||||
<probo-banner>
|
||||
<div style="border:2px solid #333;padding:12px;margin-bottom:8px;">
|
||||
<strong>[probo-banner]</strong>
|
||||
<div style="margin-top:8px;">
|
||||
<probo-accept-button><button>Accept All</button></probo-accept-button>
|
||||
<probo-reject-button><button style="margin-left:8px;">Reject All</button></probo-reject-button>
|
||||
<probo-customize-button><button style="margin-left:8px;">Customize</button></probo-customize-button>
|
||||
</div>
|
||||
</div>
|
||||
</probo-banner>
|
||||
|
||||
<probo-preference-panel>
|
||||
<div style="border:2px dashed #666;padding:12px;margin-bottom:8px;">
|
||||
<strong>[probo-preference-panel]</strong>
|
||||
<probo-category-list>
|
||||
<template>
|
||||
<div style="border:1px solid #aaa;padding:8px;margin:4px 0;">
|
||||
<span data-slot="name" style="font-weight:bold;"></span>:
|
||||
<span data-slot="description"></span>
|
||||
<probo-category-toggle>
|
||||
<label style="margin-left:8px;"><input type="checkbox" /> toggle</label>
|
||||
</probo-category-toggle>
|
||||
<probo-cookie-list hidden>
|
||||
<template>
|
||||
<div style="padding:4px 0 4px 16px;font-size:13px;">
|
||||
<span data-slot="name" style="font-weight:bold;"></span>
|
||||
— <span data-slot="description"></span>
|
||||
</div>
|
||||
</template>
|
||||
</probo-cookie-list>
|
||||
</div>
|
||||
</template>
|
||||
</probo-category-list>
|
||||
<div style="margin-top:8px;">
|
||||
<probo-accept-button><button>Accept All</button></probo-accept-button>
|
||||
<probo-reject-button><button style="margin-left:8px;">Reject All</button></probo-reject-button>
|
||||
<probo-save-button><button style="margin-left:8px;">Save Preferences</button></probo-save-button>
|
||||
</div>
|
||||
</div>
|
||||
</probo-preference-panel>
|
||||
</probo-cookie-banner-root>
|
||||
`;
|
||||
|
||||
const root = container.querySelector("probo-cookie-banner-root");
|
||||
if (root) {
|
||||
root.addEventListener("probo-ready", (e: Event) =>
|
||||
pushEvent("probo-ready", (e as CustomEvent).detail),
|
||||
);
|
||||
root.addEventListener("probo-consent", (e: Event) =>
|
||||
pushEvent("probo-consent", (e as CustomEvent).detail),
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
container.innerHTML = "";
|
||||
};
|
||||
}, [config.bannerId, config.baseUrl, pushEvent]);
|
||||
|
||||
if (!config.bannerId || !config.baseUrl) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Headless Components</h2>
|
||||
<p style={{ color: "tomato" }}>
|
||||
Set banner ID and base URL in the Config tab first.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Headless Components</h2>
|
||||
<p style={{ color: "#666", marginBottom: 16 }}>
|
||||
Uses <code>registerHeadlessComponents()</code> and renders raw headless
|
||||
elements with no themed styling. Borders show element boundaries.
|
||||
</p>
|
||||
|
||||
<div ref={containerRef} />
|
||||
|
||||
<h3>Events ({events.length})</h3>
|
||||
{events.length === 0 ? (
|
||||
<p style={{ color: "#999" }}>No events yet.</p>
|
||||
) : (
|
||||
events.map((ev, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
marginBottom: 8,
|
||||
border: "1px solid #ddd",
|
||||
padding: 8,
|
||||
background: "#f5f5f5",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: "bold", marginBottom: 4 }}>
|
||||
{ev.type}{" "}
|
||||
<span style={{ fontWeight: "normal", color: "#999" }}>
|
||||
{ev.time}
|
||||
</span>
|
||||
</div>
|
||||
<pre style={{ margin: 0, overflow: "auto" }}>
|
||||
{JSON.stringify(ev.detail, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
94
examples/cookie-banner-react/src/tabs/ThemedBannerTab.tsx
Normal file
94
examples/cookie-banner-react/src/tabs/ThemedBannerTab.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { registerCookieBanner } from "@probo/cookie-banner";
|
||||
import { useConfig } from "../hooks/useConfig";
|
||||
import type { EventEntry } from "../App";
|
||||
|
||||
let registered = false;
|
||||
|
||||
interface ThemedBannerTabProps {
|
||||
events: EventEntry[];
|
||||
pushEvent: (type: string, detail: unknown) => void;
|
||||
}
|
||||
|
||||
export function ThemedBannerTab({ events, pushEvent }: ThemedBannerTabProps) {
|
||||
const [config] = useConfig();
|
||||
const elRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!registered) {
|
||||
registerCookieBanner();
|
||||
registered = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const attachListeners = useCallback(
|
||||
(el: HTMLElement | null) => {
|
||||
elRef.current = el;
|
||||
if (!el) return;
|
||||
|
||||
el.addEventListener("probo-ready", (e: Event) =>
|
||||
pushEvent("probo-ready", (e as CustomEvent).detail),
|
||||
);
|
||||
el.addEventListener("probo-consent", (e: Event) =>
|
||||
pushEvent("probo-consent", (e as CustomEvent).detail),
|
||||
);
|
||||
},
|
||||
[pushEvent],
|
||||
);
|
||||
|
||||
if (!config.bannerId || !config.baseUrl) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Themed Banner</h2>
|
||||
<p style={{ color: "tomato" }}>
|
||||
Set banner ID and base URL in the Config tab first.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Themed Banner</h2>
|
||||
<p style={{ color: "#666", marginBottom: 16 }}>
|
||||
Uses <code>registerCookieBanner()</code> and renders{" "}
|
||||
<code><probo-cookie-banner></code>. The banner appears in the
|
||||
bottom-right corner.
|
||||
</p>
|
||||
|
||||
<probo-cookie-banner
|
||||
ref={attachListeners}
|
||||
banner-id={config.bannerId}
|
||||
base-url={config.baseUrl}
|
||||
position="bottom-right"
|
||||
/>
|
||||
|
||||
<h3>Events ({events.length})</h3>
|
||||
{events.length === 0 ? (
|
||||
<p style={{ color: "#999" }}>No events yet.</p>
|
||||
) : (
|
||||
events.map((ev, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
marginBottom: 8,
|
||||
border: "1px solid #ddd",
|
||||
padding: 8,
|
||||
background: "#f5f5f5",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: "bold", marginBottom: 4 }}>
|
||||
{ev.type}{" "}
|
||||
<span style={{ fontWeight: "normal", color: "#999" }}>
|
||||
{ev.time}
|
||||
</span>
|
||||
</div>
|
||||
<pre style={{ margin: 0, overflow: "auto" }}>
|
||||
{JSON.stringify(ev.detail, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user