ConsentOS — a privacy-first cookie consent management platform. Self-hosted, source-available alternative to OneTrust, Cookiebot, and CookieYes. Full standards coverage (IAB TCF v2.2, GPP v1, Google Consent Mode v2, GPC, Shopify Customer Privacy API), multi-tenant architecture with role-based access, configuration cascade (system → org → group → site → region), dark-pattern detection in the scanner, and a tamper-evident consent record audit trail. This is the initial public release. Prior development history is retained internally. See README.md for the feature list, architecture overview, and quick-start instructions. Licensed under the Elastic Licence 2.0 — self-host freely; do not resell as a managed service.
575 lines
20 KiB
TypeScript
575 lines
20 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
import type { BannerConfig, ButtonConfig } from '../types/api';
|
|
|
|
type DisplayMode = 'bottom_banner' | 'top_banner' | 'overlay' | 'corner_popup';
|
|
type CornerPosition = 'left' | 'right';
|
|
type Viewport = 'desktop' | 'mobile';
|
|
|
|
/* ── Default text values ─────────────────────────────────────────────── */
|
|
|
|
const DEFAULT_TITLE = 'We use cookies';
|
|
const DEFAULT_DESCRIPTION =
|
|
'We use cookies and similar technologies to enhance your browsing experience, ' +
|
|
'analyse site traffic, and personalise content. You can choose which categories to allow.';
|
|
const DEFAULT_ACCEPT_ALL = 'Accept all';
|
|
const DEFAULT_REJECT_ALL = 'Reject all';
|
|
const DEFAULT_MANAGE_PREFERENCES = 'Manage preferences';
|
|
const DEFAULT_SAVE_PREFERENCES = 'Save preferences';
|
|
|
|
interface Props {
|
|
bannerConfig: BannerConfig;
|
|
displayMode: DisplayMode;
|
|
cornerPosition?: CornerPosition;
|
|
viewport: Viewport;
|
|
privacyPolicyUrl: string | null;
|
|
siteUrl?: string | null;
|
|
previewLocale?: string;
|
|
}
|
|
|
|
export default function BannerPreview({
|
|
bannerConfig,
|
|
displayMode,
|
|
cornerPosition = 'right',
|
|
viewport,
|
|
privacyPolicyUrl,
|
|
siteUrl,
|
|
previewLocale,
|
|
}: Props) {
|
|
const [iframeLoadFailed, setIframeLoadFailed] = useState(false);
|
|
const [iframeLoaded, setIframeLoaded] = useState(false);
|
|
const siteIframeRef = useRef<HTMLIFrameElement>(null);
|
|
const bannerSrcdoc = useMemo(
|
|
() => buildBannerOnlyHtml(bannerConfig, displayMode, cornerPosition, privacyPolicyUrl, previewLocale),
|
|
[bannerConfig, displayMode, cornerPosition, privacyPolicyUrl, previewLocale],
|
|
);
|
|
const fallbackSrcdoc = useMemo(
|
|
() => buildPreviewHtml(bannerConfig, displayMode, cornerPosition, privacyPolicyUrl, previewLocale),
|
|
[bannerConfig, displayMode, cornerPosition, privacyPolicyUrl, previewLocale],
|
|
);
|
|
|
|
const fullSiteUrl = useMemo(() => {
|
|
if (!siteUrl) return null;
|
|
// Ensure the URL has a protocol
|
|
if (siteUrl.startsWith('http://') || siteUrl.startsWith('https://')) return siteUrl;
|
|
return `https://${siteUrl}`;
|
|
}, [siteUrl]);
|
|
|
|
// Reset state when the site URL changes
|
|
useEffect(() => {
|
|
setIframeLoadFailed(false);
|
|
setIframeLoaded(false);
|
|
}, [fullSiteUrl]);
|
|
|
|
const handleSiteIframeLoad = useCallback(() => {
|
|
// Check if the iframe actually loaded content by trying to access it
|
|
// If X-Frame-Options or CSP blocks it, the iframe will be blank
|
|
const iframe = siteIframeRef.current;
|
|
if (!iframe) return;
|
|
|
|
try {
|
|
// Try to detect if the iframe loaded — accessing contentDocument will throw
|
|
// for cross-origin frames, but that's fine (it means it loaded)
|
|
// If the iframe is blank/error, some browsers fire load anyway
|
|
const doc = iframe.contentDocument;
|
|
if (doc && doc.body && doc.body.innerHTML === '') {
|
|
// Empty body might mean it was blocked
|
|
setIframeLoadFailed(true);
|
|
} else {
|
|
setIframeLoaded(true);
|
|
}
|
|
} catch {
|
|
// Cross-origin — means the site loaded successfully
|
|
setIframeLoaded(true);
|
|
}
|
|
}, []);
|
|
|
|
const handleSiteIframeError = useCallback(() => {
|
|
setIframeLoadFailed(true);
|
|
}, []);
|
|
|
|
const width = viewport === 'mobile' ? 375 : '100%';
|
|
const useLiveSite = fullSiteUrl && !iframeLoadFailed;
|
|
|
|
return (
|
|
<div
|
|
className="relative overflow-hidden rounded-lg border border-border bg-mist"
|
|
style={{ height: 500 }}
|
|
data-testid="banner-preview"
|
|
>
|
|
{useLiveSite ? (
|
|
<>
|
|
{/* Live site iframe (background) */}
|
|
<iframe
|
|
ref={siteIframeRef}
|
|
src={fullSiteUrl}
|
|
title="Site preview"
|
|
sandbox="allow-scripts allow-same-origin"
|
|
onLoad={handleSiteIframeLoad}
|
|
onError={handleSiteIframeError}
|
|
style={{
|
|
width,
|
|
height: '100%',
|
|
border: 'none',
|
|
margin: viewport === 'mobile' ? '0 auto' : undefined,
|
|
display: 'block',
|
|
transition: 'width 0.3s ease',
|
|
opacity: iframeLoaded ? 1 : 0.3,
|
|
}}
|
|
/>
|
|
{/* Banner overlay on top of the live site */}
|
|
<iframe
|
|
srcDoc={bannerSrcdoc}
|
|
sandbox="allow-scripts"
|
|
title="Banner preview"
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
width: viewport === 'mobile' ? 375 : '100%',
|
|
height: '100%',
|
|
border: 'none',
|
|
margin: viewport === 'mobile' ? '0 auto' : undefined,
|
|
pointerEvents: 'none',
|
|
background: 'transparent',
|
|
}}
|
|
/>
|
|
{!iframeLoaded && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-mist/80">
|
|
<p className="text-sm text-text-secondary">Loading site preview…</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
/* Fallback: self-contained preview with placeholder content */
|
|
<iframe
|
|
srcDoc={fallbackSrcdoc}
|
|
sandbox="allow-scripts"
|
|
title="Banner preview"
|
|
style={{
|
|
width,
|
|
height: '100%',
|
|
border: 'none',
|
|
margin: viewport === 'mobile' ? '0 auto' : undefined,
|
|
display: 'block',
|
|
transition: 'width 0.3s ease',
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{iframeLoadFailed && fullSiteUrl && (
|
|
<div className="absolute bottom-2 left-2 rounded bg-status-warning-bg px-2 py-1 text-xs text-status-warning-fg ring-1 ring-status-warning-fg/20">
|
|
Could not load site preview — the site may block iframe embedding
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ── Banner-only HTML (transparent background, overlay on live site) ── */
|
|
|
|
function buildBannerOnlyHtml(
|
|
bc: BannerConfig,
|
|
displayMode: DisplayMode,
|
|
cornerPosition: CornerPosition,
|
|
privacyUrl: string | null,
|
|
previewLocale?: string,
|
|
): string {
|
|
const bg = bc.backgroundColour ?? '#ffffff';
|
|
const text = bc.textColour ?? '#1a1a2e';
|
|
const primary = bc.primaryColour ?? '#2563eb';
|
|
const font = bc.fontFamily ?? 'system-ui';
|
|
const radius = bc.borderRadius ?? 6;
|
|
const defaultButtonStyle = bc.buttonStyle ?? 'filled';
|
|
|
|
const positionStyles = getPositionStyles(displayMode, cornerPosition, radius);
|
|
const { rejectBtn, manageBtn, acceptBtn, closeBtn, logoHtml, cookieCount, privacyLink, titleText, descriptionText } =
|
|
buildBannerParts(bc, primary, text, radius, privacyUrl, defaultButtonStyle);
|
|
|
|
const fontLink = bc.customFontUrl
|
|
? `<link rel="stylesheet" href="${escapeHtml(bc.customFontUrl)}">`
|
|
: '';
|
|
|
|
const langAttr = previewLocale ? escapeHtml(previewLocale) : 'en';
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="${langAttr}">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
${fontLink}
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html, body { height: 100%; background: transparent; }
|
|
|
|
.consentos-banner {
|
|
${positionStyles}
|
|
background: ${bg};
|
|
color: ${text};
|
|
font-family: ${font}, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.12);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
border-radius: ${displayMode === 'overlay' || displayMode === 'corner_popup' ? radius + 'px' : '0'};
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.consentos-banner__content {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px 24px;
|
|
position: relative;
|
|
}
|
|
|
|
.cmp-logo { height: 28px; margin-bottom: 10px; display: block; }
|
|
.consentos-banner__title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
|
.consentos-banner__description { margin-bottom: 16px; opacity: 0.85; }
|
|
.consentos-banner__link { color: ${primary}; text-decoration: underline; }
|
|
.cmp-cookie-count { display: block; font-size: 12px; opacity: 0.6; margin-bottom: 12px; }
|
|
.consentos-banner__actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
|
|
.cmp-btn {
|
|
padding: 10px 20px;
|
|
border-radius: ${radius}px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.cmp-close {
|
|
position: absolute; top: 12px; right: 12px;
|
|
background: none; border: none; font-size: 22px;
|
|
cursor: pointer; color: ${text}; opacity: 0.5; line-height: 1;
|
|
}
|
|
|
|
.cmp-overlay-bg {
|
|
display: ${displayMode === 'overlay' ? 'block' : 'none'};
|
|
position: fixed; inset: 0;
|
|
background: rgba(0,0,0,0.4);
|
|
z-index: 2147483646;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.consentos-banner__actions { flex-direction: column; }
|
|
.cmp-btn { width: 100%; text-align: center; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="cmp-overlay-bg"></div>
|
|
<div class="consentos-banner" role="dialog" aria-label="Cookie consent">
|
|
<div class="consentos-banner__content">
|
|
${closeBtn}
|
|
${logoHtml}
|
|
<p class="consentos-banner__title">${escapeHtml(titleText)}</p>
|
|
<p class="consentos-banner__description">
|
|
${escapeHtml(descriptionText)}${privacyLink}
|
|
</p>
|
|
${cookieCount}
|
|
<div class="consentos-banner__actions">
|
|
${rejectBtn}
|
|
${manageBtn}
|
|
${acceptBtn}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
/* ── Full preview HTML (with placeholder page content, used as fallback) ── */
|
|
|
|
function buildPreviewHtml(
|
|
bc: BannerConfig,
|
|
displayMode: DisplayMode,
|
|
cornerPosition: CornerPosition,
|
|
privacyUrl: string | null,
|
|
previewLocale?: string,
|
|
): string {
|
|
const bg = bc.backgroundColour ?? '#ffffff';
|
|
const text = bc.textColour ?? '#1a1a2e';
|
|
const primary = bc.primaryColour ?? '#2563eb';
|
|
const font = bc.fontFamily ?? 'system-ui';
|
|
const radius = bc.borderRadius ?? 6;
|
|
const defaultButtonStyle = bc.buttonStyle ?? 'filled';
|
|
|
|
const positionStyles = getPositionStyles(displayMode, cornerPosition, radius);
|
|
const { rejectBtn, manageBtn, acceptBtn, closeBtn, logoHtml, cookieCount, privacyLink, titleText, descriptionText, savePreferencesText } =
|
|
buildBannerParts(bc, primary, text, radius, privacyUrl, defaultButtonStyle);
|
|
|
|
const fontLink = bc.customFontUrl
|
|
? `<link rel="stylesheet" href="${escapeHtml(bc.customFontUrl)}">`
|
|
: '';
|
|
|
|
const langAttr = previewLocale ? escapeHtml(previewLocale) : 'en';
|
|
|
|
// Build the save preferences button with accept button styling
|
|
const acceptStyle = buildButtonStyle(bc.acceptButton, defaultButtonStyle, primary, '#ffffff', 'none', radius);
|
|
const saveBtnHtml = `<button class="cmp-btn cmp-btn--primary cmp-btn--save" style="${acceptStyle}">${escapeHtml(savePreferencesText)}</button>`;
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="${langAttr}">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
${fontLink}
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
html, body {
|
|
height: 100%;
|
|
background: #f3f4f6;
|
|
font-family: ${font}, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
}
|
|
|
|
.page-content {
|
|
padding: 32px 24px;
|
|
color: #6b7280;
|
|
font-size: 13px;
|
|
line-height: 1.8;
|
|
}
|
|
.page-content h2 { color: #374151; font-size: 18px; margin-bottom: 12px; }
|
|
.page-content p { margin-bottom: 12px; }
|
|
|
|
.consentos-banner {
|
|
${positionStyles}
|
|
background: ${bg};
|
|
color: ${text};
|
|
font-family: ${font}, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.12);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
border-radius: ${displayMode === 'overlay' || displayMode === 'corner_popup' ? radius + 'px' : '0'};
|
|
}
|
|
|
|
.consentos-banner__content {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px 24px;
|
|
position: relative;
|
|
}
|
|
|
|
.cmp-logo { height: 28px; margin-bottom: 10px; display: block; }
|
|
.consentos-banner__title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
|
.consentos-banner__description { margin-bottom: 16px; opacity: 0.85; }
|
|
.consentos-banner__link { color: ${primary}; text-decoration: underline; }
|
|
.cmp-cookie-count { display: block; font-size: 12px; opacity: 0.6; margin-bottom: 12px; }
|
|
.consentos-banner__actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
|
|
.cmp-btn {
|
|
padding: 10px 20px;
|
|
border-radius: ${radius}px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: opacity 0.2s;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.cmp-close {
|
|
position: absolute; top: 12px; right: 12px;
|
|
background: none; border: none; font-size: 22px;
|
|
cursor: pointer; color: ${text}; opacity: 0.5; line-height: 1;
|
|
}
|
|
|
|
.consentos-banner__categories { display: none; margin-bottom: 16px; }
|
|
|
|
.cmp-category {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 10px 0; border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
.cmp-category__info { display: flex; flex-direction: column; flex: 1; margin-right: 12px; }
|
|
.cmp-category__name { font-weight: 500; }
|
|
.cmp-category__desc { font-size: 12px; opacity: 0.7; }
|
|
.cmp-category input[type="checkbox"] { width: 18px; height: 18px; accent-color: ${primary}; }
|
|
.cmp-btn--save { margin-top: 12px; width: 100%; }
|
|
|
|
.cmp-overlay-bg {
|
|
display: ${displayMode === 'overlay' ? 'block' : 'none'};
|
|
position: fixed; inset: 0;
|
|
background: rgba(0,0,0,0.4);
|
|
z-index: 2147483646;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.consentos-banner__actions { flex-direction: column; }
|
|
.cmp-btn { width: 100%; text-align: center; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="page-content">
|
|
<h2>Example page</h2>
|
|
<p>This is a preview of how the consent banner will appear on your site. The banner is rendered with your current theme and layout settings.</p>
|
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
|
</div>
|
|
|
|
<div class="cmp-overlay-bg"></div>
|
|
|
|
<div class="consentos-banner" role="dialog" aria-label="Cookie consent">
|
|
<div class="consentos-banner__content">
|
|
${closeBtn}
|
|
${logoHtml}
|
|
<p class="consentos-banner__title">${escapeHtml(titleText)}</p>
|
|
<p class="consentos-banner__description">
|
|
${escapeHtml(descriptionText)}${privacyLink}
|
|
</p>
|
|
${cookieCount}
|
|
<div class="consentos-banner__categories" id="cmp-prefs">
|
|
<label class="cmp-category">
|
|
<div class="cmp-category__info">
|
|
<span class="cmp-category__name">Necessary</span>
|
|
<span class="cmp-category__desc">Essential for the website to function. Always active.</span>
|
|
</div>
|
|
<input type="checkbox" checked disabled />
|
|
</label>
|
|
<label class="cmp-category">
|
|
<div class="cmp-category__info">
|
|
<span class="cmp-category__name">Functional</span>
|
|
<span class="cmp-category__desc">Enable enhanced functionality and personalisation.</span>
|
|
</div>
|
|
<input type="checkbox" />
|
|
</label>
|
|
<label class="cmp-category">
|
|
<div class="cmp-category__info">
|
|
<span class="cmp-category__name">Analytics</span>
|
|
<span class="cmp-category__desc">Help us understand how visitors interact with the site.</span>
|
|
</div>
|
|
<input type="checkbox" />
|
|
</label>
|
|
<label class="cmp-category">
|
|
<div class="cmp-category__info">
|
|
<span class="cmp-category__name">Marketing</span>
|
|
<span class="cmp-category__desc">Used to deliver personalised advertisements.</span>
|
|
</div>
|
|
<input type="checkbox" />
|
|
</label>
|
|
<label class="cmp-category">
|
|
<div class="cmp-category__info">
|
|
<span class="cmp-category__name">Personalisation</span>
|
|
<span class="cmp-category__desc">Enable content personalisation based on your profile.</span>
|
|
</div>
|
|
<input type="checkbox" />
|
|
</label>
|
|
${saveBtnHtml}
|
|
</div>
|
|
<div class="consentos-banner__actions">
|
|
${rejectBtn}
|
|
${manageBtn}
|
|
${acceptBtn}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function togglePrefs() {
|
|
var el = document.getElementById('cmp-prefs');
|
|
if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
/* ── Shared helpers ──────────────────────────────────────────────────── */
|
|
|
|
function buildButtonStyle(
|
|
config: ButtonConfig | undefined,
|
|
defaultStyle: 'filled' | 'outline',
|
|
fallbackBg: string,
|
|
fallbackText: string,
|
|
fallbackBorder: string,
|
|
radius: number,
|
|
): string {
|
|
const bg = config?.backgroundColour ?? fallbackBg;
|
|
const color = config?.textColour ?? fallbackText;
|
|
const style = config?.style ?? defaultStyle;
|
|
const border = config?.borderColour
|
|
? `1px solid ${config.borderColour}`
|
|
: style === 'outline'
|
|
? `1px solid ${config?.textColour ?? fallbackBorder}`
|
|
: style === 'text'
|
|
? 'none'
|
|
: fallbackBorder === 'none'
|
|
? 'none'
|
|
: `1px solid ${fallbackBorder}`;
|
|
const background = style === 'text' ? 'transparent' : style === 'outline' ? 'transparent' : bg;
|
|
|
|
return `background: ${background}; color: ${color}; border: ${border}; border-radius: ${radius}px;`;
|
|
}
|
|
|
|
function buildBannerParts(
|
|
bc: BannerConfig,
|
|
primary: string,
|
|
text: string,
|
|
radius: number,
|
|
privacyUrl: string | null,
|
|
defaultButtonStyle: 'filled' | 'outline',
|
|
) {
|
|
const acceptStyle = buildButtonStyle(bc.acceptButton, defaultButtonStyle, primary, '#ffffff', 'none', radius);
|
|
const rejectStyle = buildButtonStyle(bc.rejectButton, defaultButtonStyle, 'transparent', text, 'rgba(0,0,0,0.2)', radius);
|
|
const manageStyle = buildButtonStyle(bc.manageButton, defaultButtonStyle, 'transparent', text, 'rgba(0,0,0,0.2)', radius);
|
|
|
|
// Resolve text content from config or defaults
|
|
const titleText = bc.text?.title ?? DEFAULT_TITLE;
|
|
const descriptionText = bc.text?.description ?? DEFAULT_DESCRIPTION;
|
|
const acceptAllText = bc.text?.acceptAll ?? DEFAULT_ACCEPT_ALL;
|
|
const rejectAllText = bc.text?.rejectAll ?? DEFAULT_REJECT_ALL;
|
|
const managePreferencesText = bc.text?.managePreferences ?? DEFAULT_MANAGE_PREFERENCES;
|
|
const savePreferencesText = bc.text?.savePreferences ?? DEFAULT_SAVE_PREFERENCES;
|
|
|
|
const acceptBtn = `<button class="cmp-btn" style="${acceptStyle}">${escapeHtml(acceptAllText)}</button>`;
|
|
|
|
const rejectBtn = bc.showRejectAll !== false
|
|
? `<button class="cmp-btn" style="${rejectStyle}">${escapeHtml(rejectAllText)}</button>`
|
|
: '';
|
|
|
|
const manageBtn = bc.showManagePreferences !== false
|
|
? `<button class="cmp-btn" style="${manageStyle}" onclick="typeof togglePrefs==='function'&&togglePrefs()">${escapeHtml(managePreferencesText)}</button>`
|
|
: '';
|
|
|
|
const closeBtn = bc.showCloseButton
|
|
? `<button class="cmp-close" aria-label="Close">×</button>`
|
|
: '';
|
|
|
|
const logoHtml = bc.showLogo && bc.logoUrl
|
|
? `<img src="${escapeHtml(bc.logoUrl)}" alt="Logo" class="cmp-logo" />`
|
|
: '';
|
|
|
|
const cookieCount = bc.showCookieCount
|
|
? `<span class="cmp-cookie-count">12 cookies used on this site</span>`
|
|
: '';
|
|
|
|
const privacyLink = privacyUrl
|
|
? ` <a href="#" class="consentos-banner__link" onclick="return false">Privacy Policy</a>`
|
|
: '';
|
|
|
|
return { rejectBtn, manageBtn, acceptBtn, closeBtn, logoHtml, cookieCount, privacyLink, titleText, descriptionText, savePreferencesText };
|
|
}
|
|
|
|
function getPositionStyles(mode: DisplayMode, cornerPosition: CornerPosition, radius: number): string {
|
|
switch (mode) {
|
|
case 'top_banner':
|
|
return 'position: fixed; top: 0; left: 0; right: 0; z-index: 2147483647;';
|
|
case 'overlay':
|
|
return `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2147483647; width: 90%; max-width: 600px; border-radius: ${radius}px;`;
|
|
case 'corner_popup': {
|
|
const side = cornerPosition === 'left' ? 'left: 20px;' : 'right: 20px;';
|
|
return `position: fixed; bottom: 20px; ${side} z-index: 2147483647; width: 380px; max-width: calc(100% - 40px); border-radius: ${radius}px;`;
|
|
}
|
|
case 'bottom_banner':
|
|
default:
|
|
return 'position: fixed; bottom: 0; left: 0; right: 0; z-index: 2147483647;';
|
|
}
|
|
}
|
|
|
|
function escapeHtml(str: string): string {
|
|
return str
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|