12-05-2026 admin, Panel Displays
This commit is contained in:
parent
0762e3beac
commit
6a65354f4c
43 changed files with 3273 additions and 410 deletions
|
|
@ -40,25 +40,21 @@
|
|||
PLAYER FRAME – 9:16 Container
|
||||
======================================== */
|
||||
.player-frame {
|
||||
width: 100vw; height: 100vh;
|
||||
position: fixed; inset: 0;
|
||||
width: 100%; height: 100%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.player-viewport {
|
||||
width: 100%; height: 100%;
|
||||
width: min(100vw, calc(100vh * 9 / 16));
|
||||
height: min(100vh, calc(100vw * 16 / 9));
|
||||
max-width: 1080px; max-height: 1920px;
|
||||
aspect-ratio: 9 / 16;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
@media (min-aspect-ratio: 9/16) {
|
||||
.player-viewport { width: auto; height: 100vh; }
|
||||
}
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.player-viewport { width: 100vw; height: auto; }
|
||||
container-type: size;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
|
|
@ -97,11 +93,11 @@
|
|||
display: block;
|
||||
}
|
||||
.vd-footer {
|
||||
height: 9.67vh; min-height: 100px;
|
||||
height: 9.67%;
|
||||
background: #1a1a1a; color: #fff;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
font-size: 10px;
|
||||
padding: 0 3%;
|
||||
font-size: clamp(5px, 0.92cqw, 10px);
|
||||
position: relative;
|
||||
}
|
||||
.vd-footer.hidden { display: none; }
|
||||
|
|
@ -119,7 +115,7 @@
|
|||
width: 25%; display: flex; flex-direction: column; align-items: center;
|
||||
}
|
||||
.vd-qr img {
|
||||
width: 8em; max-width: 100px; aspect-ratio: 1;
|
||||
width: clamp(40px, 8.5cqw, 100px); aspect-ratio: 1;
|
||||
object-fit: contain; background: #fff;
|
||||
padding: 0.4em; border-radius: 0.6em;
|
||||
}
|
||||
|
|
@ -187,7 +183,7 @@
|
|||
.b2in-footer-url { font-size: 1.5vh; font-weight: 600; color: #20a0da; }
|
||||
.b2in-footer-name { font-size: 1.2vh; color: rgba(255,255,255,0.5); margin-left: 1vh; }
|
||||
.b2in-footer-qr img {
|
||||
height: 5vh; aspect-ratio: 1; border-radius: 0.5vh;
|
||||
height: clamp(32px, 5cqh, 96px); aspect-ratio: 1; border-radius: 0.5vh;
|
||||
background: #fff; padding: 0.3vh;
|
||||
}
|
||||
|
||||
|
|
@ -439,6 +435,18 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
function escapeHtml(value) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = value ?? '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function normalizeQrUrl(url) {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) return url;
|
||||
return `https://${url}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cabinet Display Player
|
||||
*
|
||||
|
|
@ -449,17 +457,20 @@
|
|||
*/
|
||||
class DisplayPlayer {
|
||||
constructor() {
|
||||
// Detect display ID from URL
|
||||
// Detect display context from URL
|
||||
this.previewToken = this.detectPreviewToken();
|
||||
this.moduleId = this.detectModuleId();
|
||||
this.itemId = this.detectItemId();
|
||||
this.displayId = this.detectDisplayId();
|
||||
if (!this.displayId) {
|
||||
this.showError('Keine Display-ID angegeben. URL: /display/index.html?id=1');
|
||||
if (!this.displayId && !this.previewToken && !this.moduleId) {
|
||||
this.showError('Keine Display-ID oder Vorschau angegeben. URL: /display/index.html?id=1');
|
||||
return;
|
||||
}
|
||||
|
||||
// API
|
||||
this.BASE_URL = this.detectBaseUrl();
|
||||
this.API_CONFIG = `${this.BASE_URL}/api/display/${this.displayId}/config`;
|
||||
this.API_CHECK = `${this.BASE_URL}/api/display/${this.displayId}/check`;
|
||||
this.API_CONFIG = this.detectConfigUrl();
|
||||
this.API_CHECK = this.detectCheckUrl();
|
||||
|
||||
// Timing
|
||||
this.POLL_INTERVAL = 60000;
|
||||
|
|
@ -487,7 +498,7 @@ class DisplayPlayer {
|
|||
this.errorOverlay = document.getElementById('error-overlay');
|
||||
this.errorMessage = document.getElementById('error-message');
|
||||
|
||||
this.loadingInfo.textContent = `Display #${this.displayId}`;
|
||||
this.loadingInfo.textContent = this.detectLoadingLabel();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
|
@ -506,6 +517,75 @@ class DisplayPlayer {
|
|||
return null;
|
||||
}
|
||||
|
||||
detectPreviewToken() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('preview')) {
|
||||
return params.get('preview');
|
||||
}
|
||||
const pathMatch = window.location.pathname.match(/\/preview\/([^/]+)/);
|
||||
if (pathMatch && pathMatch[1] !== 'module') {
|
||||
return pathMatch[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
detectModuleId() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('module')) {
|
||||
return params.get('module');
|
||||
}
|
||||
const pathMatch = window.location.pathname.match(/\/preview\/module\/(\d+)/);
|
||||
if (pathMatch) {
|
||||
return pathMatch[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
detectItemId() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('item')) {
|
||||
return params.get('item');
|
||||
}
|
||||
const pathMatch = window.location.pathname.match(/\/preview\/module\/\d+\/item\/(\d+)/);
|
||||
if (pathMatch) {
|
||||
return pathMatch[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
detectConfigUrl() {
|
||||
if (this.previewToken) {
|
||||
return `${this.BASE_URL}/api/display/preview/${this.previewToken}`;
|
||||
}
|
||||
if (this.moduleId && this.itemId) {
|
||||
return `${this.BASE_URL}/api/display/module/${this.moduleId}/item/${this.itemId}/preview`;
|
||||
}
|
||||
if (this.moduleId) {
|
||||
return `${this.BASE_URL}/api/display/module/${this.moduleId}/preview`;
|
||||
}
|
||||
return `${this.BASE_URL}/api/display/${this.displayId}/config`;
|
||||
}
|
||||
|
||||
detectCheckUrl() {
|
||||
if (this.previewToken || this.moduleId) {
|
||||
return null;
|
||||
}
|
||||
return `${this.BASE_URL}/api/display/${this.displayId}/check`;
|
||||
}
|
||||
|
||||
detectLoadingLabel() {
|
||||
if (this.previewToken) {
|
||||
return 'Display-Entwurf';
|
||||
}
|
||||
if (this.moduleId) {
|
||||
if (this.itemId) {
|
||||
return `Modul #${this.moduleId} / Item #${this.itemId}`;
|
||||
}
|
||||
return `Modul #${this.moduleId}`;
|
||||
}
|
||||
return `Display #${this.displayId}`;
|
||||
}
|
||||
|
||||
detectBaseUrl() {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname === 'cabinet.b2in.eu' || hostname.includes('b2in.eu')) {
|
||||
|
|
@ -519,7 +599,7 @@ class DisplayPlayer {
|
|||
// ========================================
|
||||
|
||||
async init() {
|
||||
console.log(`[Display] Initializing display #${this.displayId}`);
|
||||
console.log(`[Display] Initializing ${this.detectLoadingLabel()}`);
|
||||
|
||||
try {
|
||||
await this.fetchConfig();
|
||||
|
|
@ -563,6 +643,10 @@ class DisplayPlayer {
|
|||
}
|
||||
|
||||
startPolling() {
|
||||
if (!this.API_CHECK) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch(this.API_CHECK);
|
||||
|
|
@ -711,6 +795,7 @@ class VideoDisplayRenderer {
|
|||
constructor(container, data, onComplete) {
|
||||
this.container = container;
|
||||
this.data = data;
|
||||
this.settings = data.settings || {};
|
||||
this.onComplete = onComplete;
|
||||
this.videos = data.videoPlaylist || [];
|
||||
this.footerContent = data.footerContent || [];
|
||||
|
|
@ -752,7 +837,7 @@ class VideoDisplayRenderer {
|
|||
</div>
|
||||
<div class="vd-qr">
|
||||
<img alt="QR">
|
||||
<span class="vd-qr-label">Website</span>
|
||||
<span class="vd-qr-label">${escapeHtml(this.settings.qr_label || 'Website')}</span>
|
||||
</div>
|
||||
`;
|
||||
layer.appendChild(this.footerEl);
|
||||
|
|
@ -880,7 +965,10 @@ class VideoDisplayRenderer {
|
|||
}
|
||||
|
||||
resolveAssetUrl(src) {
|
||||
if (!src) return '';
|
||||
if (src.startsWith('http')) return src;
|
||||
if (src.startsWith('/')) return src;
|
||||
if (src.startsWith('../')) return src;
|
||||
// Assets relative to _cabinet
|
||||
return `../${src}`;
|
||||
}
|
||||
|
|
@ -929,10 +1017,20 @@ class B2inRenderer {
|
|||
layer.className = 'version-layer b2in-layer active';
|
||||
layer.setAttribute('data-theme', this.theme);
|
||||
|
||||
const headerLogoUrl = this.resolveUrl(this.settings.header_logo_url || '../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 || '';
|
||||
const footerPrefix = this.settings.footer_prefix || 'by';
|
||||
const footerNameHtml = footerName
|
||||
? `<span class="b2in-footer-name">${escapeHtml(footerPrefix ? `${footerPrefix} ${footerName}` : footerName)}</span>`
|
||||
: '';
|
||||
const qrUrl = normalizeQrUrl(this.settings.qr_url || footerUrl || 'b2in.eu');
|
||||
|
||||
layer.innerHTML = `
|
||||
<header class="b2in-header">
|
||||
<img src="../assets/b2in-logo-positive.svg" alt="B2in">
|
||||
<span class="b2in-claim">Connecting Design & Property</span>
|
||||
<img src="${escapeHtml(headerLogoUrl)}" alt="B2in">
|
||||
<span class="b2in-claim">${escapeHtml(headerClaim)}</span>
|
||||
</header>
|
||||
<section class="b2in-media">
|
||||
<div class="b2in-media-layer active" id="b2in-layer-a"></div>
|
||||
|
|
@ -944,11 +1042,11 @@ class B2inRenderer {
|
|||
</section>
|
||||
<footer class="b2in-footer">
|
||||
<div>
|
||||
<span class="b2in-footer-url">${this.settings.footer_url || 'B2in.eu'}</span>
|
||||
<span class="b2in-footer-name">by ${this.settings.footer_name || ''}</span>
|
||||
<span class="b2in-footer-url">${escapeHtml(footerUrl)}</span>
|
||||
${footerNameHtml}
|
||||
</div>
|
||||
<div class="b2in-footer-qr">
|
||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&margin=5&data=${encodeURIComponent('https://' + (this.settings.footer_url || 'b2in.eu'))}" alt="QR">
|
||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&margin=5&data=${encodeURIComponent(qrUrl)}" alt="QR">
|
||||
</div>
|
||||
</footer>
|
||||
<div class="b2in-progress-track">
|
||||
|
|
@ -1071,6 +1169,7 @@ class B2inRenderer {
|
|||
resolveUrl(url) {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('http')) return url;
|
||||
if (url.startsWith('/')) return url;
|
||||
if (url.startsWith('../')) return url;
|
||||
return `../${url}`;
|
||||
}
|
||||
|
|
@ -1147,10 +1246,12 @@ class OffersRenderer {
|
|||
const cw = this.container.clientWidth || OffersRenderer.DESIGN_W;
|
||||
const ch = this.container.clientHeight || OffersRenderer.DESIGN_H;
|
||||
const scale = Math.min(cw / OffersRenderer.DESIGN_W, ch / OffersRenderer.DESIGN_H);
|
||||
const offsetX = Math.max(0, (cw - OffersRenderer.DESIGN_W * scale) / 2);
|
||||
const offsetY = Math.max(0, (ch - OffersRenderer.DESIGN_H * scale) / 2);
|
||||
|
||||
this.slideArticles.forEach(article => {
|
||||
if (article) {
|
||||
article.style.transform = `scale(${scale})`;
|
||||
article.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1168,12 +1269,16 @@ class OffersRenderer {
|
|||
|
||||
const brand = document.createElement('div');
|
||||
brand.className = 'offer-brand';
|
||||
brand.innerHTML = '<img src="../logo-cabinet-300.png" alt="CABINET" class="offer-brand-logo">';
|
||||
const brandLogo = document.createElement('img');
|
||||
brandLogo.src = this.resolveUrl(this.settings.logo_url || '../logo-cabinet-300.png');
|
||||
brandLogo.alt = 'CABINET';
|
||||
brandLogo.className = 'offer-brand-logo';
|
||||
brand.appendChild(brandLogo);
|
||||
|
||||
if (slide.show_brand_text) {
|
||||
const brandText = document.createElement('span');
|
||||
brandText.className = 'offer-brand-text';
|
||||
brandText.textContent = 'Bielefeld';
|
||||
brandText.textContent = this.settings.brand_text || 'Bielefeld';
|
||||
brand.appendChild(brandText);
|
||||
}
|
||||
|
||||
|
|
@ -1308,25 +1413,27 @@ class OffersRenderer {
|
|||
const qrHeader = document.createElement('div');
|
||||
qrHeader.className = 'offer-qr-header';
|
||||
qrHeader.innerHTML = `
|
||||
<p class="offer-qr-title">${this.escapeHtml(slide.qr_title || 'Kontakt')}</p>
|
||||
<p class="offer-qr-subtitle">QR scannen</p>
|
||||
<p class="offer-qr-title">${this.escapeHtml(slide.qr_title || this.settings.qr_default_title || 'Kontakt')}</p>
|
||||
<p class="offer-qr-subtitle">${this.escapeHtml(this.settings.qr_subtitle || 'QR scannen')}</p>
|
||||
`;
|
||||
qrBox.appendChild(qrHeader);
|
||||
|
||||
const qrWrapper = document.createElement('div');
|
||||
qrWrapper.className = 'offer-qr-wrapper';
|
||||
if (slide.qr_url) {
|
||||
const qrUrl = slide.qr_url || this.settings.footer_url || '';
|
||||
if (qrUrl) {
|
||||
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(slide.qr_url)}`;
|
||||
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';
|
||||
qrWrapper.appendChild(qrImg);
|
||||
}
|
||||
qrBox.appendChild(qrWrapper);
|
||||
|
||||
if (slide.contact) {
|
||||
const contactText = slide.contact || this.settings.footer_claim || '';
|
||||
if (contactText) {
|
||||
const contact = document.createElement('p');
|
||||
contact.className = 'offer-qr-contact';
|
||||
contact.innerHTML = slide.contact.replace(/\n/g, '<br>');
|
||||
contact.innerHTML = contactText.replace(/\n/g, '<br>');
|
||||
qrBox.appendChild(contact);
|
||||
}
|
||||
|
||||
|
|
@ -1388,6 +1495,7 @@ class OffersRenderer {
|
|||
resolveUrl(url) {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('http')) return url;
|
||||
if (url.startsWith('/')) return url;
|
||||
if (url.startsWith('../')) return url;
|
||||
return `../${url}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue