Display CMS Optimierungen 03-06-2026
This commit is contained in:
parent
6c6d683b9a
commit
bebfa63e87
17 changed files with 925 additions and 133 deletions
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue