/**
* BadProjekt-Pass Widget
* WordPress Integration
*/
(function() {
'use strict';
// Konfiguration aus data-Attributen
const script = document.currentScript || document.querySelector('script[data-tenant]');
const config = {
tenantId: script?.dataset.tenant || '',
locale: script?.dataset.locale || 'de-DE',
appBaseUrl: script?.dataset.appBaseUrl || window.location.origin,
apiUrl: script?.dataset.apiUrl || window.location.origin + '/api'
};
// State
let projectToken = localStorage.getItem('bp_project_token');
let projectCode = localStorage.getItem('bp_project_code');
let projectId = localStorage.getItem('bp_project_id');
// Session Tracking
let sessionId = sessionStorage.getItem('bp_session_id') || generateSessionId();
if (!sessionStorage.getItem('bp_session_id')) {
sessionStorage.setItem('bp_session_id', sessionId);
}
// Scroll-Tracking
let maxScrollDepth = 0;
let pageStartTime = Date.now();
/**
* API Client
*/
const api = {
async request(url, options = {}) {
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (projectToken) {
headers['Authorization'] = `Bearer ${projectToken}`;
}
const fullUrl = config.apiUrl + url;
console.log('[API] Request:', {
method: options.method || 'GET',
url: fullUrl,
hasToken: !!projectToken,
body: options.body ? JSON.parse(options.body) : null
});
try {
const response = await fetch(fullUrl, {
...options,
headers
});
console.log('[API] Response Status:', response.status, response.statusText);
// Prüfe ob Token erneuert wurde (aus Header)
const renewedTokenHeader = response.headers.get('X-Token-Renewed');
if (renewedTokenHeader) {
console.log('[API] Token wurde erneuert, aktualisiere...');
projectToken = renewedTokenHeader;
localStorage.setItem('bp_project_token', projectToken);
console.log('[API] Token aktualisiert');
}
if (!response.ok) {
// Versuche Error-Body zu lesen
let errorBody = null;
try {
errorBody = await response.text();
console.error('[API] Error Response Body:', errorBody);
} catch (e) {
// Ignoriere Fehler beim Lesen des Error-Body
}
throw new Error(`HTTP ${response.status}: ${errorBody || response.statusText}`);
}
const jsonData = await response.json();
console.log('[API] Response Data:', jsonData);
// Prüfe ob Token erneuert wurde (aus Body - Fallback)
if (jsonData.data && jsonData.data.token) {
console.log('[API] Token wurde erneuert (aus Body), aktualisiere...');
projectToken = jsonData.data.token;
localStorage.setItem('bp_project_token', projectToken);
console.log('[API] Token aktualisiert');
}
return jsonData;
} catch (error) {
console.error('[API] Request Error:', error);
console.error('[API] Error Details:', {
message: error.message,
stack: error.stack,
name: error.name
});
throw error;
}
},
async createProject(data) {
return this.request('/public/projects', {
method: 'POST',
body: JSON.stringify(data)
});
},
async restoreProject(identifier, password, tenantId) {
return this.request('/public/projects/restore', {
method: 'POST',
body: JSON.stringify({
identifier: identifier,
password: password,
tenant_id: tenantId
})
});
},
async requestPasswordReset(identifier, tenantId) {
return this.request('/public/password/reset-request', {
method: 'POST',
body: JSON.stringify({
identifier: identifier,
tenant_id: tenantId
})
});
},
async resetPassword(resetToken, newPassword) {
return this.request('/public/password/reset', {
method: 'POST',
body: JSON.stringify({
reset_token: resetToken,
password: newPassword
})
});
},
async addProduct(projectId, productData) {
return this.request(`/projects/${projectId}/products`, {
method: 'POST',
body: JSON.stringify(productData)
});
},
async toggleMerkliste(projectId, merklisteData) {
return this.request(`/projects/${projectId}/merkliste`, {
method: 'POST',
body: JSON.stringify(merklisteData)
});
},
async saveConfiguration(projectId, configData) {
return this.request(`/projects/${projectId}/configurations`, {
method: 'POST',
body: JSON.stringify(configData)
});
},
async trackDownload(projectId, downloadData) {
return this.request(`/projects/${projectId}/downloads`, {
method: 'POST',
body: JSON.stringify(downloadData)
});
},
async checkMerklisteStatus(projectId, externalId, kategorie) {
return this.request(`/projects/${projectId}/merkliste/check?external_id=${encodeURIComponent(externalId)}&kategorie=${encodeURIComponent(kategorie)}`, {
method: 'GET'
});
}
};
/**
* Toast-System für Fehlermeldungen
*/
function showToast(message, type = 'error') {
const toast = document.createElement('div');
toast.className = `bp-toast bp-toast-${type}`;
toast.textContent = message;
// Styles
Object.assign(toast.style, {
position: 'fixed',
bottom: '20px',
right: '20px',
background: type === 'error' ? '#f44336' : '#4CAF50',
color: 'white',
padding: '12px 20px',
borderRadius: '4px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
zIndex: '10000',
fontSize: '14px',
maxWidth: '300px',
animation: 'slideInRight 0.3s ease-out'
});
// Animation CSS hinzufügen (falls noch nicht vorhanden)
if (!document.getElementById('bp-toast-styles')) {
const style = document.createElement('style');
style.id = 'bp-toast-styles';
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
document.body.appendChild(toast);
// Nach 3 Sekunden entfernen
setTimeout(() => {
toast.style.animation = 'slideOutRight 0.3s ease-out';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 3000);
}
/**
* UTM Parameter auslesen
*/
function getUtmParams() {
const params = new URLSearchParams(window.location.search);
return {
utm_source: params.get('utm_source'),
utm_medium: params.get('utm_medium'),
utm_campaign: params.get('utm_campaign'),
utm_content: params.get('utm_content'),
utm_term: params.get('utm_term')
};
}
/**
* Generiert Session-ID
*/
function generateSessionId() {
return 'bp_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
/**
* Ermittelt Herkunft (Referrer oder Direktaufruf)
*/
function getSource() {
const referrer = document.referrer;
if (!referrer || referrer === '' || referrer === window.location.href) {
return 'Direkt Aufruf - Webseite';
}
try {
const referrerUrl = new URL(referrer);
const currentUrl = new URL(window.location.href);
// Wenn Referrer von derselben Domain kommt, als "Interne Navigation" markieren
if (referrerUrl.hostname === currentUrl.hostname) {
return 'Interne Navigation';
}
// Externe Referrer zurückgeben
return referrer;
} catch (e) {
// Falls URL-Parsing fehlschlägt, Referrer trotzdem zurückgeben
return referrer;
}
}
/**
* Trackt Seitenbesuch und Scrolltiefe
*/
function trackPageVisit() {
// Reset für neue Seite
maxScrollDepth = 0;
pageStartTime = Date.now();
if (!projectId) {
return;
}
// Scrolltiefe tracken
function updateScrollDepth() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const documentHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollPercent = documentHeight > 0 ? Math.round((scrollTop / documentHeight) * 100) : 0;
if (scrollPercent > maxScrollDepth) {
maxScrollDepth = scrollPercent;
}
}
// Scroll-Event-Listener
let scrollTimeout;
window.addEventListener('scroll', () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(updateScrollDepth, 100);
}, { passive: true });
// Initiale Scrolltiefe
updateScrollDepth();
// Beim Verlassen der Seite: Daten senden
function sendSessionData() {
if (!projectId) {
return;
}
const timeOnPage = Math.round((Date.now() - pageStartTime) / 1000);
// Sende Tracking-Daten (auch wenn scroll_depth 0 ist)
const data = JSON.stringify({
session_id: sessionId,
page_url: window.location.href,
page_title: document.title,
scroll_depth: maxScrollDepth,
time_on_page: timeOnPage
});
// Verwende fetch mit keepalive (sendBeacon unterstützt keine Authorization Header)
fetch(config.apiUrl + `/projects/${projectId}/session-tracking`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': projectToken ? `Bearer ${projectToken}` : ''
},
body: data,
keepalive: true // Wichtig für zuverlässiges Tracking beim Seitenwechsel
}).catch(() => {}); // Fehler ignorieren
}
// Beim Verlassen der Seite
window.addEventListener('beforeunload', sendSessionData);
// Auch beim Seitenwechsel (falls möglich)
window.addEventListener('pagehide', sendSessionData);
}
/**
* Popup erstellen
*/
function createPopup() {
// Prüfe ob Popup nicht angezeigt werden soll (permanent)
if (localStorage.getItem('bp_popup_dont_show') === 'true') {
return null;
}
// Prüfe ob Popup bereits in dieser Session gezeigt wurde
if (sessionStorage.getItem('bp_popup_shown') === 'true') {
return null;
}
const popup = document.createElement('div');
popup.id = 'bp-popup';
popup.className = 'bp-popup';
popup.innerHTML = `
`;
document.body.appendChild(popup);
// Tab-Switching
const tabButtons = popup.querySelectorAll('.bp-tab-btn');
const identifierInput = popup.querySelector('#bp-identifier');
const formLabel = popup.querySelector('.bp-form-label');
tabButtons.forEach(btn => {
btn.addEventListener('click', () => {
tabButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const tab = btn.dataset.tab;
if (tab === 'whatsapp') {
identifierInput.type = 'tel';
identifierInput.placeholder = 'z. B. +49 123 456789';
formLabel.textContent = 'WhatsApp-Nummer';
} else {
identifierInput.type = 'text';
identifierInput.placeholder = 'z. B. max.mustermann@mail.de';
formLabel.textContent = 'E-Mail-Adresse';
}
});
});
// Close Button - Markiere Popup als gezeigt
const closePopup = () => {
sessionStorage.setItem('bp_popup_shown', 'true');
popup.remove();
};
popup.querySelector('.bp-popup-close').addEventListener('click', closePopup);
popup.querySelector('.bp-popup-overlay').addEventListener('click', closePopup);
// "Nicht wieder anzeigen" Button
popup.querySelector('#bp-dont-show-btn').addEventListener('click', () => {
localStorage.setItem('bp_popup_dont_show', 'true');
popup.remove();
});
// Wiederherstellungs-Button
const restoreBtn = popup.querySelector('#bp-restore-btn');
const restoreInput = popup.querySelector('#bp-restore-identifier');
const restoreGroup = popup.querySelector('.bp-restore-group');
// Zeige Wiederherstellungs-Feld wenn "Bereits eine ID vorhanden?" Link geklickt wird
const showRestoreLink = document.createElement('a');
showRestoreLink.href = '#';
showRestoreLink.className = 'bp-show-restore';
showRestoreLink.textContent = 'Bereits eine ID vorhanden?';
showRestoreLink.style.cssText = 'display: block; margin-top: 10px; color: #666; text-decoration: underline; font-size: 14px;';
showRestoreLink.addEventListener('click', (e) => {
e.preventDefault();
restoreGroup.style.display = 'block';
showRestoreLink.style.display = 'none';
// Setze required nur wenn Feld sichtbar ist
const restorePasswordInput = popup.querySelector('#bp-restore-password');
if (restorePasswordInput) {
restorePasswordInput.setAttribute('required', 'required');
}
});
popup.querySelector('.bp-form-group').appendChild(showRestoreLink);
// Passwort vergessen Link
const forgotPasswordLink = popup.querySelector('#bp-forgot-password-link');
if (forgotPasswordLink) {
forgotPasswordLink.addEventListener('click', (e) => {
e.preventDefault();
showPasswordResetDialog(popup);
});
}
restoreBtn.addEventListener('click', async () => {
const identifier = restoreInput.value.trim();
const password = popup.querySelector('#bp-restore-password').value;
if (!identifier) {
showToast('Bitte geben Sie einen Projekt-Code, E-Mail oder Telefonnummer ein', 'error');
return;
}
if (!password) {
showToast('Bitte geben Sie Ihr Passwort ein', 'error');
return;
}
try {
const result = await api.restoreProject(identifier, password, config.tenantId);
if (result.success && result.data) {
projectToken = result.data.token;
projectCode = result.data.project_code;
projectId = result.data.project.id;
localStorage.setItem('bp_project_token', projectToken);
localStorage.setItem('bp_project_code', projectCode);
localStorage.setItem('bp_project_id', projectId);
sessionStorage.setItem('bp_popup_shown', 'true');
popup.remove();
showToast('Projekt erfolgreich wiederhergestellt!', 'success');
// Mache Buttons sichtbar
document.querySelectorAll('.bp-add-to-merkliste, [data-bp-merkliste-id], .bp-add-to-project, [data-bp-product-id], .bp-download-link, [data-bp-download-id]').forEach(element => {
element.classList.remove('hidden');
});
// Initialisiere Buttons erneut
initMerklisteButtons();
initProductButtons();
initDownloadLinks();
// Starte Session-Tracking
trackPageVisit();
}
} catch (error) {
showToast(error.message || 'Projekt nicht gefunden. Bitte prüfen Sie Ihre Eingabe.', 'error');
}
});
// Form Submit
popup.querySelector('#bp-popup-form').addEventListener('submit', async (e) => {
e.preventDefault();
// Entferne required vom restore_password Feld, da es nicht Teil des Hauptformulars ist
const restorePasswordInput = popup.querySelector('#bp-restore-password');
if (restorePasswordInput) {
restorePasswordInput.removeAttribute('required');
}
const formData = new FormData(e.target);
const identifier = formData.get('identifier');
const password = formData.get('password');
const consent = formData.get('consent_tracking') === '1';
const consentNewsletter = formData.get('consent_newsletter') === '1';
// Passwort-Validierung
if (!password || password.length < 8) {
showToast('Passwort muss mindestens 8 Zeichen lang sein', 'error');
return;
}
const activeTab = popup.querySelector('.bp-tab-btn.active').dataset.tab;
const isEmail = activeTab === 'email' || identifier.includes('@');
const projectData = {
tenant_id: config.tenantId,
password: password, // Passwort hinzufügen
consent_tracking: consent ? 1 : 0,
consent_newsletter: consentNewsletter ? 1 : 0,
source: getSource()
};
if (isEmail) {
projectData.email = identifier;
} else {
projectData.phone = identifier;
}
if (consent) {
Object.assign(projectData, getUtmParams());
}
try {
const result = await api.createProject(projectData);
if (result.success && result.data) {
projectToken = result.data.token;
projectCode = result.data.project_code;
projectId = result.data.project.id;
localStorage.setItem('bp_project_token', projectToken);
localStorage.setItem('bp_project_code', projectCode);
localStorage.setItem('bp_project_id', projectId);
sessionStorage.setItem('bp_popup_shown', 'true');
popup.remove();
showToast('Projektpass erfolgreich erstellt!', 'success');
// Mache Buttons sichtbar nach Projekt-Erstellung
document.querySelectorAll('.bp-add-to-merkliste, [data-bp-merkliste-id], .bp-add-to-project, [data-bp-product-id], .bp-download-link, [data-bp-download-id]').forEach(element => {
element.classList.remove('hidden');
});
// Initialisiere Buttons erneut mit neuem Projekt
initMerklisteButtons();
initProductButtons();
initDownloadLinks();
// Starte Session-Tracking für neues Projekt
trackPageVisit();
}
} catch (error) {
showToast('Fehler beim Erstellen des Projektpasses', 'error');
}
});
return popup;
}
/**
* Zeigt Passwort-Reset-Dialog
*/
function showPasswordResetDialog(parentPopup) {
const dialog = document.createElement('div');
dialog.className = 'bp-popup';
dialog.innerHTML = `
`;
document.body.appendChild(dialog);
// Close Button
dialog.querySelector('.bp-popup-close').addEventListener('click', () => {
dialog.remove();
});
dialog.querySelector('.bp-popup-overlay').addEventListener('click', () => {
dialog.remove();
});
// Reset-Request Form
dialog.querySelector('#bp-reset-request-form').addEventListener('submit', async (e) => {
e.preventDefault();
const identifier = dialog.querySelector('#bp-reset-identifier').value.trim();
if (!identifier) {
showToast('Bitte geben Sie eine E-Mail-Adresse oder Telefonnummer ein', 'error');
return;
}
try {
const result = await api.requestPasswordReset(identifier, config.tenantId);
if (result.success) {
// Zeige Token-Eingabe (für Entwicklung - in Produktion sollte Token per E-Mail/SMS kommen)
if (result.data && result.data.reset_token) {
dialog.querySelector('#bp-reset-token').value = result.data.reset_token;
dialog.querySelector('#bp-reset-token-section').style.display = 'block';
showToast('Reset-Link wurde gesendet. Bitte prüfen Sie Ihre E-Mail/SMS.', 'success');
} else {
showToast('Reset-Link wurde gesendet. Bitte prüfen Sie Ihre E-Mail/SMS.', 'success');
}
}
} catch (error) {
showToast(error.message || 'Fehler beim Anfordern des Reset-Links', 'error');
}
});
// Reset Password Button
dialog.querySelector('#bp-reset-password-btn').addEventListener('click', async () => {
const resetToken = dialog.querySelector('#bp-reset-token').value.trim();
const newPassword = dialog.querySelector('#bp-new-password').value;
if (!resetToken) {
showToast('Bitte geben Sie den Reset-Token ein', 'error');
return;
}
if (!newPassword || newPassword.length < 8) {
showToast('Passwort muss mindestens 8 Zeichen lang sein', 'error');
return;
}
try {
const result = await api.resetPassword(resetToken, newPassword);
if (result.success) {
showToast('Passwort erfolgreich zurückgesetzt!', 'success');
dialog.remove();
if (parentPopup) {
parentPopup.remove();
}
}
} catch (error) {
showToast(error.message || 'Fehler beim Zurücksetzen des Passworts', 'error');
}
});
}
/**
* Erfolgsmeldung
*/
function showSuccessMessage() {
const message = document.createElement('div');
message.className = 'bp-success-message';
message.textContent = 'Projektpass erstellt!';
document.body.appendChild(message);
setTimeout(() => message.remove(), 3000);
}
/**
* "Mein Projekt" Button
*/
function initProjectButton() {
const buttons = document.querySelectorAll('[id="bp-pass-entry-button"], [data-bp-entry="true"]');
buttons.forEach(button => {
button.addEventListener('click', () => {
if (projectCode) {
window.open(`${config.appBaseUrl}/p/${projectCode}`, '_blank');
} else {
createPopup();
}
});
});
}
/**
* Prüft Button-Status und aktualisiert visuell (für Merkliste-Buttons)
*/
async function updateMerklisteButtonStatus(button) {
if (!projectId) {
return;
}
const externalId = button.dataset.bpMerklisteId || button.dataset.bpProductId || '';
const kategorie = button.dataset.bpMerklisteKategorie || button.dataset.bpMerklisteKategorie || 'Produkt';
if (!externalId) {
console.warn('updateMerklisteButtonStatus: externalId fehlt', button);
return;
}
try {
const status = await api.checkMerklisteStatus(projectId, externalId, kategorie);
if (status && status.success && status.data?.in_merkliste) {
button.classList.add('merkliste-true');
button.textContent = button.dataset.bpMerklisteTextAdded || button.dataset.bpProductTextAdded || '✓ In Merkliste';
} else {
button.classList.remove('merkliste-true');
button.textContent = button.dataset.bpMerklisteTextAdd || button.dataset.bpProductTextAdd || 'Zur Merkliste';
}
} catch (error) {
console.warn('updateMerklisteButtonStatus Fehler:', error);
// Bei Fehler: Status basierend auf aktueller Klasse setzen (Fallback)
if (!button.classList.contains('merkliste-true')) {
button.classList.remove('merkliste-true');
button.textContent = button.dataset.bpMerklisteTextAdd || button.dataset.bpProductTextAdd || 'Zur Merkliste';
}
}
}
/**
* Merkliste-Buttons (neue allgemeine Funktion mit Toggle)
*/
function initMerklisteButtons() {
const buttons = document.querySelectorAll('.bp-add-to-merkliste, [data-bp-merkliste-id]');
console.log('[embed.js] initMerklisteButtons: Gefunden', buttons.length, 'Buttons');
buttons.forEach((button, index) => {
console.log(`[embed.js] Initialisiere Button ${index + 1}:`, {
externalId: button.dataset.bpMerklisteId || button.dataset.bpProductId,
kategorie: button.dataset.bpMerklisteKategorie || 'Produkt',
hasMerklisteTrue: button.classList.contains('merkliste-true')
});
// Entferne alte Event-Listener (verhindere Duplikate)
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
// Entferne "hidden" Klasse nur wenn Projekt vorhanden
if (projectId) {
newButton.classList.remove('hidden');
// Prüfe initialen Status
updateMerklisteButtonStatus(newButton);
}
newButton.addEventListener('click', async (e) => {
console.log('[embed.js] Merkliste-Button geklickt!', {
externalId: newButton.dataset.bpMerklisteId,
kategorie: newButton.dataset.bpMerklisteKategorie,
projectId: projectId
});
e.preventDefault();
e.stopPropagation();
if (!projectId) {
console.log('[embed.js] Kein projectId - öffne Popup');
createPopup();
return;
}
const kategorie = newButton.dataset.bpMerklisteKategorie || 'Produkt';
if (!kategorie) {
showToast('Kategorie fehlt! Bitte data-bp-merkliste-kategorie setzen.', 'error');
return;
}
const externalId = newButton.dataset.bpMerklisteId || '';
if (!externalId) {
showToast('ID fehlt! Bitte data-bp-merkliste-id setzen.', 'error');
return;
}
// Prüfe aktuellen Status VOR dem Toggle (optimistisches Update)
const isCurrentlyInMerkliste = newButton.classList.contains('merkliste-true');
// Optimistisches Update (sofort visuell ändern)
if (isCurrentlyInMerkliste) {
newButton.classList.remove('merkliste-true');
newButton.textContent = newButton.dataset.bpMerklisteTextAdd || 'Zur Merkliste';
} else {
newButton.classList.add('merkliste-true');
newButton.textContent = newButton.dataset.bpMerklisteTextAdded || '✓ In Merkliste';
}
// Deaktiviere Button während Request
newButton.disabled = true;
const merklisteData = {
external_id: externalId,
titel: newButton.dataset.bpMerklisteTitel || newButton.dataset.bpMerklisteName || '',
kategorie: kategorie,
bild: newButton.dataset.bpMerklisteBild || newButton.dataset.bpMerklisteImage || '',
marke: newButton.dataset.bpMerklisteMarke || newButton.dataset.bpMerklisteBrand || null,
serie: newButton.dataset.bpMerklisteSerie || newButton.dataset.bpMerklisteSeries || null,
sku: newButton.dataset.bpMerklisteSku || null,
source_url: window.location.href,
source_system: 'wordpress'
};
try {
console.log('[Merkliste] Toggle Start:', {
externalId,
kategorie,
isCurrentlyInMerkliste,
projectId,
merklisteData
});
const result = await api.toggleMerkliste(projectId, merklisteData);
console.log('[Merkliste] Toggle Response:', result);
console.log('[Merkliste] Response Structure:', {
success: result?.success,
data: result?.data,
action: result?.data?.action,
message: result?.message
});
if (!result) {
throw new Error('Keine Response vom Server erhalten');
}
if (!result.success) {
throw new Error(result.message || result.error || 'Fehler beim Toggle');
}
if (!result.data) {
console.warn('[Merkliste] Response hat kein data-Objekt:', result);
throw new Error('Ungültige Response-Struktur');
}
// Korrigiere Status basierend auf Response
const action = result.data.action;
console.log('[Merkliste] Action:', action);
if (action === 'removed') {
newButton.classList.remove('merkliste-true');
newButton.textContent = newButton.dataset.bpMerklisteTextAdd || 'Zur Merkliste';
showToast('Aus Merkliste entfernt', 'success');
console.log('[Merkliste] Button-Status auf "entfernt" gesetzt');
} else if (action === 'added' || action === 'already_exists') {
newButton.classList.add('merkliste-true');
newButton.textContent = newButton.dataset.bpMerklisteTextAdded || '✓ In Merkliste';
if (action === 'already_exists') {
showToast('Bereits in Merkliste', 'info');
} else {
showToast('Zur Merkliste hinzugefügt', 'success');
}
console.log('[Merkliste] Button-Status auf "hinzugefügt" gesetzt');
} else {
console.warn('[Merkliste] Unbekannte Action:', action);
// Fallback: Prüfe Status vom Server
setTimeout(() => {
updateMerklisteButtonStatus(newButton);
}, 500);
}
// Prüfe Status nochmal (sicherstellen)
setTimeout(() => {
console.log('[Merkliste] Prüfe Button-Status erneut...');
updateMerklisteButtonStatus(newButton);
}, 500);
} catch (error) {
console.error('[Merkliste] Fehler beim Toggle:', error);
console.error('[Merkliste] Error Details:', {
message: error.message,
stack: error.stack,
name: error.name
});
// Revert optimistisches Update bei Fehler
if (isCurrentlyInMerkliste) {
newButton.classList.add('merkliste-true');
newButton.textContent = newButton.dataset.bpMerklisteTextAdded || '✓ In Merkliste';
console.log('[Merkliste] Revert: Button zurück auf "in Merkliste"');
} else {
newButton.classList.remove('merkliste-true');
newButton.textContent = newButton.dataset.bpMerklisteTextAdd || 'Zur Merkliste';
console.log('[Merkliste] Revert: Button zurück auf "nicht in Merkliste"');
}
showToast(error.message || 'Fehler beim Aktualisieren der Merkliste', 'error');
} finally {
newButton.disabled = false;
}
});
});
}
/**
* Produkt-Buttons (alte Funktion für Rückwärtskompatibilität)
*/
function initProductButtons() {
const buttons = document.querySelectorAll('.bp-add-to-project, [data-bp-product-id]');
buttons.forEach(button => {
// Entferne alte Event-Listener (verhindere Duplikate)
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
// Entferne "hidden" Klasse nur wenn Projekt vorhanden
if (projectId) {
newButton.classList.remove('hidden');
const externalId = newButton.dataset.bpProductId || '';
if (externalId) {
updateMerklisteButtonStatus(newButton);
}
}
newButton.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
if (!projectId) {
createPopup();
return;
}
// Konvertiere alte Struktur zu neuer Merkliste-Struktur
const externalId = newButton.dataset.bpProductId || '';
const merklisteData = {
external_id: externalId,
titel: newButton.dataset.bpProductName || '',
kategorie: 'Produkt',
bild: newButton.dataset.bpProductImage || '',
marke: newButton.dataset.bpProductBrand || null,
serie: newButton.dataset.bpProductSeries || null,
sku: newButton.dataset.bpProductSku || null,
source_url: window.location.href,
source_system: 'wordpress'
};
// Prüfe aktuellen Status VOR dem Toggle (optimistisches Update)
const isCurrentlyInMerkliste = newButton.classList.contains('merkliste-true');
// Optimistisches Update (sofort visuell ändern)
if (isCurrentlyInMerkliste) {
newButton.classList.remove('merkliste-true');
newButton.textContent = newButton.dataset.bpProductTextAdd || 'Zum BadProjekt-Pass hinzufügen';
} else {
newButton.classList.add('merkliste-true');
newButton.textContent = newButton.dataset.bpProductTextAdded || '✓ Hinzugefügt';
}
// Deaktiviere Button während Request
newButton.disabled = true;
try {
console.log('[Merkliste-Product] Toggle Start:', {
externalId,
isCurrentlyInMerkliste,
projectId,
merklisteData
});
const result = await api.toggleMerkliste(projectId, merklisteData);
console.log('[Merkliste-Product] Toggle Response:', result);
console.log('[Merkliste-Product] Response Structure:', {
success: result?.success,
data: result?.data,
action: result?.data?.action,
message: result?.message
});
if (!result) {
throw new Error('Keine Response vom Server erhalten');
}
if (!result.success) {
throw new Error(result.message || result.error || 'Fehler beim Toggle');
}
if (!result.data) {
console.warn('[Merkliste-Product] Response hat kein data-Objekt:', result);
throw new Error('Ungültige Response-Struktur');
}
// Korrigiere Status basierend auf Response
const action = result.data.action;
console.log('[Merkliste-Product] Action:', action);
if (action === 'removed') {
newButton.classList.remove('merkliste-true');
newButton.textContent = newButton.dataset.bpProductTextAdd || 'Zum BadProjekt-Pass hinzufügen';
showToast('Aus Merkliste entfernt', 'success');
console.log('[Merkliste-Product] Button-Status auf "entfernt" gesetzt');
} else if (action === 'added' || action === 'already_exists') {
newButton.classList.add('merkliste-true');
newButton.textContent = newButton.dataset.bpProductTextAdded || '✓ Hinzugefügt';
if (action === 'already_exists') {
showToast('Bereits in Merkliste', 'info');
} else {
showToast('Zur Merkliste hinzugefügt', 'success');
}
console.log('[Merkliste-Product] Button-Status auf "hinzugefügt" gesetzt');
} else {
console.warn('[Merkliste-Product] Unbekannte Action:', action);
// Fallback: Prüfe Status vom Server
setTimeout(() => {
updateMerklisteButtonStatus(newButton);
}, 500);
}
// Prüfe Status nochmal (sicherstellen)
setTimeout(() => {
console.log('[Merkliste-Product] Prüfe Button-Status erneut...');
updateMerklisteButtonStatus(newButton);
}, 500);
} catch (error) {
console.error('[Merkliste-Product] Fehler beim Toggle:', error);
console.error('[Merkliste-Product] Error Details:', {
message: error.message,
stack: error.stack,
name: error.name
});
// Revert optimistisches Update bei Fehler
if (isCurrentlyInMerkliste) {
newButton.classList.add('merkliste-true');
newButton.textContent = newButton.dataset.bpProductTextAdded || '✓ Hinzugefügt';
console.log('[Merkliste-Product] Revert: Button zurück auf "in Merkliste"');
} else {
newButton.classList.remove('merkliste-true');
newButton.textContent = newButton.dataset.bpProductTextAdd || 'Zum BadProjekt-Pass hinzufügen';
console.log('[Merkliste-Product] Revert: Button zurück auf "nicht in Merkliste"');
}
showToast(error.message || 'Fehler beim Aktualisieren der Merkliste', 'error');
} finally {
newButton.disabled = false;
}
});
});
}
/**
* Download-Links
*/
function initDownloadLinks() {
const links = document.querySelectorAll('.bp-download-link, [data-bp-download-id]');
links.forEach(link => {
// Entferne alte Event-Listener (falls vorhanden) durch Klonen
const newLink = link.cloneNode(true);
link.parentNode?.replaceChild(newLink, link);
const linkToUse = newLink;
// Entferne "hidden" Klasse nur wenn Projekt vorhanden
if (projectId) {
linkToUse.classList.remove('hidden');
}
linkToUse.addEventListener('click', async (e) => {
// Prüfe ob Link in neuem Tab geöffnet werden soll
const openInNewTab = linkToUse.target === '_blank' || linkToUse.getAttribute('target') === '_blank';
const downloadUrl = linkToUse.href;
if (!projectId) {
// Wenn kein Projekt vorhanden, lasse Standard-Verhalten zu
return; // Standard-Navigation läuft (inkl. target="_blank")
}
// Für target="_blank": Lasse Standard-Verhalten zu, tracke nur im Hintergrund
// Das verhindert Popup-Blocker-Probleme
if (openInNewTab) {
// Lassen Sie den Link normal öffnen (kein preventDefault)
// Tracking im Hintergrund (nicht-blockierend)
const downloadData = {
download_id: linkToUse.dataset.bpDownloadId || '',
title: linkToUse.dataset.bpDownloadTitle || linkToUse.textContent,
category: linkToUse.dataset.bpDownloadCategory || '',
file_url: downloadUrl,
thumbnail_url: linkToUse.dataset.bpDownloadThumbnail || null,
source_page: window.location.href,
source_system: 'wordpress'
};
// Starte Tracking im Hintergrund (nicht-blockierend)
api.trackDownload(projectId, downloadData).catch(error => {
console.warn('Download tracking failed:', error);
});
// Lassen Sie den Link normal öffnen (Standard-Verhalten)
return; // Kein preventDefault, Link öffnet sich normal in neuem Tab
}
// Für gleichen Tab: Verhindere Navigation, tracke, dann navigiere
e.preventDefault();
const downloadData = {
download_id: linkToUse.dataset.bpDownloadId || '',
title: linkToUse.dataset.bpDownloadTitle || linkToUse.textContent,
category: linkToUse.dataset.bpDownloadCategory || '',
file_url: downloadUrl,
thumbnail_url: linkToUse.dataset.bpDownloadThumbnail || null,
source_page: window.location.href,
source_system: 'wordpress'
};
try {
// Sende Tracking-Request (mit Timeout, damit Download nicht zu lange wartet)
const result = await Promise.race([
api.trackDownload(projectId, downloadData),
new Promise((resolve) => setTimeout(() => resolve({}), 2000)) // Max 2 Sekunden warten
]);
// Optional: Toast für Update (nur wenn nicht Timeout)
if (result && result.data?.action === 'updated') {
// Leise aktualisiert, kein Toast nötig
}
} catch (error) {
// Fehler ignorieren, Download läuft trotzdem
console.warn('Download tracking failed:', error);
}
// Öffne im gleichen Tab nach Tracking
window.location.href = downloadUrl;
});
});
}
/**
* 3D-Konfigurator-Seite erkennen und Daten erfassen
*/
function detectConfigurationPage() {
if (!projectId) {
return; // Kein Projekt vorhanden
}
// Suche nach Data-Attributen (Body oder Container-Element)
let configElement = document.body;
// Prüfe ob Container-Element mit Data-Attributen existiert
const configContainer = document.querySelector('[data-bp-config-id]');
if (configContainer) {
configElement = configContainer;
}
// Prüfe ob Konfigurationsseite (mindestens ID muss vorhanden sein)
const configId = configElement?.dataset?.bpConfigId ||
document.querySelector('meta[name="bp-configuration-id"]')?.getAttribute('content') ||
null;
if (!configId) {
return; // Keine Konfigurationsseite
}
// Budget aus Data-Attribut oder Meta-Tag
const budget = configElement?.dataset?.bpConfigBudget ?
parseFloat(configElement.dataset.bpConfigBudget) :
(document.querySelector('meta[name="bp-budget"]') ?
parseFloat(document.querySelector('meta[name="bp-budget"]').getAttribute('content')) :
null);
// Sammle alle Konfigurationsdaten aus Data-Attributen
const configData = {};
if (!configElement) {
return;
}
// Einfache Felder
if (configElement.dataset.bpConfigId) configData.id = configElement.dataset.bpConfigId;
if (configElement.dataset.bpConfigErstelltAm) configData.erstellt_am = configElement.dataset.bpConfigErstelltAm;
if (configElement.dataset.bpConfigBadplanungsId) configData.badplanungs_id = configElement.dataset.bpConfigBadplanungsId;
if (configElement.dataset.bpConfigAngebotPdfLink) configData.angebot_pdf_link = configElement.dataset.bpConfigAngebotPdfLink;
if (configElement.dataset.bpConfigBadplanungsApiLink) configData.badplanungs_api_link = configElement.dataset.bpConfigBadplanungsApiLink;
if (configElement.dataset.bpConfigBadplanungsDeeplink) configData.badplanungs_deeplink = configElement.dataset.bpConfigBadplanungsDeeplink;
// Preis-Struktur (verschachtelt)
if (configElement.dataset.bpConfigPreisGesamt || configElement.dataset.bpConfigPreisMontage || configElement.dataset.bpConfigPreisMaterial) {
configData.preis = {};
if (configElement.dataset.bpConfigPreisGesamt) configData.preis.gesamt = parseFloat(configElement.dataset.bpConfigPreisGesamt);
if (configElement.dataset.bpConfigPreisMontage) configData.preis.montage = parseFloat(configElement.dataset.bpConfigPreisMontage);
if (configElement.dataset.bpConfigPreisMaterial) configData.preis.material = parseFloat(configElement.dataset.bpConfigPreisMaterial);
}
// Module (Array - komma-getrennt oder JSON)
if (configElement.dataset.bpConfigModule) {
try {
// Versuche zuerst als JSON zu parsen
configData.module = JSON.parse(configElement.dataset.bpConfigModule);
} catch (e) {
// Falls kein JSON, als komma-getrennte Liste behandeln
const moduleNames = configElement.dataset.bpConfigModule.split(',').map(name => name.trim()).filter(name => name);
configData.module = moduleNames.map(name => ({ name: name }));
}
}
// Fallback: JSON-LD falls vorhanden (für Rückwärtskompatibilität)
if (Object.keys(configData).length === 0) {
const jsonLd = document.querySelector('script[type="application/ld+json"]');
if (jsonLd) {
try {
const ldData = JSON.parse(jsonLd.textContent);
if (ldData.configurationData) {
Object.assign(configData, ldData.configurationData);
}
} catch (e) {
console.warn('Konnte JSON-LD nicht parsen:', e);
}
}
}
// Erfasse Konfiguration (Fehler werden ignoriert, damit die Seite nicht blockiert wird)
api.saveConfiguration(projectId, {
configuration_id: configId,
budget: budget || (configData.preis?.gesamt ? parseFloat(configData.preis.gesamt) : null),
configuration_data: Object.keys(configData).length > 0 ? configData : null,
source_url: window.location.href
}).catch(error => {
// Fehler wird nur geloggt, blockiert aber nicht die Seite
console.warn('Konfiguration konnte nicht gespeichert werden (möglicherweise fehlt die Migration):', error);
});
}
/**
* Popup-Trigger
*/
function initPopupTriggers() {
// Prüfe ob Popup nicht angezeigt werden soll (permanent)
if (localStorage.getItem('bp_popup_dont_show') === 'true') {
return;
}
// Prüfe ob Popup bereits in dieser Session gezeigt wurde
if (sessionStorage.getItem('bp_popup_shown') === 'true') {
return;
}
// Timer (8 Sekunden)
setTimeout(() => {
if (!projectCode && !document.getElementById('bp-popup')) {
const popup = createPopup();
if (popup) {
// Flag wird beim Schließen gesetzt, nicht hier
}
}
}, 8000);
// Scroll-Tiefe (50%)
let scrolled = false;
window.addEventListener('scroll', () => {
if (scrolled || projectCode) return;
const scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
if (scrollPercent > 50 && !document.getElementById('bp-popup')) {
scrolled = true;
const popup = createPopup();
if (popup) {
// Flag wird beim Schließen gesetzt, nicht hier
}
}
});
// Exit Intent
document.addEventListener('mouseleave', (e) => {
if (e.clientY <= 0 && !projectCode && !document.getElementById('bp-popup')) {
const popup = createPopup();
if (popup) {
// Flag wird beim Schließen gesetzt, nicht hier
}
}
});
}
/**
* Öffnet Popup manuell (z.B. über Button/Link)
*/
function openPopupManually() {
// Entferne Session-Flag, damit Popup angezeigt wird
sessionStorage.removeItem('bp_popup_shown');
// Prüfe auch ob Popup bereits existiert
const existingPopup = document.getElementById('bp-popup');
if (existingPopup) {
existingPopup.remove();
}
createPopup();
}
// Exponiere Funktion global für manuelles Öffnen
window.bpOpenPopup = openPopupManually;
// Unterstütze auch data-Attribute für automatische Event-Listener
document.addEventListener('DOMContentLoaded', () => {
// Suche alle Buttons/Links mit data-bp-open-popup Attribut
document.querySelectorAll('[data-bp-open-popup]').forEach(element => {
element.addEventListener('click', (e) => {
e.preventDefault();
openPopupManually();
});
});
});
/**
* Initialisierung
*/
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
return;
}
initProjectButton();
initMerklisteButtons(); // Neue allgemeine Merkliste-Funktion
initProductButtons(); // Alte Funktion für Rückwärtskompatibilität
initDownloadLinks();
initPopupTriggers();
detectConfigurationPage(); // 3D-Konfigurator-Erkennung
trackPageVisit(); // Seitenbesuch und Scrolltiefe tracken
// Force re-initialization nach kurzer Verzögerung (für Caching-Probleme)
setTimeout(() => {
// Prüfe ob Buttons sichtbar sein sollten
if (projectId) {
document.querySelectorAll('.bp-add-to-merkliste, [data-bp-merkliste-id], .bp-add-to-project, [data-bp-product-id], .bp-download-link, [data-bp-download-id]').forEach(element => {
element.classList.remove('hidden');
});
// Initialisiere Buttons erneut
initMerklisteButtons();
initProductButtons();
initDownloadLinks();
}
}, 100);
}
// Start
init();
// Export für externe Nutzung
window.BadProjektPass = {
api,
config,
getProjectCode: () => projectCode,
getProjectToken: () => projectToken,
openProject: () => {
if (projectCode) {
window.open(`${config.appBaseUrl}/p/${projectCode}`, '_blank');
} else {
createPopup();
}
}
};
})();