Implement HeaderAdBanner component and integrate ad loading logic with consent handling
This commit is contained in:
@@ -1,90 +1,208 @@
|
||||
<template>
|
||||
<div v-if="isEnabled" class="header-ad-banner">
|
||||
<ins
|
||||
ref="adElement"
|
||||
class="adsbygoogle"
|
||||
style="display:inline-block;width:320px;height:50px"
|
||||
:data-ad-client="adClient"
|
||||
:data-ad-slot="adSlot"
|
||||
></ins>
|
||||
<div v-if="isEnabled" :class="['header-ad-banner', { sticky: finalSticky } ]">
|
||||
<div ref="adContainer" class="ad-container" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
|
||||
const adElement = ref(null);
|
||||
const adContainer = ref(null);
|
||||
|
||||
// Provider selection via env: 'adsense' or 'propeller'
|
||||
const provider = (import.meta.env.VITE_AD_PROVIDER || 'propeller').toLowerCase();
|
||||
|
||||
// AdSense config (kept for compatibility)
|
||||
const adClient = import.meta.env.VITE_ADSENSE_CLIENT || '';
|
||||
const adSlot = import.meta.env.VITE_ADSENSE_HEADER_SLOT || '';
|
||||
const isEnabled = computed(() => Boolean(adClient && adSlot));
|
||||
|
||||
function ensureAdSenseScript() {
|
||||
if (!isEnabled.value) return;
|
||||
if (document.querySelector('script[data-adsense-loader="true"]')) return;
|
||||
// Propeller config: you can set the script URL and the slot id via env
|
||||
const propScriptUrl = import.meta.env.VITE_PROP_SCRIPT_URL || '';
|
||||
const propSlotId = import.meta.env.VITE_PROP_SLOT || '';
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.async = true;
|
||||
script.src = `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${adClient}`;
|
||||
script.crossOrigin = 'anonymous';
|
||||
script.dataset.adsenseLoader = 'true';
|
||||
document.head.appendChild(script);
|
||||
// Show only if the selected provider has config
|
||||
const isEnabled = computed(() => {
|
||||
if (provider === 'adsense') return Boolean(adClient && adSlot);
|
||||
if (provider === 'propeller') return Boolean(propScriptUrl && propSlotId);
|
||||
return false;
|
||||
});
|
||||
|
||||
// Small sticky header option (thin sticky header)
|
||||
const sticky = (import.meta.env.VITE_HEADER_STICKY || 'true') === 'true';
|
||||
|
||||
// A/B test config
|
||||
const abRatio = Number(import.meta.env.VITE_AB_RATIO || '50'); // percent for variant B
|
||||
|
||||
function getStoredVariant() {
|
||||
try {
|
||||
const v = localStorage.getItem('ads_ab_variant');
|
||||
if (v === 'A' || v === 'B') return v;
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function assignVariant() {
|
||||
const existing = getStoredVariant();
|
||||
if (existing) return existing;
|
||||
const r = Math.random() * 100;
|
||||
const variant = r < abRatio ? 'B' : 'A';
|
||||
try {
|
||||
localStorage.setItem('ads_ab_variant', variant);
|
||||
} catch (e) {}
|
||||
window.dispatchEvent(new CustomEvent('ads:ab-assigned', { detail: { variant } }));
|
||||
return variant;
|
||||
}
|
||||
|
||||
const variant = getStoredVariant() || assignVariant();
|
||||
// finalSticky follows the variant: A = static, B = sticky
|
||||
const finalSticky = variant === 'B' ? true : variant === 'A' ? false : sticky;
|
||||
|
||||
function hasConsent() {
|
||||
try {
|
||||
return localStorage.getItem('ads_consent') === 'true';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function listenForConsentEvent() {
|
||||
function onConsent() {
|
||||
loadAdIfNeeded();
|
||||
window.removeEventListener('ads:consent-granted', onConsent);
|
||||
}
|
||||
window.addEventListener('ads:consent-granted', onConsent);
|
||||
}
|
||||
|
||||
function ensureScriptLoaded(src, attrName) {
|
||||
if (document.querySelector(`script[data-ad-loader="${attrName}"]`)) return Promise.resolve();
|
||||
return new Promise((resolve, reject) => {
|
||||
const s = document.createElement('script');
|
||||
s.async = true;
|
||||
s.src = src;
|
||||
s.dataset.adLoader = attrName;
|
||||
s.onload = () => resolve();
|
||||
s.onerror = (e) => reject(e);
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
async function renderPropeller() {
|
||||
if (!adContainer.value) return;
|
||||
try {
|
||||
await ensureScriptLoaded(propScriptUrl, 'propeller');
|
||||
// Provider-specific insertion: Propeller usually exposes a global builder or expects a div with an id.
|
||||
// We create a lightweight iframe fallback that publishers can map on Propeller dashboard by slot id.
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.width = '100%';
|
||||
iframe.height = '90';
|
||||
iframe.frameBorder = '0';
|
||||
iframe.scrolling = 'no';
|
||||
iframe.style.border = '0';
|
||||
iframe.title = 'Advertisement';
|
||||
// Minimal message to the ad server; the exact ad markup is controlled via the external propeller script/endpoint.
|
||||
iframe.src = `${propScriptUrl}?slot=${encodeURIComponent(propSlotId)}&responsive=1`;
|
||||
adContainer.value.innerHTML = '';
|
||||
adContainer.value.appendChild(iframe);
|
||||
} catch (err) {
|
||||
console.warn('Propeller ad load failed', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function renderAdSense() {
|
||||
if (!adContainer.value) return;
|
||||
try {
|
||||
await ensureScriptLoaded(`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${adClient}`, 'adsense');
|
||||
const ins = document.createElement('ins');
|
||||
ins.className = 'adsbygoogle';
|
||||
ins.style.display = 'block';
|
||||
ins.style.width = '100%';
|
||||
ins.style.height = '50px';
|
||||
ins.setAttribute('data-ad-client', adClient);
|
||||
ins.setAttribute('data-ad-slot', adSlot);
|
||||
adContainer.value.innerHTML = '';
|
||||
adContainer.value.appendChild(ins);
|
||||
(window.adsbygoogle = window.adsbygoogle || []).push({});
|
||||
} catch (err) {
|
||||
console.warn('AdSense load failed', err);
|
||||
}
|
||||
}
|
||||
|
||||
let adLoaded = false;
|
||||
async function loadAdIfNeeded() {
|
||||
if (adLoaded || !isEnabled.value) return;
|
||||
adLoaded = true;
|
||||
try {
|
||||
if (adContainer.value) adContainer.value.dataset.abVariant = variant;
|
||||
window.dispatchEvent(new CustomEvent('ads:load-start', { detail: { provider, variant } }));
|
||||
} catch (e) {}
|
||||
if (provider === 'propeller') await renderPropeller();
|
||||
else if (provider === 'adsense') await renderAdSense();
|
||||
try { window.dispatchEvent(new CustomEvent('ads:loaded', { detail: { provider, variant } })); } catch (e) {}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!isEnabled.value) return;
|
||||
|
||||
ensureAdSenseScript();
|
||||
await nextTick();
|
||||
|
||||
try {
|
||||
// Avoid duplicate initialization on remount.
|
||||
if (adElement.value?.dataset.adsInitialized === 'true') return;
|
||||
(window.adsbygoogle = window.adsbygoogle || []).push({});
|
||||
if (adElement.value) {
|
||||
adElement.value.dataset.adsInitialized = 'true';
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('AdSense Banner konnte nicht initialisiert werden:', error);
|
||||
// If consent present, load immediately (but still lazy after nextTick to avoid blocking)
|
||||
if (hasConsent()) {
|
||||
await nextTick();
|
||||
await loadAdIfNeeded();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, wait for either a user interaction or consent event
|
||||
const onFirstInteraction = async () => {
|
||||
window.removeEventListener('pointerdown', onFirstInteraction);
|
||||
window.removeEventListener('scroll', onFirstInteraction);
|
||||
await loadAdIfNeeded();
|
||||
};
|
||||
|
||||
window.addEventListener('pointerdown', onFirstInteraction, { once: true });
|
||||
window.addEventListener('scroll', onFirstInteraction, { once: true });
|
||||
listenForConsentEvent();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-ad-banner {
|
||||
flex: 0 0 auto;
|
||||
width: 320px;
|
||||
min-width: 320px;
|
||||
max-width: 320px;
|
||||
height: 50px;
|
||||
margin: 0 16px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-height: 90px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header-ad-banner.sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 600;
|
||||
background: rgba(255,255,255,0.98);
|
||||
backdrop-filter: blur(4px);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.ad-container {
|
||||
width: 100%;
|
||||
max-width: 980px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-ad-banner :deep(ins) {
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.header-ad-banner {
|
||||
width: 300px;
|
||||
min-width: 300px;
|
||||
max-width: 300px;
|
||||
height: 50px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.header-ad-banner :deep(ins) {
|
||||
width: 300px !important;
|
||||
height: 50px !important;
|
||||
}
|
||||
.ad-container iframe,
|
||||
.ad-container ins {
|
||||
width: 100%;
|
||||
max-width: 970px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.header-ad-banner {
|
||||
display: none;
|
||||
.ad-container iframe,
|
||||
.ad-container ins {
|
||||
height: 50px;
|
||||
max-width: 320px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HeaderAdBanner v-if="chatStore.currentConversation" />
|
||||
<ChatWindow />
|
||||
</div>
|
||||
<ChatInput />
|
||||
|
||||
Reference in New Issue
Block a user