Display CMS Optimierungen 03-06-2026

This commit is contained in:
Kevin Adametz 2026-06-03 15:43:18 +02:00
parent 6c6d683b9a
commit bebfa63e87
17 changed files with 925 additions and 133 deletions

View file

@ -270,6 +270,9 @@
transform-origin: top left;
flex-shrink: 0;
}
.offer-slide--no-header { grid-template-rows: 1fr auto; }
.offer-slide--no-bottom { grid-template-rows: auto 1fr; }
.offer-slide--no-header.offer-slide--no-bottom { grid-template-rows: 1fr; }
/* Offer Header */
.offer-header {
@ -309,6 +312,13 @@
display: grid; grid-template-columns: 1fr 300px; gap: 24px;
align-items: stretch;
}
.offer-bottom--info-only,
.offer-bottom--qr-only {
grid-template-columns: 1fr;
}
.offer-bottom--compact {
align-items: start;
}
/* Info Box */
.offer-info {
@ -317,6 +327,13 @@
border: 1px solid #e8e8e8; border-radius: 24px;
padding: 28px; min-height: 340px;
}
.offer-info--compact {
min-height: 0;
padding: 24px 28px;
}
.offer-info--title-only {
padding: 22px 28px;
}
.offer-info-content { flex: 1; }
.offer-eyebrow {
font-size: 18px; color: #737373; letter-spacing: 0.14em;
@ -546,6 +563,57 @@ function normalizeQrUrl(url) {
return `https://${url}`;
}
function qrCodeImageUrl(url, size = 300, margin = 8, extraParams = {}) {
const normalizedUrl = normalizeQrUrl(url);
if (!normalizedUrl) return '';
const params = new URLSearchParams({
size: `${size}x${size}`,
margin: String(margin),
data: normalizedUrl,
...extraParams,
});
return `https://api.qrserver.com/v1/create-qr-code/?${params.toString()}`;
}
function setQrImage(img, url, options = {}) {
const baseSrc = options.imageUrl || qrCodeImageUrl(url, options.size || 300, options.margin || 8, options.params || {});
if (!img || !baseSrc) return false;
const maxAttempts = options.maxAttempts || 3;
let attempt = 0;
img.alt = options.alt || 'QR Code';
img.loading = 'eager';
img.decoding = 'async';
img.referrerPolicy = 'no-referrer';
img.style.visibility = 'hidden';
const load = () => {
attempt++;
const separator = baseSrc.includes('?') ? '&' : '?';
img.src = `${baseSrc}${separator}cb=${Date.now()}-${attempt}`;
};
img.onload = () => {
img.style.visibility = 'visible';
};
img.onerror = () => {
if (attempt < maxAttempts) {
setTimeout(load, attempt * 1000);
return;
}
img.style.visibility = 'hidden';
};
load();
return true;
}
/**
* Cabinet Display Player
*
@ -1157,8 +1225,7 @@ class VideoDisplayRenderer {
headline.textContent = content.headline || '';
subline.textContent = content.subline || '';
if (content.url) {
qrImg.src = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&margin=10&data=${encodeURIComponent(content.url)}`;
if ((content.url || content.qr_image_url) && setQrImage(qrImg, content.url, { imageUrl: content.qr_image_url, margin: 10, alt: 'QR' })) {
qrContainer.style.display = 'flex';
} else {
qrContainer.style.display = 'none';
@ -1218,7 +1285,7 @@ class B2inRenderer {
layer.className = 'version-layer b2in-layer active' + (this.settings.show_footer === false ? ' no-footer' : '');
layer.setAttribute('data-theme', this.theme);
const headerLogoUrl = this.resolveUrl(this.settings.header_logo_url || '../assets/b2in-logo-positive.svg');
const headerLogoUrl = this.resolveUrl(this.settings.header_logo_url || '/_cabinet/assets/b2in-logo-positive.svg');
const headerClaim = this.settings.header_claim || 'Connecting Design & Property';
const footerUrl = this.settings.footer_url || 'B2in.eu';
const footerName = this.settings.footer_name || '';
@ -1227,6 +1294,7 @@ class B2inRenderer {
? `<span class="b2in-footer-name">${escapeHtml(footerPrefix ? `${footerPrefix} ${footerName}` : footerName)}</span>`
: '';
const qrUrl = normalizeQrUrl(this.settings.qr_url || footerUrl || 'b2in.eu');
const qrImageUrl = this.settings.qr_image_url || '';
// Footer visibility, brand element visibility + corner positioning
const showFooter = this.settings.show_footer !== false;
@ -1269,7 +1337,7 @@ class B2inRenderer {
${footerNameHtml}
</div>
<div class="b2in-footer-qr">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&margin=5&data=${encodeURIComponent(qrUrl)}" alt="QR">
<img alt="QR">
</div>
</footer>`
: '';
@ -1300,6 +1368,11 @@ class B2inRenderer {
this.headline = layer.querySelector('#b2in-headline');
this.subline = layer.querySelector('#b2in-subline');
this.progress = layer.querySelector('#b2in-progress');
const footerQrImg = layer.querySelector('.b2in-footer-qr img');
if (footerQrImg) {
setQrImage(footerQrImg, qrUrl, { imageUrl: qrImageUrl, size: 200, margin: 5, alt: 'QR' });
}
}
start() {
@ -1409,6 +1482,8 @@ class B2inRenderer {
if (!url) return '';
if (url.startsWith('http')) return url;
if (url.startsWith('/')) return url;
if (url.startsWith('../assets/')) return `/_cabinet/assets/${url.slice('../assets/'.length)}`;
if (!url.includes('/') && url.endsWith('.svg')) return `/_cabinet/assets/${url}`;
if (url.startsWith('../')) return url;
return `../${url}`;
}
@ -1512,12 +1587,19 @@ class OffersRenderer {
qr: (slide.show_qr ?? has(slide.qr_url)) && has(qrUrl),
contact: (slide.show_contact ?? has(slide.contact)) && has(contactText),
};
const showInfo = has(slide.title) || show.eyebrow || show.subline || show.bullets || show.price || show.disclaimer;
const showQrBox = show.qr || show.contact;
const showBottom = showInfo || showQrBox;
const infoBlockCount = [has(slide.title), show.eyebrow, show.subline, show.bullets, show.price, show.disclaimer]
.filter(Boolean).length;
const wrapper = document.createElement('div');
wrapper.className = 'offers-slide-container';
const article = document.createElement('article');
article.className = 'offer-slide';
if (!show.logo) article.classList.add('offer-slide--no-header');
if (!showBottom) article.classList.add('offer-slide--no-bottom');
// --- HEADER (Logo & Marke) – per slide, toggleable ---
if (show.logo) {
@ -1549,9 +1631,6 @@ class OffersRenderer {
}
article.appendChild(header);
} else {
// Without the header row, let the hero stay the flexible middle row.
article.style.gridTemplateRows = '1fr auto';
}
// --- HERO ---
@ -1571,101 +1650,114 @@ class OffersRenderer {
article.appendChild(hero);
if (!showBottom) {
wrapper.appendChild(article);
return wrapper;
}
// --- BOTTOM: Info + QR ---
const bottom = document.createElement('section');
bottom.className = 'offer-bottom';
if (showInfo && !showQrBox) bottom.classList.add('offer-bottom--info-only');
if (!showInfo && showQrBox) bottom.classList.add('offer-bottom--qr-only');
if (showInfo && infoBlockCount <= 3 && !show.price && !show.disclaimer) bottom.classList.add('offer-bottom--compact');
// Info
const info = document.createElement('div');
info.className = 'offer-info';
if (showInfo) {
const info = document.createElement('div');
info.className = 'offer-info';
if (infoBlockCount <= 3 && !show.price && !show.disclaimer) info.classList.add('offer-info--compact');
if (infoBlockCount === 1 && has(slide.title)) info.classList.add('offer-info--title-only');
const infoContent = document.createElement('div');
infoContent.className = 'offer-info-content';
const infoContent = document.createElement('div');
infoContent.className = 'offer-info-content';
if (show.eyebrow) {
const eyebrow = document.createElement('p');
eyebrow.className = 'offer-eyebrow';
eyebrow.textContent = slide.eyebrow;
infoContent.appendChild(eyebrow);
}
if (slide.title) {
const title = document.createElement('h1');
title.className = 'offer-title medium';
title.innerHTML = slide.title.replace(/\n/g, '<br>');
infoContent.appendChild(title);
}
if (show.subline) {
const subline = document.createElement('p');
subline.className = 'offer-subline';
subline.textContent = slide.subline;
infoContent.appendChild(subline);
}
if (show.bullets) {
const ul = document.createElement('ul');
ul.className = 'offer-bullets';
slide.bullets.forEach(text => {
if (!text) return;
const li = document.createElement('li');
li.innerHTML = `<span class="offer-bullet-dot"></span><span>${this.escapeHtml(text)}</span>`;
ul.appendChild(li);
});
infoContent.appendChild(ul);
}
info.appendChild(infoContent);
// Price block
if (show.price) {
const priceBlock = document.createElement('div');
priceBlock.className = 'offer-price-block';
const priceRow = document.createElement('div');
priceRow.className = 'offer-price-row';
const price = document.createElement('span');
price.className = 'offer-price';
price.textContent = slide.price;
priceRow.appendChild(price);
if (slide.original_price) {
const note = document.createElement('div');
note.className = slide.strike_original_price ? 'offer-price-note strike' : 'offer-price-note';
note.textContent = slide.original_price;
priceRow.appendChild(note);
if (show.eyebrow) {
const eyebrow = document.createElement('p');
eyebrow.className = 'offer-eyebrow';
eyebrow.textContent = slide.eyebrow;
infoContent.appendChild(eyebrow);
}
if (slide.tag_text) {
const note = document.createElement('div');
note.className = 'offer-price-note';
const tag = document.createElement('span');
tag.className = 'offer-impulse-tag';
tag.textContent = slide.tag_text;
note.appendChild(tag);
priceRow.appendChild(note);
if (slide.title) {
const title = document.createElement('h1');
title.className = 'offer-title medium';
title.innerHTML = slide.title.replace(/\n/g, '<br>');
infoContent.appendChild(title);
}
priceBlock.appendChild(priceRow);
info.appendChild(priceBlock);
}
if (show.subline) {
const subline = document.createElement('p');
subline.className = 'offer-subline';
subline.textContent = slide.subline;
infoContent.appendChild(subline);
}
// Disclaimer
if (show.disclaimer) {
const footer = document.createElement('div');
footer.className = 'offer-info-footer';
const disc = document.createElement('span');
disc.className = 'offer-disclaimer';
disc.textContent = slide.disclaimer;
footer.appendChild(disc);
info.appendChild(footer);
}
if (show.bullets) {
const ul = document.createElement('ul');
ul.className = 'offer-bullets';
slide.bullets.forEach(text => {
if (!text) return;
const li = document.createElement('li');
li.innerHTML = `<span class="offer-bullet-dot"></span><span>${this.escapeHtml(text)}</span>`;
ul.appendChild(li);
});
infoContent.appendChild(ul);
}
bottom.appendChild(info);
info.appendChild(infoContent);
// Price block
if (show.price) {
const priceBlock = document.createElement('div');
priceBlock.className = 'offer-price-block';
const priceRow = document.createElement('div');
priceRow.className = 'offer-price-row';
const price = document.createElement('span');
price.className = 'offer-price';
price.textContent = slide.price;
priceRow.appendChild(price);
if (slide.original_price) {
const note = document.createElement('div');
note.className = slide.strike_original_price ? 'offer-price-note strike' : 'offer-price-note';
note.textContent = slide.original_price;
priceRow.appendChild(note);
}
if (slide.tag_text) {
const note = document.createElement('div');
note.className = 'offer-price-note';
const tag = document.createElement('span');
tag.className = 'offer-impulse-tag';
tag.textContent = slide.tag_text;
note.appendChild(tag);
priceRow.appendChild(note);
}
priceBlock.appendChild(priceRow);
info.appendChild(priceBlock);
}
// Disclaimer
if (show.disclaimer) {
const footer = document.createElement('div');
footer.className = 'offer-info-footer';
const disc = document.createElement('span');
disc.className = 'offer-disclaimer';
disc.textContent = slide.disclaimer;
footer.appendChild(disc);
info.appendChild(footer);
}
bottom.appendChild(info);
}
// QR / Contact box – only when at least one of the two is enabled.
if (show.qr || show.contact) {
if (showQrBox) {
const qrBox = document.createElement('aside');
qrBox.className = 'offer-qr-box';
@ -1681,8 +1773,15 @@ class OffersRenderer {
const qrWrapper = document.createElement('div');
qrWrapper.className = 'offer-qr-wrapper';
const qrImg = document.createElement('img');
qrImg.src = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&color=000000&bgcolor=ffffff&margin=8&data=${encodeURIComponent(normalizeQrUrl(qrUrl))}`;
qrImg.alt = 'QR Code';
setQrImage(qrImg, qrUrl, {
imageUrl: slide.qr_image_url || '',
size: 300,
margin: 8,
params: {
color: '000000',
bgcolor: 'ffffff',
},
});
qrWrapper.appendChild(qrImg);
qrBox.appendChild(qrWrapper);
}
@ -1695,8 +1794,6 @@ class OffersRenderer {
}
bottom.appendChild(qrBox);
} else {
bottom.style.gridTemplateColumns = '1fr';
}
article.appendChild(bottom);
@ -1757,6 +1854,8 @@ class OffersRenderer {
if (!url) return '';
if (url.startsWith('http')) return url;
if (url.startsWith('/')) return url;
if (url.startsWith('../assets/')) return `/_cabinet/assets/${url.slice('../assets/'.length)}`;
if (!url.includes('/') && url.endsWith('.svg')) return `/_cabinet/assets/${url}`;
if (url.startsWith('../')) return url;
return `../${url}`;
}