Files
singlechat/client/src/components/HeaderAdBanner.vue

391 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<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 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 || '';
// Propeller config: you can set the script URL and the slot id via env
// Defaults set to the in-page push tag you provided (can be overridden via env)
const propScriptUrl = import.meta.env.VITE_PROP_SCRIPT_URL || 'https://nap5k.com/tag.min.js';
const propSlotId = import.meta.env.VITE_PROP_SLOT || '11023637';
// 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';
// Debug bypass: allow forcing ad load with ?ads_debug=1
const debugForceLoad = typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('ads_debug') === '1';
// Auto load flag (env) - set to 'true' to load ads on mount without waiting for interaction
const autoLoad = (import.meta.env.VITE_PROP_AUTOLOAD || '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 {
// Insert Propeller in-page push tag exactly as provided by PropellerAds.
// Avoid duplicate insertion if same script+zone already present.
if (document.querySelector(`script[src="${propScriptUrl}"][data-zone="${propSlotId}"]`)) {
return;
}
// create placeholder first and insert into ad container
const placeholder = document.createElement('div');
placeholder.className = 'propeller-ad-placeholder';
adContainer.value.appendChild(placeholder);
const s = document.createElement('script');
s.dataset.zone = propSlotId;
s.src = propScriptUrl;
s.async = true;
s.onload = () => {
console.log('Propeller script loaded:', propScriptUrl, 'zone:', propSlotId);
};
s.onerror = (e) => {
console.warn('Propeller script failed to load:', propScriptUrl, e);
try { window.dispatchEvent(new CustomEvent('ads:load-failed', { detail: { provider: 'propeller', zone: propSlotId, error: String(e) } })); } catch {}
};
// Append the script directly into the placeholder so the provider injects into it
placeholder.appendChild(s);
// Observe the document for injected ad nodes and relocate them into our placeholder
const observeAndRelocate = () => {
const parent = document.body;
try {
const observer = new MutationObserver(mutations => {
for (const m of mutations) {
for (const node of Array.from(m.addedNodes)) {
try {
// Handle text nodes that may contain raw script/HTML
if (node.nodeType === Node.TEXT_NODE) {
const txt = (node.nodeValue || '').trim();
if (txt.includes('<script') || txt.length > 500) {
// remove large/raw script text nodes to avoid visible JS
node.parentNode && node.parentNode.removeChild(node);
}
continue;
}
if (!(node instanceof HTMLElement)) continue;
// If element contains script tags, extract and execute them safely
const innerScripts = Array.from(node.querySelectorAll('script'));
if (innerScripts.length) {
for (const sc of innerScripts) {
try {
if (sc.src) {
const s2 = document.createElement('script');
s2.src = sc.src;
s2.async = true;
document.head.appendChild(s2);
} else if (sc.textContent) {
const s2 = document.createElement('script');
s2.textContent = sc.textContent;
document.head.appendChild(s2);
}
} catch (e) {
console.warn('execute inner script failed', e);
}
// remove the inline script node from original element to avoid visible code
sc.parentNode && sc.parentNode.removeChild(sc);
}
}
// If the added node is a script element itself, execute safely via head insertion
if (node.tagName === 'SCRIPT') {
const src = node.getAttribute('src');
if (src) {
const s2 = document.createElement('script');
s2.src = src;
s2.async = true;
s2.onload = () => console.log('relocated script loaded', src);
s2.onerror = (e) => console.warn('relocated script failed', src, e);
document.head.appendChild(s2);
node.parentNode && node.parentNode.removeChild(node);
continue;
} else if (node.textContent) {
const s2 = document.createElement('script');
s2.textContent = node.textContent;
document.head.appendChild(s2);
node.parentNode && node.parentNode.removeChild(node);
continue;
}
}
// Detect iframe ad nodes or nodes with nap5k indicators
const iframe = node.tagName === 'IFRAME' ? node : node.querySelector && node.querySelector('iframe');
const src = iframe ? (iframe.getAttribute('src') || '') : '';
const isAdNode = src.includes('nap5k') || (node.dataset && node.dataset.zone === propSlotId) || /nap5k|propel|propeller|inpage|push/i.test((node.className || '') + ' ' + (node.id || ''));
if (isAdNode) {
try {
// move iframe/ad node into placeholder
placeholder.innerHTML = '';
if (iframe && iframe.parentNode) {
placeholder.appendChild(iframe);
} else {
placeholder.appendChild(node);
}
const moved = placeholder.firstElementChild;
if (moved) {
// Normalize size
moved.style.maxWidth = '100%';
moved.style.display = 'block';
// If provider inserted a fixed/absolute positioned element (overlays),
// force it to behave as inline block inside our header placeholder.
try {
const cs = getComputedStyle(moved);
if (cs.position === 'fixed' || cs.position === 'absolute') {
moved.style.setProperty('position', 'relative', 'important');
moved.style.setProperty('top', 'auto', 'important');
moved.style.setProperty('right', 'auto', 'important');
moved.style.setProperty('left', 'auto', 'important');
moved.style.setProperty('bottom', 'auto', 'important');
moved.style.setProperty('z-index', '1', 'important');
moved.style.setProperty('width', '100%', 'important');
moved.style.setProperty('max-width', '970px', 'important');
}
if (moved.tagName === 'IFRAME') {
moved.style.width = '100%';
moved.style.height = moved.style.height || '60px';
}
} catch (e) {
console.warn('style normalize failed', e);
}
}
observer.disconnect();
try { window.dispatchEvent(new CustomEvent('ads:relocated',{detail:{provider:'propeller',zone:propSlotId}})); } catch {}
return;
} catch (e) {
console.warn('Failed to relocate ad node', e);
}
}
} catch (e) {
console.warn('mutation handler error', e);
}
}
}
});
observer.observe(parent, { childList: true, subtree: true });
// Check existing iframes right away
const existing = Array.from(document.querySelectorAll('iframe')).find(f => (f.src || '').includes('nap5k'));
if (existing) {
placeholder.innerHTML = '';
placeholder.appendChild(existing);
observer.disconnect();
}
// Failsafe: stop observing after 6s
setTimeout(() => observer.disconnect(), 6000);
} catch (e) {
console.warn('observeAndRelocate error', e);
}
};
// Start observing shortly after script insertion to allow provider to run
setTimeout(observeAndRelocate, 100);
// Fallback: wenn nach ScriptLoad innerhalb 3s kein Ad im Placeholder erscheint,
// lade ein iframeBackup. Das erhöht die Chance, ein sichtbares Banner im Header zu zeigen.
setTimeout(() => {
try {
const ph = adContainer.value.querySelector('.propeller-ad-placeholder');
const hasChild = ph && ph.children && ph.children.length > 0;
const height = ph ? parseInt(getComputedStyle(ph).height || '0', 10) : 0;
if (!hasChild && (!height || height === 0)) {
console.log('Ad placeholder empty — inserting iframe fallback');
const iframe = document.createElement('iframe');
iframe.width = '100%';
iframe.height = '90';
iframe.frameBorder = '0';
iframe.scrolling = 'no';
iframe.style.border = '0';
iframe.title = 'Advertisement';
// Fallback URL: attempt to call tag URL with slot param — may be accepted by provider
iframe.src = `${propScriptUrl}?slot=${encodeURIComponent(propSlotId)}&responsive=1`;
ph.innerHTML = '';
ph.appendChild(iframe);
try { window.dispatchEvent(new CustomEvent('ads:fallback-inserted',{detail:{provider:'propeller',zone:propSlotId}})); } catch {}
}
} catch (e) {
console.warn('Fallback insertion failed', e);
}
}, 3000);
} 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;
// If consent present, autoLoad, or debugForceLoad => load immediately
if (hasConsent() || autoLoad || debugForceLoad) {
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 {
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;
}
.ad-container iframe,
.ad-container ins {
width: 100%;
max-width: 970px;
height: 60px;
}
@media (max-width: 720px) {
.ad-container iframe,
.ad-container ins {
height: 50px;
max-width: 320px;
}
}
</style>