20-02-2026
BIN
public/_cabinet/assets/cabinet-intro.jpg
Normal file
|
After Width: | Height: | Size: 894 KiB |
BIN
public/_cabinet/assets/goya.jpg
Normal file
|
After Width: | Height: | Size: 788 KiB |
BIN
public/_cabinet/assets/goya1.jpg
Normal file
|
After Width: | Height: | Size: 487 KiB |
BIN
public/_cabinet/assets/goya2.jpg
Normal file
|
After Width: | Height: | Size: 449 KiB |
BIN
public/_cabinet/assets/tango.jpg
Normal file
|
After Width: | Height: | Size: 837 KiB |
|
|
@ -290,69 +290,11 @@
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
/* Fullscreen Button */
|
||||
#fullscreen-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 1000;
|
||||
background-color: rgba(0, 159, 227, 0.8);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#fullscreen-btn:hover {
|
||||
background-color: rgba(0, 159, 227, 1);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
#fullscreen-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Button ausblenden wenn bereits im Fullscreen */
|
||||
#fullscreen-btn.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Fullscreen Reminder (nach Reload) */
|
||||
#fullscreen-btn.reminder {
|
||||
background-color: rgba(255, 152, 0, 0.95);
|
||||
animation: pulse 2s infinite;
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 12px rgba(255, 152, 0, 0.6);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- FULLSCREEN BUTTON -->
|
||||
<button id="fullscreen-btn" title="Vollbildmodus aktivieren">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle;">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
||||
</svg>
|
||||
<span style="vertical-align: middle;">V 1.3</span>
|
||||
</button>
|
||||
|
||||
<div id="main-container">
|
||||
<!-- VIDEO BEREICH -->
|
||||
|
|
@ -376,99 +318,6 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
/* ==============================================
|
||||
FULLSCREEN BUTTON LOGIC MIT AUTO-REMINDER
|
||||
============================================== */
|
||||
|
||||
const fullscreenBtn = document.getElementById('fullscreen-btn');
|
||||
const FULLSCREEN_STATE_KEY = 'cabinet_fullscreen_was_active';
|
||||
|
||||
// Fullscreen aktivieren
|
||||
function enterFullscreen() {
|
||||
const elem = document.documentElement;
|
||||
|
||||
// Merken dass Fullscreen aktiviert wurde (für nach Reload)
|
||||
localStorage.setItem(FULLSCREEN_STATE_KEY, 'true');
|
||||
|
||||
if (elem.requestFullscreen) {
|
||||
elem.requestFullscreen();
|
||||
} else if (elem.webkitRequestFullscreen) { // Safari/Chrome
|
||||
elem.webkitRequestFullscreen();
|
||||
} else if (elem.mozRequestFullScreen) { // Firefox
|
||||
elem.mozRequestFullScreen();
|
||||
} else if (elem.msRequestFullscreen) { // IE/Edge
|
||||
elem.msRequestFullscreen();
|
||||
}
|
||||
|
||||
window.displayLogger?.log('Fullscreen aktiviert');
|
||||
}
|
||||
|
||||
// Button Event Listener
|
||||
fullscreenBtn.addEventListener('click', () => {
|
||||
enterFullscreen();
|
||||
// Reminder-Klasse entfernen falls vorhanden
|
||||
fullscreenBtn.classList.remove('reminder');
|
||||
});
|
||||
|
||||
// Prüfen ob Fullscreen vorher aktiv war (nach Reload)
|
||||
function checkFullscreenRestore() {
|
||||
const wasFullscreen = localStorage.getItem(FULLSCREEN_STATE_KEY);
|
||||
|
||||
if (wasFullscreen === 'true') {
|
||||
// Fullscreen war vorher aktiv → Auffälliger Reminder
|
||||
fullscreenBtn.classList.add('reminder');
|
||||
fullscreenBtn.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle;">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
||||
</svg>
|
||||
<span style="vertical-align: middle;">⚠️ Vollbild aktivieren!</span>
|
||||
`;
|
||||
|
||||
window.displayLogger?.warn('Fullscreen-Reminder angezeigt (war vorher aktiv)', {
|
||||
reason: 'Page reload',
|
||||
previousState: 'fullscreen'
|
||||
});
|
||||
|
||||
// Nach 30 Sekunden automatisch versuchen (falls Kiosk-Mode)
|
||||
setTimeout(() => {
|
||||
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
|
||||
window.displayLogger?.log('Versuche Auto-Fullscreen (Kiosk-Mode?)');
|
||||
enterFullscreen();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
}
|
||||
|
||||
// Button ausblenden wenn bereits im Fullscreen
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
if (document.fullscreenElement) {
|
||||
fullscreenBtn.classList.add('hidden');
|
||||
fullscreenBtn.classList.remove('reminder');
|
||||
localStorage.setItem(FULLSCREEN_STATE_KEY, 'true');
|
||||
} else {
|
||||
fullscreenBtn.classList.remove('hidden');
|
||||
// Fullscreen wurde verlassen → State clearen
|
||||
localStorage.removeItem(FULLSCREEN_STATE_KEY);
|
||||
window.displayLogger?.log('Fullscreen verlassen');
|
||||
}
|
||||
});
|
||||
|
||||
// Webkit Fullscreen Change (Chrome/Safari)
|
||||
document.addEventListener('webkitfullscreenchange', () => {
|
||||
if (document.webkitFullscreenElement) {
|
||||
fullscreenBtn.classList.add('hidden');
|
||||
fullscreenBtn.classList.remove('reminder');
|
||||
localStorage.setItem(FULLSCREEN_STATE_KEY, 'true');
|
||||
} else {
|
||||
fullscreenBtn.classList.remove('hidden');
|
||||
localStorage.removeItem(FULLSCREEN_STATE_KEY);
|
||||
window.displayLogger?.log('Fullscreen verlassen (webkit)');
|
||||
}
|
||||
});
|
||||
|
||||
// Check beim Laden der Seite
|
||||
checkFullscreenRestore();
|
||||
|
||||
/* ==============================================
|
||||
KONFIGURATION WIRD DYNAMISCH GELADEN
|
||||
============================================== */
|
||||
|
|
|
|||
991
public/_cabinet/index_2.html
Normal file
|
|
@ -0,0 +1,991 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cabinet Digital Signage Bielefeld</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
(function() {
|
||||
const LOG_URL = 'https://cabinet.b2in.eu/logger.php';
|
||||
|
||||
// Kontext-Informationen für besseres Debugging
|
||||
let appContext = {
|
||||
currentVideo: null,
|
||||
currentFooter: null,
|
||||
videoPlaylistLength: 0,
|
||||
footerContentLength: 0,
|
||||
lastActivity: Date.now()
|
||||
};
|
||||
|
||||
// Logging-Funktion mit Kontext
|
||||
function sendLog(level, message, additionalData = {}) {
|
||||
try {
|
||||
const logData = {
|
||||
level: level,
|
||||
message: String(message),
|
||||
timestamp: new Date().toISOString(),
|
||||
context: {
|
||||
...appContext,
|
||||
...additionalData
|
||||
},
|
||||
viewport: `${window.innerWidth}x${window.innerHeight}`,
|
||||
connection: navigator.onLine ? 'online' : 'offline'
|
||||
};
|
||||
|
||||
fetch(LOG_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(logData),
|
||||
keepalive: true // Wichtig für Logs beim Verlassen der Seite
|
||||
}).catch(() => {}); // Fehler beim Loggen ignorieren
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Globale Fehler abfangen
|
||||
window.onerror = function(msg, url, line, col, error) {
|
||||
sendLog('FATAL', `JavaScript Error: ${msg}`, {
|
||||
file: url,
|
||||
line: line,
|
||||
column: col,
|
||||
stack: error?.stack
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
// Unhandled Promise Rejections (sehr wichtig für async/await!)
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
sendLog('ERROR', `Unhandled Promise Rejection: ${event.reason}`, {
|
||||
promise: event.promise?.toString()
|
||||
});
|
||||
});
|
||||
|
||||
// Console.error überschreiben
|
||||
const originalError = console.error;
|
||||
console.error = function(...args) {
|
||||
sendLog('ERROR', `Console Error: ${args.join(' ')}`);
|
||||
originalError.apply(console, args);
|
||||
};
|
||||
|
||||
// Console.warn überschreiben (für Warnungen)
|
||||
const originalWarn = console.warn;
|
||||
console.warn = function(...args) {
|
||||
sendLog('WARNING', `Console Warning: ${args.join(' ')}`);
|
||||
originalWarn.apply(console, args);
|
||||
};
|
||||
|
||||
// Resource Loading Errors (z.B. Videos, Bilder)
|
||||
window.addEventListener('error', function(event) {
|
||||
if (event.target !== window) {
|
||||
const element = event.target;
|
||||
const tagName = element.tagName;
|
||||
const src = element.src || element.href;
|
||||
|
||||
sendLog('ERROR', `Resource Failed to Load: ${tagName}`, {
|
||||
src: src,
|
||||
type: tagName
|
||||
});
|
||||
}
|
||||
}, true); // useCapture = true, um alle Events zu fangen
|
||||
|
||||
// Online/Offline Status überwachen
|
||||
window.addEventListener('online', () => {
|
||||
sendLog('INFO', 'Connection restored');
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
sendLog('WARNING', 'Connection lost');
|
||||
});
|
||||
|
||||
// Heartbeat: Alle 5 Minuten ein "alive" Signal senden
|
||||
setInterval(() => {
|
||||
sendLog('INFO', 'Heartbeat - Display is running', {
|
||||
uptime: Math.floor((Date.now() - appContext.lastActivity) / 1000) + 's'
|
||||
});
|
||||
}, 5 * 60 * 1000); // Alle 5 Minuten
|
||||
|
||||
// Initial Log beim Start
|
||||
sendLog('INFO', 'Display started', {
|
||||
userAgent: navigator.userAgent,
|
||||
screen: `${screen.width}x${screen.height}`,
|
||||
url: window.location.href
|
||||
});
|
||||
|
||||
// Export für andere Scripts
|
||||
window.displayLogger = {
|
||||
log: (msg, data) => sendLog('INFO', msg, data),
|
||||
warn: (msg, data) => sendLog('WARNING', msg, data),
|
||||
error: (msg, data) => sendLog('ERROR', msg, data),
|
||||
setContext: (key, value) => { appContext[key] = value; }
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
/* --- GRUNDGERÜST --- */
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background-color: #000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
/* Seitenverhältnis 9:16 (1080:1920) beibehalten */
|
||||
aspect-ratio: 9 / 16;
|
||||
}
|
||||
|
||||
/* Wenn Bildschirm breiter als 9:16 ist, nach Höhe skalieren */
|
||||
@media (min-aspect-ratio: 9/16) {
|
||||
#main-container {
|
||||
width: auto;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* Wenn Bildschirm höher als 9:16 ist, nach Breite skalieren */
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
#main-container {
|
||||
width: 100vw;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- VIDEO BEREICH (Oben) --- */
|
||||
#video-wrapper {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; /* Video füllt den Bereich randlos */
|
||||
object-position: center 15%; /* Fallback-Position, wird per JavaScript überschrieben */
|
||||
display: block;
|
||||
|
||||
/* Performance-Optimierungen für Video */
|
||||
will-change: transform; /* Hint für Browser-Optimierung */
|
||||
transform: translateZ(0); /* Hardware-Beschleunigung aktivieren */
|
||||
backface-visibility: hidden; /* Reduziert Rendering-Last */
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* --- FOOTER BEREICH (Unten - ca. 16.67% der Höhe = 320px bei 1920px) --- */
|
||||
#footer {
|
||||
height: 9.67vh;
|
||||
min-height: 100px;
|
||||
background-color: #1a1a1a; /* Dunkelgrau */
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px; /* 60px bei 1080px Breite */
|
||||
box-sizing: border-box;
|
||||
font-size: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Progress Bar am oberen Rand des Footers */
|
||||
#progress-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background-color: #009FE3; /* Cabinet Blau */
|
||||
width: 0%;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
#progress-bar.animate {
|
||||
animation: progressAnimation 30s linear;
|
||||
}
|
||||
|
||||
@keyframes progressAnimation {
|
||||
from {
|
||||
width: 0%;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- INHALTE IM FOOTER --- */
|
||||
.cta-text-container {
|
||||
width: 75%;
|
||||
opacity: 1;
|
||||
transition: opacity 1s ease-in-out;
|
||||
}
|
||||
|
||||
.cta-headline {
|
||||
font-size: 2.0em; /* Relativ zur Footer-Schriftgröße */
|
||||
font-weight: 300;
|
||||
margin-bottom: 0.3em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.cta-subline {
|
||||
font-size: 2.4em; /* Relativ zur Footer-Schriftgröße */
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
width: 25%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
opacity: 1;
|
||||
transition: opacity 1s ease-in-out;
|
||||
}
|
||||
|
||||
.qr-code-img {
|
||||
width: 8em; /* Relativ zur Footer-Schriftgröße */
|
||||
height: auto;
|
||||
max-width: 100px;
|
||||
aspect-ratio: 1 / 1; /* Erzwingt quadratische Form */
|
||||
object-fit: contain;
|
||||
background-color: white; /* Weißer Hintergrund für Lesbarkeit */
|
||||
padding: 0.4em;
|
||||
border-radius: 0.6em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scan-hint {
|
||||
margin-top: 0.8em;
|
||||
font-size: 1.3em; /* Relativ zur Footer-Schriftgröße */
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 600;
|
||||
color: #009FE3; /* Akzentfarbe */
|
||||
}
|
||||
|
||||
/* Hilfsklasse für den Überblend-Effekt */
|
||||
.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Loading Indicator */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Fullscreen Button */
|
||||
#fullscreen-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 1000;
|
||||
background-color: rgba(0, 159, 227, 0.8);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#fullscreen-btn:hover {
|
||||
background-color: rgba(0, 159, 227, 1);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
#fullscreen-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Button ausblenden wenn bereits im Fullscreen */
|
||||
#fullscreen-btn.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Fullscreen Reminder (nach Reload) */
|
||||
#fullscreen-btn.reminder {
|
||||
background-color: rgba(255, 152, 0, 0.95);
|
||||
animation: pulse 2s infinite;
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 12px rgba(255, 152, 0, 0.6);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- FULLSCREEN BUTTON -->
|
||||
<button id="fullscreen-btn" title="Vollbildmodus aktivieren">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle;">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
||||
</svg>
|
||||
<span style="vertical-align: middle;">V 1.3</span>
|
||||
</button>
|
||||
|
||||
<div id="main-container">
|
||||
<!-- VIDEO BEREICH -->
|
||||
<div id="video-wrapper">
|
||||
<!-- Videos werden hier geladen. 'muted' ist oft nötig für Autoplay -->
|
||||
<video id="video-player" autoplay muted playsinline></video>
|
||||
</div>
|
||||
|
||||
<!-- FOOTER BEREICH -->
|
||||
<div id="footer">
|
||||
<div id="progress-bar"></div>
|
||||
<div class="cta-text-container" id="text-area">
|
||||
<div class="cta-headline" id="headline">LADEN...</div>
|
||||
<div class="cta-subline" id="subline">Inhalte werden geladen</div>
|
||||
</div>
|
||||
|
||||
<div class="qr-container" id="qr-area">
|
||||
<img src="" id="qr-image" class="qr-code-img" alt="QR Code">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ==============================================
|
||||
FULLSCREEN BUTTON LOGIC MIT AUTO-REMINDER
|
||||
============================================== */
|
||||
|
||||
const fullscreenBtn = document.getElementById('fullscreen-btn');
|
||||
const FULLSCREEN_STATE_KEY = 'cabinet_fullscreen_was_active';
|
||||
|
||||
// Fullscreen aktivieren
|
||||
function enterFullscreen() {
|
||||
const elem = document.documentElement;
|
||||
|
||||
// Merken dass Fullscreen aktiviert wurde (für nach Reload)
|
||||
localStorage.setItem(FULLSCREEN_STATE_KEY, 'true');
|
||||
|
||||
if (elem.requestFullscreen) {
|
||||
elem.requestFullscreen();
|
||||
} else if (elem.webkitRequestFullscreen) { // Safari/Chrome
|
||||
elem.webkitRequestFullscreen();
|
||||
} else if (elem.mozRequestFullScreen) { // Firefox
|
||||
elem.mozRequestFullScreen();
|
||||
} else if (elem.msRequestFullscreen) { // IE/Edge
|
||||
elem.msRequestFullscreen();
|
||||
}
|
||||
|
||||
window.displayLogger?.log('Fullscreen aktiviert');
|
||||
}
|
||||
|
||||
// Button Event Listener
|
||||
fullscreenBtn.addEventListener('click', () => {
|
||||
enterFullscreen();
|
||||
// Reminder-Klasse entfernen falls vorhanden
|
||||
fullscreenBtn.classList.remove('reminder');
|
||||
});
|
||||
|
||||
// Prüfen ob Fullscreen vorher aktiv war (nach Reload)
|
||||
function checkFullscreenRestore() {
|
||||
const wasFullscreen = localStorage.getItem(FULLSCREEN_STATE_KEY);
|
||||
|
||||
if (wasFullscreen === 'true') {
|
||||
// Fullscreen war vorher aktiv → Auffälliger Reminder
|
||||
fullscreenBtn.classList.add('reminder');
|
||||
fullscreenBtn.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle;">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
||||
</svg>
|
||||
<span style="vertical-align: middle;">⚠️ Vollbild aktivieren!</span>
|
||||
`;
|
||||
|
||||
window.displayLogger?.warn('Fullscreen-Reminder angezeigt (war vorher aktiv)', {
|
||||
reason: 'Page reload',
|
||||
previousState: 'fullscreen'
|
||||
});
|
||||
|
||||
// Nach 30 Sekunden automatisch versuchen (falls Kiosk-Mode)
|
||||
setTimeout(() => {
|
||||
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
|
||||
window.displayLogger?.log('Versuche Auto-Fullscreen (Kiosk-Mode?)');
|
||||
enterFullscreen();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
}
|
||||
|
||||
// Button ausblenden wenn bereits im Fullscreen
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
if (document.fullscreenElement) {
|
||||
fullscreenBtn.classList.add('hidden');
|
||||
fullscreenBtn.classList.remove('reminder');
|
||||
localStorage.setItem(FULLSCREEN_STATE_KEY, 'true');
|
||||
} else {
|
||||
fullscreenBtn.classList.remove('hidden');
|
||||
// Fullscreen wurde verlassen → State clearen
|
||||
localStorage.removeItem(FULLSCREEN_STATE_KEY);
|
||||
window.displayLogger?.log('Fullscreen verlassen');
|
||||
}
|
||||
});
|
||||
|
||||
// Webkit Fullscreen Change (Chrome/Safari)
|
||||
document.addEventListener('webkitfullscreenchange', () => {
|
||||
if (document.webkitFullscreenElement) {
|
||||
fullscreenBtn.classList.add('hidden');
|
||||
fullscreenBtn.classList.remove('reminder');
|
||||
localStorage.setItem(FULLSCREEN_STATE_KEY, 'true');
|
||||
} else {
|
||||
fullscreenBtn.classList.remove('hidden');
|
||||
localStorage.removeItem(FULLSCREEN_STATE_KEY);
|
||||
window.displayLogger?.log('Fullscreen verlassen (webkit)');
|
||||
}
|
||||
});
|
||||
|
||||
// Check beim Laden der Seite
|
||||
checkFullscreenRestore();
|
||||
|
||||
/* ==============================================
|
||||
KONFIGURATION WIRD DYNAMISCH GELADEN
|
||||
============================================== */
|
||||
|
||||
let videoPlaylist = [];
|
||||
let footerContent = [];
|
||||
let footerContentLength = 0;
|
||||
|
||||
// Basis-URL für Assets und API (b2in.eu Server)
|
||||
const BASE_URL = 'https://b2in.eu';
|
||||
|
||||
// API-URL für die Konfiguration (CORS ist aktiviert für cabinet.b2in.eu)
|
||||
const API_URL = BASE_URL + '/api/display/config';
|
||||
|
||||
/* ==============================================
|
||||
KONFIGURATION LADEN
|
||||
============================================== */
|
||||
|
||||
async function loadConfiguration() {
|
||||
try {
|
||||
window.displayLogger?.log('Lade Konfiguration...', { url: API_URL });
|
||||
|
||||
const response = await fetch(API_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const config = await response.json();
|
||||
videoPlaylist = config.videoPlaylist || [];
|
||||
footerContent = config.footerContent || [];
|
||||
|
||||
window.displayLogger?.setContext('videoPlaylistLength', videoPlaylist.length);
|
||||
window.displayLogger?.setContext('footerContentLength', footerContent.length);
|
||||
window.displayLogger?.log('Konfiguration erfolgreich geladen', {
|
||||
videos: videoPlaylist.length,
|
||||
footerItems: footerContent.length
|
||||
});
|
||||
|
||||
console.log('Konfiguration geladen:', config);
|
||||
|
||||
// Überprüfe, ob Videos vorhanden sind
|
||||
if (videoPlaylist.length === 0) {
|
||||
console.warn('Keine Videos in der Playlist vorhanden');
|
||||
window.displayLogger?.warn('Keine Videos in Playlist');
|
||||
document.getElementById('headline').innerText = 'KEINE VIDEOS';
|
||||
document.getElementById('subline').innerText = 'Bitte fügen Sie Videos im CMS hinzu';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Überprüfe, ob Footer-Inhalte vorhanden sind
|
||||
if (footerContent.length === 0) {
|
||||
console.warn('Keine Footer-Inhalte vorhanden - Footer wird ausgeblendet');
|
||||
footerContentLength = 0;
|
||||
// Footer ausblenden
|
||||
const footer = document.getElementById('footer');
|
||||
if (footer) {
|
||||
footer.style.display = 'none';
|
||||
}
|
||||
// Video-Wrapper auf 100% Höhe setzen
|
||||
const videoWrapper = document.getElementById('video-wrapper');
|
||||
if (videoWrapper) {
|
||||
videoWrapper.style.flexGrow = '1';
|
||||
videoWrapper.style.height = '100%';
|
||||
}
|
||||
} else {
|
||||
// Footer anzeigen, falls er zuvor ausgeblendet wurde
|
||||
const footer = document.getElementById('footer');
|
||||
if (footer) {
|
||||
footer.style.display = 'flex';
|
||||
}
|
||||
footerContentLength = 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Konfiguration:', error);
|
||||
window.displayLogger?.error('Konfiguration konnte nicht geladen werden', {
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
document.getElementById('headline').innerText = 'FEHLER';
|
||||
document.getElementById('subline').innerText = 'Konfiguration konnte nicht geladen werden';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
PROGRAMM-LOGIK
|
||||
============================================== */
|
||||
|
||||
// --- ROBUSTER VIDEO PLAYER MIT MEMORY-MANAGEMENT ---
|
||||
const videoElement = document.getElementById('video-player');
|
||||
let currentVideoIndex = 0;
|
||||
let videoStartTimeout = null;
|
||||
let videoWatchdogInterval = null;
|
||||
let lastVideoTime = 0;
|
||||
let videoStuckCount = 0;
|
||||
let consecutiveErrors = 0;
|
||||
const MAX_CONSECUTIVE_ERRORS = 3;
|
||||
const VIDEO_START_TIMEOUT = 10000; // 10 Sekunden
|
||||
const VIDEO_WATCHDOG_INTERVAL = 5000; // Alle 5 Sekunden prüfen
|
||||
|
||||
// Video-Element optimieren für Memory-Management
|
||||
videoElement.setAttribute('preload', 'metadata'); // Nur Metadaten vorladen, nicht ganzes Video
|
||||
|
||||
function cleanupVideo() {
|
||||
// Wichtig: Stoppt Video und gibt Speicher frei
|
||||
try {
|
||||
videoElement.pause();
|
||||
videoElement.removeAttribute('src');
|
||||
videoElement.load(); // Triggert Garbage Collection des alten Videos
|
||||
|
||||
// Timeouts clearen
|
||||
if (videoStartTimeout) {
|
||||
clearTimeout(videoStartTimeout);
|
||||
videoStartTimeout = null;
|
||||
}
|
||||
|
||||
window.displayLogger?.log('Video cleanup durchgeführt');
|
||||
} catch (e) {
|
||||
window.displayLogger?.error('Video cleanup error', { error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
function playNextVideo() {
|
||||
if (videoPlaylist.length === 0) return;
|
||||
|
||||
// Watchdog zurücksetzen
|
||||
lastVideoTime = 0;
|
||||
videoStuckCount = 0;
|
||||
|
||||
const video = videoPlaylist[currentVideoIndex];
|
||||
const videoSrc = BASE_URL + "/_cabinet/" + video.src;
|
||||
|
||||
// Kontext aktualisieren
|
||||
window.displayLogger?.setContext('currentVideo', video.src);
|
||||
window.displayLogger?.setContext('currentVideoIndex', currentVideoIndex);
|
||||
|
||||
// WICHTIG: Altes Video cleanup BEVOR neues geladen wird
|
||||
cleanupVideo();
|
||||
|
||||
// Kleiner Delay um Cleanup abzuschließen
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Neues Video laden
|
||||
videoElement.src = videoSrc;
|
||||
|
||||
if(footerContentLength !== 0 && video.position !== undefined) {
|
||||
videoElement.style.objectPosition = `center ${video.position}%`;
|
||||
}
|
||||
|
||||
// Timeout für Video-Start
|
||||
videoStartTimeout = setTimeout(() => {
|
||||
window.displayLogger?.error('Video start timeout', {
|
||||
video: video.src,
|
||||
timeout: VIDEO_START_TIMEOUT
|
||||
});
|
||||
// Nächstes Video probieren
|
||||
skipToNextVideo('timeout');
|
||||
}, VIDEO_START_TIMEOUT);
|
||||
|
||||
// Video abspielen
|
||||
videoElement.play()
|
||||
.then(() => {
|
||||
window.displayLogger?.log(`Video started: ${video.src}`);
|
||||
consecutiveErrors = 0; // Erfolg → Error-Counter zurücksetzen
|
||||
|
||||
// Start-Timeout clearen
|
||||
if (videoStartTimeout) {
|
||||
clearTimeout(videoStartTimeout);
|
||||
videoStartTimeout = null;
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.log("Autoplay blocked/failed", e);
|
||||
window.displayLogger?.error(`Video play failed: ${video.src}`, {
|
||||
error: e.message
|
||||
});
|
||||
|
||||
consecutiveErrors++;
|
||||
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
||||
window.displayLogger?.error('Zu viele aufeinanderfolgende Fehler', {
|
||||
count: consecutiveErrors
|
||||
});
|
||||
// Seite nach 30 Sekunden neu laden
|
||||
setTimeout(() => location.reload(), 30000);
|
||||
} else {
|
||||
// Nächstes Video probieren
|
||||
skipToNextVideo('play_failed');
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
window.displayLogger?.error('Exception beim Video-Laden', {
|
||||
error: e.message,
|
||||
stack: e.stack
|
||||
});
|
||||
skipToNextVideo('exception');
|
||||
}
|
||||
}, 100); // 100ms Delay für Cleanup
|
||||
|
||||
// Index weiterschalten
|
||||
currentVideoIndex++;
|
||||
if (currentVideoIndex >= videoPlaylist.length) {
|
||||
currentVideoIndex = 0;
|
||||
// Playlist-Loop abgeschlossen → Log für Monitoring
|
||||
window.displayLogger?.log('Playlist-Loop abgeschlossen, starte von vorne');
|
||||
}
|
||||
}
|
||||
|
||||
function skipToNextVideo(reason) {
|
||||
window.displayLogger?.warn('Überspringe zum nächsten Video', { reason: reason });
|
||||
playNextVideo();
|
||||
}
|
||||
|
||||
// Video Watchdog: Prüft ob Video wirklich läuft
|
||||
function startVideoWatchdog() {
|
||||
if (videoWatchdogInterval) {
|
||||
clearInterval(videoWatchdogInterval);
|
||||
}
|
||||
|
||||
videoWatchdogInterval = setInterval(() => {
|
||||
if (videoPlaylist.length === 0) return;
|
||||
|
||||
const currentTime = videoElement.currentTime;
|
||||
const isPaused = videoElement.paused;
|
||||
const hasEnded = videoElement.ended;
|
||||
const isStuck = (currentTime === lastVideoTime && !isPaused && !hasEnded);
|
||||
|
||||
// Debug-Log
|
||||
if (isStuck) {
|
||||
videoStuckCount++;
|
||||
window.displayLogger?.warn('Video scheint stecken geblieben zu sein', {
|
||||
currentTime: currentTime,
|
||||
isPaused: isPaused,
|
||||
hasEnded: hasEnded,
|
||||
stuckCount: videoStuckCount,
|
||||
src: videoElement.src
|
||||
});
|
||||
|
||||
// Wenn 2x hintereinander stuck → Recovery
|
||||
if (videoStuckCount >= 2) {
|
||||
window.displayLogger?.error('Video definitiv stuck - starte nächstes', {
|
||||
currentTime: currentTime,
|
||||
src: videoElement.src
|
||||
});
|
||||
skipToNextVideo('watchdog_stuck');
|
||||
}
|
||||
} else {
|
||||
// Video läuft normal → Counter zurücksetzen
|
||||
if (videoStuckCount > 0) {
|
||||
window.displayLogger?.log('Video läuft wieder normal');
|
||||
}
|
||||
videoStuckCount = 0;
|
||||
}
|
||||
|
||||
lastVideoTime = currentTime;
|
||||
}, VIDEO_WATCHDOG_INTERVAL);
|
||||
}
|
||||
|
||||
// Video Events
|
||||
videoElement.addEventListener('ended', () => {
|
||||
window.displayLogger?.log('Video ended', {
|
||||
src: videoElement.src
|
||||
});
|
||||
playNextVideo();
|
||||
});
|
||||
|
||||
videoElement.addEventListener('error', (e) => {
|
||||
const error = videoElement.error;
|
||||
const errorCode = error?.code;
|
||||
const errorMessage = {
|
||||
1: 'MEDIA_ERR_ABORTED',
|
||||
2: 'MEDIA_ERR_NETWORK',
|
||||
3: 'MEDIA_ERR_DECODE',
|
||||
4: 'MEDIA_ERR_SRC_NOT_SUPPORTED'
|
||||
}[errorCode] || 'UNKNOWN';
|
||||
|
||||
window.displayLogger?.error('Video Error Event', {
|
||||
code: errorCode,
|
||||
message: error?.message,
|
||||
src: videoElement.src,
|
||||
mediaError: errorMessage
|
||||
});
|
||||
|
||||
// Bei Fehler → Nächstes Video
|
||||
consecutiveErrors++;
|
||||
skipToNextVideo(`error_${errorMessage}`);
|
||||
});
|
||||
|
||||
videoElement.addEventListener('stalled', () => {
|
||||
window.displayLogger?.warn('Video stalled (buffering)', {
|
||||
src: videoElement.src,
|
||||
currentTime: videoElement.currentTime
|
||||
});
|
||||
});
|
||||
|
||||
videoElement.addEventListener('waiting', () => {
|
||||
window.displayLogger?.warn('Video waiting (buffering)', {
|
||||
src: videoElement.src,
|
||||
currentTime: videoElement.currentTime
|
||||
});
|
||||
});
|
||||
|
||||
videoElement.addEventListener('playing', () => {
|
||||
window.displayLogger?.log('Video playing event', {
|
||||
src: videoElement.src,
|
||||
currentTime: videoElement.currentTime
|
||||
});
|
||||
});
|
||||
|
||||
videoElement.addEventListener('canplay', () => {
|
||||
window.displayLogger?.log('Video canplay event', {
|
||||
src: videoElement.src
|
||||
});
|
||||
});
|
||||
|
||||
// --- FOOTER ROTATION LOGIC ---
|
||||
let currentFooterIndex = 0;
|
||||
const textArea = document.getElementById('text-area');
|
||||
const qrArea = document.getElementById('qr-area');
|
||||
const headlineEl = document.getElementById('headline');
|
||||
const sublineEl = document.getElementById('subline');
|
||||
const qrImageEl = document.getElementById('qr-image');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
|
||||
function restartProgressBar() {
|
||||
// Animation zurücksetzen und neu starten
|
||||
progressBar.classList.remove('animate');
|
||||
void progressBar.offsetWidth; // Force reflow
|
||||
progressBar.classList.add('animate');
|
||||
}
|
||||
|
||||
function updateFooter() {
|
||||
if (footerContent.length === 0) return;
|
||||
|
||||
// 1. Ausblenden
|
||||
textArea.classList.add('fade-out');
|
||||
qrArea.classList.add('fade-out');
|
||||
|
||||
// Progress Bar neu starten
|
||||
restartProgressBar();
|
||||
|
||||
// 2. Warten, Inhalt tauschen, Einblenden
|
||||
setTimeout(() => {
|
||||
const content = footerContent[currentFooterIndex];
|
||||
|
||||
// Kontext aktualisieren
|
||||
window.displayLogger?.setContext('currentFooter', currentFooterIndex);
|
||||
|
||||
// Text setzen
|
||||
headlineEl.innerText = content.headline;
|
||||
sublineEl.innerText = content.subline;
|
||||
|
||||
// QR Code nur generieren wenn URL vorhanden
|
||||
if (content.url) {
|
||||
// QR Code generieren (API Aufruf)
|
||||
const qrSize = "300x300";
|
||||
const qrColor = "000000"; // Schwarz
|
||||
const qrBg = "ffffff"; // Weiß
|
||||
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${qrSize}&color=${qrColor}&bgcolor=${qrBg}&margin=10&data=${encodeURIComponent(content.url)}`;
|
||||
|
||||
qrImageEl.src = qrUrl;
|
||||
qrArea.style.display = 'flex'; // QR-Bereich anzeigen
|
||||
} else {
|
||||
// Kein QR-Code - QR-Bereich ausblenden
|
||||
qrArea.style.display = 'none';
|
||||
// Text-Container auf volle Breite
|
||||
textArea.style.width = '100%';
|
||||
}
|
||||
|
||||
// Index weiterschalten
|
||||
currentFooterIndex++;
|
||||
if (currentFooterIndex >= footerContent.length) {
|
||||
currentFooterIndex = 0;
|
||||
}
|
||||
|
||||
// 3. Einblenden
|
||||
if (content.url) {
|
||||
qrImageEl.onload = () => {
|
||||
textArea.classList.remove('fade-out');
|
||||
qrArea.classList.remove('fade-out');
|
||||
};
|
||||
}
|
||||
// Fallback falls Bild sofort da ist (Cache) oder kein QR-Code
|
||||
setTimeout(() => {
|
||||
textArea.classList.remove('fade-out');
|
||||
if (content.url) {
|
||||
qrArea.classList.remove('fade-out');
|
||||
}
|
||||
}, 100);
|
||||
|
||||
}, 1000); // 1 Sekunde für Fade-Out Animation
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
MEMORY MANAGEMENT & PERFORMANCE
|
||||
============================================== */
|
||||
|
||||
// Memory-Optimierung: Regelmäßig Browser aufräumen
|
||||
function performMemoryOptimization() {
|
||||
try {
|
||||
// Performance-Metriken loggen falls verfügbar
|
||||
if (performance.memory) {
|
||||
const memUsed = Math.round(performance.memory.usedJSHeapSize / 1048576);
|
||||
const memLimit = Math.round(performance.memory.jsHeapSizeLimit / 1048576);
|
||||
const memPercent = Math.round((memUsed / memLimit) * 100);
|
||||
|
||||
window.displayLogger?.log('Memory Status', {
|
||||
usedMB: memUsed,
|
||||
limitMB: memLimit,
|
||||
percentUsed: memPercent
|
||||
});
|
||||
|
||||
// Warnung wenn Speicher über 80%
|
||||
if (memPercent > 80) {
|
||||
window.displayLogger?.warn('Hohe Speicherauslastung', {
|
||||
percentUsed: memPercent,
|
||||
usedMB: memUsed
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Cache-Infos loggen
|
||||
const cacheInfo = {
|
||||
videoBuffered: videoElement.buffered.length,
|
||||
videoDuration: videoElement.duration,
|
||||
videoReadyState: videoElement.readyState
|
||||
};
|
||||
|
||||
window.displayLogger?.log('Video Cache Status', cacheInfo);
|
||||
|
||||
} catch (e) {
|
||||
window.displayLogger?.error('Memory optimization error', {
|
||||
error: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Automatischer Page-Reload bei kritischen Problemen (Failsafe)
|
||||
let criticalErrorCount = 0;
|
||||
function checkCriticalErrors() {
|
||||
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
||||
criticalErrorCount++;
|
||||
window.displayLogger?.error('Kritischer Zustand erkannt', {
|
||||
consecutiveErrors: consecutiveErrors,
|
||||
criticalErrorCount: criticalErrorCount
|
||||
});
|
||||
|
||||
if (criticalErrorCount >= 3) {
|
||||
window.displayLogger?.error('Zu viele kritische Fehler - Seite wird neu geladen');
|
||||
setTimeout(() => location.reload(), 5000);
|
||||
}
|
||||
} else {
|
||||
criticalErrorCount = 0; // Zurücksetzen wenn alles normal läuft
|
||||
}
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
INITIALISIERUNG
|
||||
============================================== */
|
||||
|
||||
async function initialize() {
|
||||
const success = await loadConfiguration();
|
||||
|
||||
if (success && videoPlaylist.length > 0) {
|
||||
// Start Video
|
||||
playNextVideo();
|
||||
|
||||
// Start Video Watchdog (überwacht ob Videos laufen)
|
||||
startVideoWatchdog();
|
||||
window.displayLogger?.log('Video Watchdog gestartet');
|
||||
|
||||
// Start Footer Loop
|
||||
if (footerContent.length > 0) {
|
||||
updateFooter();
|
||||
setInterval(updateFooter, 30000); // Alle 30.000 ms (30 sek) wechseln
|
||||
|
||||
// Progress Bar initial starten
|
||||
restartProgressBar();
|
||||
}
|
||||
|
||||
// Memory-Optimierung alle 10 Minuten
|
||||
setInterval(performMemoryOptimization, 10 * 60 * 1000);
|
||||
window.displayLogger?.log('Memory Optimizer gestartet (alle 10 Min)');
|
||||
|
||||
// Critical Error Check alle 30 Sekunden
|
||||
setInterval(checkCriticalErrors, 30 * 1000);
|
||||
|
||||
// Initial Memory Check nach 30 Sekunden
|
||||
setTimeout(performMemoryOptimization, 30000);
|
||||
}
|
||||
}
|
||||
|
||||
// Beim Laden der Seite initialisieren
|
||||
initialize();
|
||||
|
||||
// Auto-Reload alle 5 Minuten, um neue Inhalte zu laden
|
||||
setInterval(async () => {
|
||||
console.log('Prüfe auf neue Konfiguration...');
|
||||
const oldFooterCount = footerContent.length;
|
||||
await loadConfiguration();
|
||||
|
||||
// Wenn Footer-Inhalte hinzugefügt oder entfernt wurden, Seite neu laden
|
||||
if ((oldFooterCount === 0 && footerContent.length > 0) ||
|
||||
(oldFooterCount > 0 && footerContent.length === 0)) {
|
||||
console.log('Footer-Status hat sich geändert - Seite wird neu geladen');
|
||||
location.reload();
|
||||
}
|
||||
}, 5 * 60 * 1000); // 5 Minuten
|
||||
|
||||
// Präventiver Page-Reload alle 6 Stunden (verhindert Memory-Leaks über lange Zeit)
|
||||
setTimeout(() => {
|
||||
window.displayLogger?.log('Präventiver Reload nach 6 Stunden');
|
||||
location.reload();
|
||||
}, 6 * 60 * 60 * 1000); // 6 Stunden
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
public/_cabinet/logo-cabinet-300.png
Normal file
|
After Width: | Height: | Size: 359 KiB |
BIN
public/_cabinet/logo-cabinet.png.webp
Normal file
|
After Width: | Height: | Size: 136 KiB |
494
public/_cabinet/offer.html
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>CABINET Display – 9:16</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#ffffff;
|
||||
--fg:#111111;
|
||||
--muted:#6b6b6b;
|
||||
--line:#e9e9e9;
|
||||
--card:#f7f7f7;
|
||||
--radius:28px;
|
||||
--pad:64px; /* Safe-Area */
|
||||
--maxw:1080px;
|
||||
--maxh:1920px;
|
||||
--shadow:0 18px 50px rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
margin:0;
|
||||
background:#0f0f10;
|
||||
color:var(--fg);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding:24px;
|
||||
}
|
||||
|
||||
/* 9:16 Display-Rahmen */
|
||||
.screen{
|
||||
width:min(92vw, var(--maxw));
|
||||
aspect-ratio: 9 / 16;
|
||||
background:var(--bg);
|
||||
border-radius:var(--radius);
|
||||
box-shadow:var(--shadow);
|
||||
overflow:hidden;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
/* Slides */
|
||||
.slide{
|
||||
position:absolute;
|
||||
inset:0;
|
||||
padding:var(--pad);
|
||||
display:grid;
|
||||
grid-template-rows: 156px 1fr 360px; /* Header / Hero / Info+Footer */
|
||||
gap:24px;
|
||||
opacity:0;
|
||||
transform:scale(1.01);
|
||||
transition:opacity .6s ease, transform .6s ease;
|
||||
}
|
||||
.slide.active{
|
||||
opacity:1;
|
||||
transform:scale(1);
|
||||
z-index:2;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header{
|
||||
display:flex;
|
||||
align-items:flex-end;
|
||||
justify-content:space-between;
|
||||
padding-bottom:18px;
|
||||
border-bottom:1px solid var(--line);
|
||||
}
|
||||
.brand{
|
||||
font-size:34px;
|
||||
letter-spacing:.08em;
|
||||
text-transform:uppercase;
|
||||
font-weight:600;
|
||||
}
|
||||
.tagline{
|
||||
font-size:26px;
|
||||
color:var(--muted);
|
||||
font-weight:400;
|
||||
text-align:right;
|
||||
line-height:1.2;
|
||||
}
|
||||
|
||||
/* Hero Bildbereich (Platzhalter, später durch echtes Bild ersetzen) */
|
||||
.hero{
|
||||
border-radius:24px;
|
||||
background:
|
||||
radial-gradient(1200px 800px at 20% 15%, rgba(0,0,0,.06), rgba(0,0,0,0) 55%),
|
||||
linear-gradient(135deg, #f2f2f2, #fbfbfb);
|
||||
border:1px solid var(--line);
|
||||
overflow:hidden;
|
||||
position:relative;
|
||||
display:flex;
|
||||
align-items:flex-end;
|
||||
padding:36px;
|
||||
}
|
||||
.hero .hero-note{
|
||||
font-size:22px;
|
||||
color:rgba(0,0,0,.55);
|
||||
background:rgba(255,255,255,.75);
|
||||
border:1px solid var(--line);
|
||||
border-radius:999px;
|
||||
padding:10px 16px;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
/* Optional: wenn ihr ein echtes Bild nutzen wollt */
|
||||
.hero.has-image{
|
||||
background-size:cover;
|
||||
background-position:center;
|
||||
}
|
||||
|
||||
/* Info + Footer */
|
||||
.bottom{
|
||||
display:grid;
|
||||
grid-template-columns: 1fr 320px; /* Textblock / QR */
|
||||
gap:28px;
|
||||
align-items:stretch;
|
||||
}
|
||||
|
||||
.info{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
justify-content:space-between;
|
||||
border-radius:24px;
|
||||
border:1px solid var(--line);
|
||||
background:linear-gradient(180deg, #ffffff, #fafafa);
|
||||
padding:28px;
|
||||
min-height:360px;
|
||||
}
|
||||
|
||||
.eyebrow{
|
||||
font-size:22px;
|
||||
color:var(--muted);
|
||||
letter-spacing:.08em;
|
||||
text-transform:uppercase;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
.title{
|
||||
font-size:58px;
|
||||
line-height:1.05;
|
||||
font-weight:650;
|
||||
margin:0 0 10px 0;
|
||||
}
|
||||
.subline{
|
||||
font-size:28px;
|
||||
color:var(--muted);
|
||||
margin:0 0 16px 0;
|
||||
}
|
||||
|
||||
.price-row{
|
||||
display:flex;
|
||||
align-items:baseline;
|
||||
justify-content:space-between;
|
||||
gap:16px;
|
||||
margin-top:10px;
|
||||
padding-top:16px;
|
||||
border-top:1px solid var(--line);
|
||||
}
|
||||
.price{
|
||||
font-size:86px;
|
||||
font-weight:700;
|
||||
letter-spacing:-.02em;
|
||||
margin:0;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.uvp{
|
||||
font-size:24px;
|
||||
color:var(--muted);
|
||||
text-align:right;
|
||||
line-height:1.15;
|
||||
}
|
||||
|
||||
.bullets{
|
||||
margin:18px 0 0 0;
|
||||
padding:0;
|
||||
list-style:none;
|
||||
display:grid;
|
||||
gap:10px;
|
||||
}
|
||||
.bullets li{
|
||||
font-size:30px;
|
||||
line-height:1.25;
|
||||
display:flex;
|
||||
gap:12px;
|
||||
align-items:flex-start;
|
||||
}
|
||||
.dot{
|
||||
width:10px;height:10px;border-radius:50%;
|
||||
background:#111;
|
||||
margin-top:14px;
|
||||
flex:0 0 auto;
|
||||
opacity:.8;
|
||||
}
|
||||
|
||||
/* Footer CTA + QR */
|
||||
.qr{
|
||||
border-radius:24px;
|
||||
border:1px solid var(--line);
|
||||
background:var(--card);
|
||||
padding:18px;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
justify-content:space-between;
|
||||
gap:14px;
|
||||
}
|
||||
.qr .cta{
|
||||
font-size:26px;
|
||||
line-height:1.2;
|
||||
color:var(--fg);
|
||||
font-weight:600;
|
||||
}
|
||||
.qr .contact{
|
||||
font-size:22px;
|
||||
color:var(--muted);
|
||||
line-height:1.2;
|
||||
}
|
||||
.qr .code{
|
||||
flex:1;
|
||||
border-radius:18px;
|
||||
background:#fff;
|
||||
border:1px dashed #d8d8d8;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
overflow:hidden;
|
||||
}
|
||||
/* Einfacher QR-Platzhalter (kein echter QR) */
|
||||
.fake-qr{
|
||||
width:88%;
|
||||
aspect-ratio:1/1;
|
||||
background:
|
||||
linear-gradient(90deg, #000 10px, transparent 10px) 0 0 / 26px 26px,
|
||||
linear-gradient(#000 10px, transparent 10px) 0 0 / 26px 26px;
|
||||
opacity:.13;
|
||||
position:relative;
|
||||
}
|
||||
.fake-qr::before,.fake-qr::after{
|
||||
content:"";
|
||||
position:absolute;
|
||||
inset:0;
|
||||
background:
|
||||
radial-gradient(circle at 18% 18%, #000 0 28%, transparent 29% 100%),
|
||||
radial-gradient(circle at 82% 18%, #000 0 28%, transparent 29% 100%),
|
||||
radial-gradient(circle at 18% 82%, #000 0 28%, transparent 29% 100%);
|
||||
opacity:.28;
|
||||
mix-blend-mode:multiply;
|
||||
}
|
||||
|
||||
.disclaimer{
|
||||
font-size:18px;
|
||||
color:var(--muted);
|
||||
margin-top:10px;
|
||||
}
|
||||
|
||||
/* Kleiner Slide-Indikator (optional) */
|
||||
.progress{
|
||||
position:absolute;
|
||||
left:0; right:0; bottom:0;
|
||||
height:6px;
|
||||
background:rgba(255,255,255,.0);
|
||||
}
|
||||
.bar{
|
||||
height:100%;
|
||||
width:0%;
|
||||
background:#111;
|
||||
opacity:.22;
|
||||
transition:width linear;
|
||||
}
|
||||
|
||||
@media (max-width: 560px){
|
||||
:root{ --pad:28px; }
|
||||
.slide{ grid-template-rows: 120px 1fr 380px; }
|
||||
.bottom{ grid-template-columns: 1fr; }
|
||||
.qr{ min-height:240px; }
|
||||
.price{ font-size:72px; }
|
||||
.title{ font-size:46px; }
|
||||
.bullets li{ font-size:24px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="screen" aria-label="9:16 Display">
|
||||
<!-- SLIDE 0: Intro -->
|
||||
<section class="slide active" data-duration="8000" id="slide-0">
|
||||
<header class="header">
|
||||
<div class="brand">CABINET Bielefeld</div>
|
||||
<div class="tagline">Planung • Beratung<br>Lieferung/Montage</div>
|
||||
</header>
|
||||
|
||||
<div class="hero">
|
||||
<div class="hero-note">Einzelstücke & Ausstellungsdeals – nur solange verfügbar</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="info">
|
||||
<div>
|
||||
<div class="eyebrow">Heute im Fokus</div>
|
||||
<h1 class="title" style="margin-bottom:12px;">Kuratiert. Hochwertig. Sofort.</h1>
|
||||
<p class="subline">Scannen Sie den QR-Code für Kontakt & Store-Infos.</p>
|
||||
</div>
|
||||
<div class="price-row" style="border-top:none; padding-top:0; margin-top:0;">
|
||||
<div style="font-size:26px;color:var(--muted);">
|
||||
CABINET Bielefeld – im Store ansprechen oder direkt reservieren
|
||||
</div>
|
||||
<div style="font-size:22px;color:var(--muted);text-align:right;">
|
||||
Zwischenverkauf vorbehalten
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="qr">
|
||||
<div>
|
||||
<div class="cta">Kontakt & Store-Infos</div>
|
||||
<div class="contact">QR scannen</div>
|
||||
</div>
|
||||
<div class="code" aria-label="QR Code Platzhalter">
|
||||
<div class="fake-qr"></div>
|
||||
</div>
|
||||
<div class="contact">WhatsApp: … · Tel.: …</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SLIDE 1: GOYA – Variante B -->
|
||||
<section class="slide" data-duration="10000" id="slide-1">
|
||||
<header class="header">
|
||||
<div class="brand">CABINET</div>
|
||||
<div class="tagline">Ausstellungsware<br>Einzelstück</div>
|
||||
</header>
|
||||
|
||||
<div class="hero">
|
||||
<div class="hero-note">Bildplatzhalter: GOYA Sideboard (Hero-Foto)</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="info">
|
||||
<div>
|
||||
<div class="eyebrow">Ausstellungsware</div>
|
||||
<h2 class="title">GOYA Sideboard</h2>
|
||||
<p class="subline">Marke: Sudbrock</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="price-row">
|
||||
<p class="price">489 €</p>
|
||||
<div class="uvp">
|
||||
brutto<br>
|
||||
UVP neu: 4.744 €*
|
||||
</div>
|
||||
</div>
|
||||
<div class="disclaimer">*UVP nur, sofern belegbar. Zwischenverkauf vorbehalten.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="qr">
|
||||
<div>
|
||||
<div class="cta">Infos & Reservierung</div>
|
||||
<div class="contact">QR scannen</div>
|
||||
</div>
|
||||
<div class="code">
|
||||
<div class="fake-qr"></div>
|
||||
</div>
|
||||
<div class="contact">WhatsApp: … · Tel.: …</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SLIDE 2: GOYA – Variante B -->
|
||||
<section class="slide" data-duration="12000" id="slide-2">
|
||||
<header class="header">
|
||||
<div class="brand">CABINET</div>
|
||||
<div class="tagline">GOYA<br>Konditionen</div>
|
||||
</header>
|
||||
|
||||
<div class="hero">
|
||||
<div class="hero-note">Bildplatzhalter: GOYA Detail/2. Ansicht</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="info">
|
||||
<div>
|
||||
<div class="eyebrow">GOYA | Konditionen</div>
|
||||
<h2 class="title" style="font-size:52px;">Details auf einen Blick</h2>
|
||||
<ul class="bullets">
|
||||
<li><span class="dot"></span><span>Eingelagertes Einzelstück</span></li>
|
||||
<li><span class="dot"></span><span>Abholung: Lager Rheda-Wiedenbrück</span></li>
|
||||
<li><span class="dot"></span><span>Lieferung/Montage optional</span></li>
|
||||
<li><span class="dot"></span><span>Preis gilt ohne Lieferung/Montage</span></li>
|
||||
<li><span class="dot"></span><span>Deckel: Weiß matt (erneuert)</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="price-row">
|
||||
<div style="font-size:26px;color:var(--muted);">
|
||||
Details & Reservierung
|
||||
</div>
|
||||
<div style="font-size:26px;font-weight:650;">
|
||||
QR scannen
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="qr">
|
||||
<div>
|
||||
<div class="cta">Details: QR scannen</div>
|
||||
<div class="contact">Reservierung & Kontakt</div>
|
||||
</div>
|
||||
<div class="code">
|
||||
<div class="fake-qr"></div>
|
||||
</div>
|
||||
<div class="contact">Zwischenverkauf vorbehalten</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SLIDE 3: TANDO – Variante C -->
|
||||
<section class="slide" data-duration="10000" id="slide-3">
|
||||
<header class="header">
|
||||
<div class="brand">CABINET</div>
|
||||
<div class="tagline">Nur 1× im Store<br>Sofort</div>
|
||||
</header>
|
||||
|
||||
<div class="hero">
|
||||
<div class="hero-note">Bildplatzhalter: TANDO Spiegel (Foto im Laden)</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="info">
|
||||
<div>
|
||||
<div class="eyebrow">Nur 1× im Store</div>
|
||||
<h2 class="title">TANDO Spiegel</h2>
|
||||
<p class="subline">Ansehen & mitnehmen – heute</p>
|
||||
</div>
|
||||
|
||||
<div class="price-row">
|
||||
<p class="price">199 €</p>
|
||||
<div class="uvp">
|
||||
brutto<br>
|
||||
Jetzt sichern
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="qr">
|
||||
<div>
|
||||
<div class="cta">Jetzt sichern</div>
|
||||
<div class="contact">QR scannen oder Team ansprechen</div>
|
||||
</div>
|
||||
<div class="code">
|
||||
<div class="fake-qr"></div>
|
||||
</div>
|
||||
<div class="contact">WhatsApp: … · Tel.: …</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="progress" aria-hidden="true">
|
||||
<div class="bar" id="bar"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Simple Slide-Rotation
|
||||
const slides = Array.from(document.querySelectorAll(".slide"));
|
||||
const bar = document.getElementById("bar");
|
||||
|
||||
let idx = 0;
|
||||
let timer = null;
|
||||
|
||||
function show(i){
|
||||
slides.forEach((s, n) => s.classList.toggle("active", n === i));
|
||||
const dur = Number(slides[i].dataset.duration || 9000);
|
||||
|
||||
// progress animation
|
||||
bar.style.transition = "none";
|
||||
bar.style.width = "0%";
|
||||
// force reflow
|
||||
void bar.offsetWidth;
|
||||
bar.style.transition = `width ${dur}ms linear`;
|
||||
bar.style.width = "100%";
|
||||
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
idx = (idx + 1) % slides.length;
|
||||
show(idx);
|
||||
}, dur);
|
||||
}
|
||||
|
||||
show(idx);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
149
public/_cabinet/offer.md
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
Kurzbriefing für Entwickler: 9:16 HTML-Display (CABINET Bielefeld) – Produktloop
|
||||
|
||||
Ziel
|
||||
Auf dem vorhandenen 9:16-Display (HTML/Webseite) sollen zusätzlich zu CABINET-Werbevideos produktbezogene Angebots-Slides laufen. Fokus: Abverkauf von 2 Produkten (GOYA + TANDO) in einem hochwertigen, ruhigen CABINET-Look (clean, viel Weißraum, klare Typo).
|
||||
|
||||
⸻
|
||||
|
||||
1) Format & technische Vorgaben
|
||||
• Format: 9:16 Hochkant, 1080×1920 px (responsive skalierbar).
|
||||
• Safe-Area: 64 px Rand innen (keine wichtigen Inhalte außerhalb).
|
||||
• Loop: 4 Slides (Slide 0–3) als Rotation.
|
||||
• Timing:
|
||||
• Slide 0 (Intro): 8s
|
||||
• Slide 1 (GOYA Preis/Hero): 10s
|
||||
• Slide 2 (GOYA Details/Konditionen): 12s
|
||||
• Slide 3 (TANDO Preis/Hero): 10s
|
||||
• anschließend wieder Slide 0
|
||||
• Transition: weiche Fades (0,5–0,7s), keine harten Effekte.
|
||||
• Assets: pro Slide 1 Hintergrundbild (Hero) + optional QR als PNG/SVG.
|
||||
|
||||
⸻
|
||||
|
||||
2) Layout-Konzept (einheitlich für alle Slides)
|
||||
|
||||
Seitenaufbau (von oben nach unten):
|
||||
1. Header: Marke + kurze Kontextzeile (z. B. „Ausstellungsware / Einzelstück“)
|
||||
2. Hero: großes Bild (clean, hochwertig)
|
||||
3. Bottom Area:
|
||||
• links: Text-/Preisblock (Produktname, Subline, Preis, ggf. UVP/Bullets)
|
||||
• rechts: QR-Box (CTA + QR + Kontaktzeile)
|
||||
|
||||
Wiederkehrende Elemente
|
||||
• QR-Box immer rechts unten gleich platziert.
|
||||
• CTA-Texte kurz und klar (z. B. „Infos & Reservierung: QR“).
|
||||
• Disclaimer klein: „Zwischenverkauf vorbehalten.“
|
||||
|
||||
⸻
|
||||
|
||||
3) Inhalte pro Slide (finale Texte)
|
||||
|
||||
Slide 0 – Intro
|
||||
|
||||
Header links: CABINET Bielefeld
|
||||
Header rechts: Planung • Beratung (Zeilenumbruch möglich) + Lieferung/Montage
|
||||
Hero Badge/Teaser: Einzelstücke & Ausstellungsdeals – nur solange verfügbar
|
||||
Textblock (links unten):
|
||||
• Eyebrow: Heute im Fokus
|
||||
• Headline: Kuratiert. Hochwertig. Sofort.
|
||||
• Subline: Scannen Sie den QR-Code für Kontakt & Store-Infos.
|
||||
• Footer klein: CABINET Bielefeld – im Store ansprechen oder direkt reservieren
|
||||
• Disclaimer klein: Zwischenverkauf vorbehalten
|
||||
|
||||
QR-Box (rechts):
|
||||
• Titel: Kontakt & Store-Infos
|
||||
• Sub: QR scannen
|
||||
• Kontaktzeile: WhatsApp: … · Tel.: …
|
||||
• QR führt auf: Kontakt-/Store-Seite (URL wird geliefert)
|
||||
|
||||
⸻
|
||||
|
||||
Slide 1 – GOYA (Variante B: Preis/Hero)
|
||||
|
||||
Header links: CABINET
|
||||
Header rechts: Ausstellungsware + Einzelstück
|
||||
Hero Hinweis: Bild GOYA (Hero-Foto)
|
||||
|
||||
Textblock:
|
||||
• Eyebrow: Ausstellungsware
|
||||
• Titel: GOYA Sideboard
|
||||
• Subline: Marke: Sudbrock
|
||||
• Preis groß: 489 €
|
||||
• Preiszusatz: brutto
|
||||
• UVP-Zeile: UVP neu: 4.744 €*
|
||||
• Disclaimer klein: *UVP nur, sofern belegbar. Zwischenverkauf vorbehalten.
|
||||
|
||||
QR-Box:
|
||||
• Titel: Infos & Reservierung
|
||||
• Sub: QR scannen
|
||||
• Kontaktzeile: WhatsApp: … · Tel.: …
|
||||
• QR führt auf: GOYA-Detail/Reservierung (URL wird geliefert)
|
||||
|
||||
⸻
|
||||
|
||||
Slide 2 – GOYA (Variante B: Konditionen)
|
||||
|
||||
Header links: CABINET
|
||||
Header rechts: GOYA + Konditionen
|
||||
Hero Hinweis: GOYA Detail/2. Ansicht
|
||||
|
||||
Textblock:
|
||||
• Eyebrow: GOYA | Konditionen
|
||||
• Headline: Details auf einen Blick
|
||||
• Bullets:
|
||||
1. Eingelagertes Einzelstück
|
||||
2. Abholung: Lager Rheda-Wiedenbrück
|
||||
3. Lieferung/Montage optional
|
||||
4. Preis gilt ohne Lieferung/Montage
|
||||
5. Deckel: Weiß matt (erneuert)
|
||||
• Footer: Details & Reservierung + QR scannen
|
||||
|
||||
QR-Box:
|
||||
• Titel: Details: QR scannen
|
||||
• Sub: Reservierung & Kontakt
|
||||
• Disclaimer: Zwischenverkauf vorbehalten
|
||||
• QR führt auf: GOYA-Detail/Reservierung (gleiche URL wie Slide 1)
|
||||
|
||||
⸻
|
||||
|
||||
Slide 3 – TANDO (Variante C: Impulskauf)
|
||||
|
||||
Header links: CABINET
|
||||
Header rechts: Nur 1× im Store + Sofort
|
||||
Hero Hinweis: Foto des Spiegels im Laden (TANDO)
|
||||
|
||||
Textblock:
|
||||
• Eyebrow: Nur 1× im Store
|
||||
• Titel: TANDO Spiegel
|
||||
• Subline: Ansehen & mitnehmen – heute
|
||||
• Preis groß: 199 €
|
||||
• Preiszusatz: brutto
|
||||
• kleine Zusatzzeile: Jetzt sichern
|
||||
|
||||
QR-Box:
|
||||
• Titel: Jetzt sichern
|
||||
• Sub: QR scannen oder Team ansprechen
|
||||
• Kontaktzeile: WhatsApp: … · Tel.: …
|
||||
• QR führt auf: TANDO-Detail/Info (URL wird geliefert)
|
||||
|
||||
⸻
|
||||
|
||||
4) Inhalte/Logik als Konfiguration (Wunsch)
|
||||
|
||||
Bitte so bauen, dass Texte/QR-Links/Zeiten leicht austauschbar sind:
|
||||
• slides[] (JSON) mit Feldern: duration, headerLeft, headerRightLines[], heroImage, eyebrow, title, subline, price, priceNote, uvp, bullets[], qrTitle, qrSub, qrLink, contactLine, disclaimer.
|
||||
|
||||
⸻
|
||||
|
||||
5) Offene Inputs vom Auftraggeber (Platzhalter)
|
||||
• WhatsApp Nummer / Tel.
|
||||
• QR-Ziel-URLs:
|
||||
• Kontakt/Store
|
||||
• GOYA Detail/Reservierung
|
||||
• TANDO Detail/Info
|
||||
• Bildassets (1080×mind. 1400 empfohlen):
|
||||
• GOYA Hero
|
||||
• GOYA Detail
|
||||
• TANDO im Store
|
||||
|
||||
Das Briefing ist so geschrieben, dass der Entwickler es direkt als HTML/CSS/JS-Slider umsetzen kann (oder in eure bestehende HTML-Playlist integrieren).
|
||||
126
public/_cabinet/offers/config.json
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
{
|
||||
"meta": {
|
||||
"version": "1.0",
|
||||
"location": "CABINET Bielefeld",
|
||||
"updated": "2026-02-05"
|
||||
},
|
||||
"settings": {
|
||||
"loop": true,
|
||||
"transition": {
|
||||
"type": "fade",
|
||||
"duration": 600
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"whatsapp": "0521 ...",
|
||||
"phone": "0521 ...",
|
||||
"storeUrl": "https://cabinet-bielefeld.de"
|
||||
},
|
||||
"slides": [
|
||||
{
|
||||
"id": "intro",
|
||||
"file": "slide-0-intro.html",
|
||||
"duration": 8000,
|
||||
"type": "intro",
|
||||
"content": {
|
||||
"headerLeft": "CABINET Bielefeld",
|
||||
"headerRight": [
|
||||
"Planung • Beratung",
|
||||
"Lieferung & Montage"
|
||||
],
|
||||
"heroBadge": "Einzelstücke & Ausstellungsdeals – nur solange verfügbar",
|
||||
"eyebrow": "Heute im Fokus",
|
||||
"title": "Kuratiert. Hochwertig. Sofort.",
|
||||
"subline": "Scannen Sie den QR-Code für Kontakt & Store-Infos.",
|
||||
"footer": "CABINET Bielefeld – im Store ansprechen oder direkt reservieren",
|
||||
"disclaimer": "Zwischenverkauf vorbehalten",
|
||||
"qr": {
|
||||
"title": "Kontakt & Store-Infos",
|
||||
"subtitle": "QR scannen",
|
||||
"url": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "goya-hero",
|
||||
"file": "slide-1-goya-hero.html",
|
||||
"duration": 10000,
|
||||
"type": "product-hero",
|
||||
"content": {
|
||||
"headerLeft": "CABINET",
|
||||
"headerRight": [
|
||||
"Ausstellungsware",
|
||||
"Einzelstück"
|
||||
],
|
||||
"heroImage": "assets/goya-hero.jpg",
|
||||
"eyebrow": "Ausstellungsware",
|
||||
"title": "GOYA Sideboard",
|
||||
"subline": "Marke: Sudbrock",
|
||||
"price": "489 €",
|
||||
"priceNote": "brutto",
|
||||
"uvp": "UVP neu: 4.744 €*",
|
||||
"disclaimer": "*UVP nur, sofern belegbar. Zwischenverkauf vorbehalten.",
|
||||
"qr": {
|
||||
"title": "Infos & Reservierung",
|
||||
"subtitle": "QR scannen",
|
||||
"url": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "goya-details",
|
||||
"file": "slide-2-goya-details.html",
|
||||
"duration": 12000,
|
||||
"type": "product-details",
|
||||
"content": {
|
||||
"headerLeft": "CABINET",
|
||||
"headerRight": [
|
||||
"GOYA",
|
||||
"Konditionen"
|
||||
],
|
||||
"heroImage": "assets/goya-detail.jpg",
|
||||
"eyebrow": "GOYA | Konditionen",
|
||||
"title": "Details auf einen Blick",
|
||||
"bullets": [
|
||||
"Eingelagertes Einzelstück",
|
||||
"Abholung: Lager Rheda-Wiedenbrück",
|
||||
"Lieferung/Montage optional",
|
||||
"Preis gilt ohne Lieferung/Montage",
|
||||
"Deckel: Weiß matt (erneuert)"
|
||||
],
|
||||
"cta": "Details & Reservierung",
|
||||
"qr": {
|
||||
"title": "Details: QR scannen",
|
||||
"subtitle": "Reservierung & Kontakt",
|
||||
"url": ""
|
||||
},
|
||||
"disclaimer": "Zwischenverkauf vorbehalten"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tando",
|
||||
"file": "slide-3-tando.html",
|
||||
"duration": 10000,
|
||||
"type": "product-impulse",
|
||||
"content": {
|
||||
"headerLeft": "CABINET",
|
||||
"headerRight": [
|
||||
"Nur 1× im Store",
|
||||
"Sofort"
|
||||
],
|
||||
"heroImage": "assets/tando-store.jpg",
|
||||
"eyebrow": "Nur 1× im Store",
|
||||
"title": "TANDO Spiegel",
|
||||
"subline": "Ansehen & mitnehmen – heute",
|
||||
"price": "199 €",
|
||||
"priceNote": "brutto",
|
||||
"impulseTag": "Jetzt sichern",
|
||||
"qr": {
|
||||
"title": "Jetzt sichern",
|
||||
"subtitle": "QR scannen oder Team ansprechen",
|
||||
"url": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
408
public/_cabinet/offers/player.html
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CABINET – Angebote Display</title>
|
||||
<style>
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
/* Container für 9:16 Aspect Ratio */
|
||||
.player-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.player-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 1080px;
|
||||
max-height: 1920px;
|
||||
aspect-ratio: 9 / 16;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-aspect-ratio: 9/16) {
|
||||
.player-frame {
|
||||
width: auto;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.player-frame {
|
||||
width: 100vw;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* iFrames für Slides */
|
||||
.slide-frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slide-frame.active {
|
||||
opacity: 1;
|
||||
z-index: 2;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.slide-frame.preload {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Progress Indicator */
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: #009FE3;
|
||||
transition: width linear;
|
||||
}
|
||||
|
||||
/* Slide Indicators */
|
||||
.slide-indicators {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 100;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.indicator.active {
|
||||
background: #009FE3;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Debug Info (optional, ausblendbar) */
|
||||
.debug-info {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: #fff;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.debug-info.visible {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="player-container">
|
||||
<div class="player-frame" id="player">
|
||||
<!-- Loading Overlay -->
|
||||
<div class="loading-overlay" id="loading">
|
||||
<span class="loading-text">Lädt Angebote...</span>
|
||||
</div>
|
||||
|
||||
<!-- Slides werden hier dynamisch eingefügt -->
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progress"></div>
|
||||
</div>
|
||||
|
||||
<!-- Slide Indicators (optional) -->
|
||||
<div class="slide-indicators" id="indicators"></div>
|
||||
|
||||
<!-- Debug Info -->
|
||||
<div class="debug-info" id="debug">
|
||||
Slide: <span id="debug-slide">0</span> / <span id="debug-total">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* CABINET Offer Slide Player
|
||||
* Lädt und rotiert durch Slides basierend auf config.json
|
||||
*/
|
||||
|
||||
const CONFIG_URL = 'config.json';
|
||||
const DEBUG_MODE = false; // Auf true setzen für Debug-Infos
|
||||
|
||||
class SlidePlayer {
|
||||
constructor() {
|
||||
this.config = null;
|
||||
this.slides = [];
|
||||
this.currentIndex = 0;
|
||||
this.frames = [];
|
||||
this.timer = null;
|
||||
this.isPlaying = false;
|
||||
|
||||
// DOM Elements
|
||||
this.player = document.getElementById('player');
|
||||
this.loading = document.getElementById('loading');
|
||||
this.progress = document.getElementById('progress');
|
||||
this.indicators = document.getElementById('indicators');
|
||||
this.debug = document.getElementById('debug');
|
||||
this.debugSlide = document.getElementById('debug-slide');
|
||||
this.debugTotal = document.getElementById('debug-total');
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
this.debug.classList.add('visible');
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
console.log('[Player] Initializing...');
|
||||
|
||||
// Lade Konfiguration
|
||||
await this.loadConfig();
|
||||
|
||||
// Erstelle iFrames für alle Slides
|
||||
this.createFrames();
|
||||
|
||||
// Erstelle Indikatoren
|
||||
this.createIndicators();
|
||||
|
||||
// Warte bis erster Slide geladen ist
|
||||
await this.preloadSlide(0);
|
||||
|
||||
// Verstecke Loading
|
||||
this.loading.classList.add('hidden');
|
||||
|
||||
// Starte Rotation
|
||||
this.play();
|
||||
|
||||
console.log('[Player] Ready!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Player] Init error:', error);
|
||||
this.showError('Fehler beim Laden der Angebote');
|
||||
}
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
const response = await fetch(CONFIG_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Config load failed: ${response.status}`);
|
||||
}
|
||||
this.config = await response.json();
|
||||
this.slides = this.config.slides || [];
|
||||
|
||||
console.log(`[Player] Loaded ${this.slides.length} slides`);
|
||||
this.debugTotal.textContent = this.slides.length;
|
||||
}
|
||||
|
||||
createFrames() {
|
||||
this.slides.forEach((slide, index) => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'slide-frame';
|
||||
iframe.id = `frame-${index}`;
|
||||
iframe.setAttribute('loading', 'lazy');
|
||||
iframe.setAttribute('data-src', slide.file);
|
||||
|
||||
// Füge vor Progress Bar ein
|
||||
this.player.insertBefore(iframe, this.player.querySelector('.progress-bar'));
|
||||
this.frames.push(iframe);
|
||||
});
|
||||
}
|
||||
|
||||
createIndicators() {
|
||||
this.slides.forEach((_, index) => {
|
||||
const dot = document.createElement('div');
|
||||
dot.className = 'indicator';
|
||||
dot.dataset.index = index;
|
||||
this.indicators.appendChild(dot);
|
||||
});
|
||||
}
|
||||
|
||||
async preloadSlide(index) {
|
||||
const frame = this.frames[index];
|
||||
if (!frame) return;
|
||||
|
||||
// Wenn noch nicht geladen, lade jetzt
|
||||
if (!frame.src) {
|
||||
const src = frame.getAttribute('data-src');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
frame.onload = () => {
|
||||
console.log(`[Player] Slide ${index} loaded`);
|
||||
resolve();
|
||||
};
|
||||
frame.onerror = () => {
|
||||
console.error(`[Player] Slide ${index} failed to load`);
|
||||
resolve(); // Trotzdem weiter
|
||||
};
|
||||
frame.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
showSlide(index) {
|
||||
// Deaktiviere alle Frames
|
||||
this.frames.forEach((frame, i) => {
|
||||
frame.classList.remove('active', 'preload');
|
||||
});
|
||||
|
||||
// Aktiviere aktuellen Frame
|
||||
const currentFrame = this.frames[index];
|
||||
if (currentFrame) {
|
||||
currentFrame.classList.add('active');
|
||||
}
|
||||
|
||||
// Update Indikatoren
|
||||
const dots = this.indicators.querySelectorAll('.indicator');
|
||||
dots.forEach((dot, i) => {
|
||||
dot.classList.toggle('active', i === index);
|
||||
});
|
||||
|
||||
// Update Debug
|
||||
this.debugSlide.textContent = index + 1;
|
||||
|
||||
// Preload nächsten Slide
|
||||
const nextIndex = (index + 1) % this.slides.length;
|
||||
this.preloadSlide(nextIndex);
|
||||
|
||||
console.log(`[Player] Showing slide ${index}: ${this.slides[index].id}`);
|
||||
}
|
||||
|
||||
startProgress(duration) {
|
||||
// Reset progress
|
||||
this.progress.style.transition = 'none';
|
||||
this.progress.style.width = '0%';
|
||||
|
||||
// Force reflow
|
||||
void this.progress.offsetWidth;
|
||||
|
||||
// Animate
|
||||
this.progress.style.transition = `width ${duration}ms linear`;
|
||||
this.progress.style.width = '100%';
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.isPlaying) return;
|
||||
this.isPlaying = true;
|
||||
|
||||
this.showCurrentSlide();
|
||||
}
|
||||
|
||||
showCurrentSlide() {
|
||||
const slide = this.slides[this.currentIndex];
|
||||
const duration = slide.duration || 8000;
|
||||
|
||||
// Zeige Slide
|
||||
this.showSlide(this.currentIndex);
|
||||
|
||||
// Starte Progress
|
||||
this.startProgress(duration);
|
||||
|
||||
// Timer für nächsten Slide
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
this.nextSlide();
|
||||
}, duration);
|
||||
}
|
||||
|
||||
nextSlide() {
|
||||
this.currentIndex = (this.currentIndex + 1) % this.slides.length;
|
||||
this.showCurrentSlide();
|
||||
}
|
||||
|
||||
prevSlide() {
|
||||
this.currentIndex = (this.currentIndex - 1 + this.slides.length) % this.slides.length;
|
||||
this.showCurrentSlide();
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.isPlaying = false;
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.loading.innerHTML = `<span class="loading-text" style="color: #c00;">${message}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Player
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const player = new SlidePlayer();
|
||||
player.init();
|
||||
|
||||
// Optional: Keyboard Controls (für Testing)
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowRight') player.nextSlide();
|
||||
if (e.key === 'ArrowLeft') player.prevSlide();
|
||||
if (e.key === ' ') player.isPlaying ? player.pause() : player.play();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
493
public/_cabinet/offers/shared-styles.css
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
/**
|
||||
* CABINET Display - Shared Styles
|
||||
* Format: 9:16 (1080×1920px)
|
||||
* Safe-Area: 64px
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Colors */
|
||||
--bg: #ffffff;
|
||||
--fg: #1a1a1a;
|
||||
--fg-strong: #000000;
|
||||
--muted: #737373;
|
||||
--muted-light: #999999;
|
||||
--line: #e8e8e8;
|
||||
--card: #f5f5f5;
|
||||
--accent: #009FE3; /* Cabinet Blau */
|
||||
|
||||
/* Spacing */
|
||||
--safe-area: 64px;
|
||||
--radius: 24px;
|
||||
--radius-sm: 16px;
|
||||
|
||||
/* Typography Scale (modular) */
|
||||
--text-xs: 16px;
|
||||
--text-sm: 18px;
|
||||
--text-base: 20px;
|
||||
--text-lg: 24px;
|
||||
--text-xl: 28px;
|
||||
--text-2xl: 32px;
|
||||
--text-3xl: 42px;
|
||||
--text-4xl: 54px;
|
||||
--text-5xl: 64px;
|
||||
--text-6xl: 84px;
|
||||
|
||||
/* Font */
|
||||
--font-main: 'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
|
||||
/* Dimensions */
|
||||
--max-width: 1080px;
|
||||
--max-height: 1920px;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-main);
|
||||
background: #0a0a0a;
|
||||
color: var(--fg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
/* Text Rendering */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
SCREEN CONTAINER (9:16 Frame)
|
||||
======================================== */
|
||||
|
||||
.screen {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
max-width: var(--max-width);
|
||||
max-height: var(--max-height);
|
||||
aspect-ratio: 9 / 16;
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Maintain aspect ratio */
|
||||
@media (min-aspect-ratio: 9/16) {
|
||||
.screen {
|
||||
width: auto;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.screen {
|
||||
width: 100vw;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
SLIDE LAYOUT
|
||||
======================================== */
|
||||
|
||||
.slide {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: var(--safe-area);
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 32px;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
HEADER
|
||||
======================================== */
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
height: 82px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fg-strong);
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--muted);
|
||||
text-align: right;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
HERO SECTION
|
||||
======================================== */
|
||||
|
||||
.hero {
|
||||
border-radius: var(--radius);
|
||||
background: linear-gradient(145deg, #f5f5f5, #fafafa);
|
||||
border: 1px solid var(--line);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.hero.has-image {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 500;
|
||||
color: var(--fg);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 100px;
|
||||
padding: 14px 24px;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
letter-spacing: 0.01em;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Placeholder für Entwicklung */
|
||||
.hero-placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: var(--muted);
|
||||
background:
|
||||
radial-gradient(circle at 30% 30%, rgba(0,0,0,0.03), transparent 50%),
|
||||
linear-gradient(145deg, #f2f2f2, #fafafa);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
BOTTOM SECTION (Info + QR)
|
||||
======================================== */
|
||||
|
||||
.bottom {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* Info Box */
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(180deg, #ffffff, #fafafa);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 28px;
|
||||
min-height: 340px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--text-4xl);
|
||||
line-height: 1.08;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px;
|
||||
color: var(--fg-strong);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.title.large {
|
||||
font-size: var(--text-5xl);
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.title.medium {
|
||||
font-size: var(--text-3xl);
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.subline {
|
||||
font-size: var(--text-xl);
|
||||
color: var(--muted);
|
||||
line-height: 1.35;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Price Display */
|
||||
.price-block {
|
||||
margin-top: auto;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: var(--text-6xl);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--fg-strong);
|
||||
line-height: 1;
|
||||
font-feature-settings: 'tnum' 1; /* Tabular numbers */
|
||||
}
|
||||
|
||||
.price-note {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--muted);
|
||||
text-align: right;
|
||||
line-height: 1.35;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Bullets List */
|
||||
.bullets {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.bullets li {
|
||||
font-size: var(--text-xl);
|
||||
line-height: 1.35;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
color: var(--fg);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.bullets .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
margin-top: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Footer Text (within info box) */
|
||||
.info-footer {
|
||||
margin-top: auto;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: var(--text-base);
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.footer-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
QR BOX
|
||||
======================================== */
|
||||
|
||||
.qr-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.qr-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--fg-strong);
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.qr-subtitle {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.qr-code-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px dashed #ddd;
|
||||
padding: 16px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.qr-code-wrapper img {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
height: auto;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
/* QR Placeholder */
|
||||
.qr-placeholder {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
#e0e0e0,
|
||||
#e0e0e0 12px,
|
||||
transparent 12px,
|
||||
transparent 16px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
#e0e0e0,
|
||||
#e0e0e0 12px,
|
||||
transparent 12px,
|
||||
transparent 16px
|
||||
);
|
||||
opacity: 0.5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.qr-contact {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
DISCLAIMER
|
||||
======================================== */
|
||||
|
||||
.disclaimer {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--muted-light);
|
||||
margin-top: 12px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
ANIMATIONS (for player integration)
|
||||
======================================== */
|
||||
|
||||
.slide.fade-in {
|
||||
animation: fadeIn 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.slide.fade-out {
|
||||
animation: fadeOut 0.6s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
UTILITY CLASSES
|
||||
======================================== */
|
||||
|
||||
.text-accent {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mt-auto {
|
||||
margin-top: auto;
|
||||
}
|
||||
100
public/_cabinet/offers/slide-0-intro.html
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CABINET – Intro</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./shared-styles.css">
|
||||
<style>
|
||||
/* Slide-spezifische Anpassungen */
|
||||
.hero {
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 40%, rgba(0, 159, 227, 0.05), transparent 60%),
|
||||
linear-gradient(165deg, #f8f8f8, #ffffff);
|
||||
}
|
||||
.hero.has-image {
|
||||
background: url('../assets/cabinet-intro.jpg') center/cover no-repeat;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 500;
|
||||
padding: 16px 28px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="screen">
|
||||
<article class="slide" data-duration="8000">
|
||||
|
||||
<!-- HEADER -->
|
||||
<header class="header">
|
||||
<div class="brand">
|
||||
<img src="./logo-cabinet-300.png" alt="CABINET" class="brand-logo">
|
||||
<span class="brand-text">Bielefeld</span>
|
||||
</div>
|
||||
<div class="tagline">
|
||||
Planung • Beratung<br>
|
||||
Lieferung & Montage
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero has-image">
|
||||
<span class="hero-badge">Ausstellungsdeals – solange verfügbar</span>
|
||||
</section>
|
||||
|
||||
<!-- BOTTOM: Info + QR -->
|
||||
<section class="bottom">
|
||||
<div class="info">
|
||||
<div class="info-content">
|
||||
<p class="eyebrow">Heute im Fokus</p>
|
||||
<h1 class="title large">Kuratiert.<br>Hochwertig.<br>Sofort.</h1>
|
||||
</div>
|
||||
|
||||
<div class="info-footer">
|
||||
<span class="footer-text disclaimer">Zwischenverkauf vorbehalten</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="qr-box">
|
||||
<div class="qr-header">
|
||||
<p class="qr-title">Kontakt</p>
|
||||
<p class="qr-subtitle">QR scannen</p>
|
||||
</div>
|
||||
<div class="qr-code-wrapper">
|
||||
<img id="qr-code" src="" alt="QR Code">
|
||||
</div>
|
||||
<p class="qr-contact">0521 98620100<br>Tel. oder WhatsApp</p>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// QR-Code Konfiguration
|
||||
const QR_URL = 'https://cabinet-bielefeld.de'; // Ziel-URL anpassen
|
||||
|
||||
// QR-Code generieren
|
||||
function generateQR(targetUrl) {
|
||||
const size = '300x300';
|
||||
const color = '000000';
|
||||
const bg = 'ffffff';
|
||||
const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}&color=${color}&bgcolor=${bg}&margin=8&data=${encodeURIComponent(targetUrl)}`;
|
||||
|
||||
const qrImg = document.getElementById('qr-code');
|
||||
if (qrImg) {
|
||||
qrImg.src = qrApiUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Bei Seitenaufruf QR generieren
|
||||
document.addEventListener('DOMContentLoaded', () => generateQR(QR_URL));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
105
public/_cabinet/offers/slide-1-goya-hero.html
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CABINET – GOYA Sideboard</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./shared-styles.css">
|
||||
<style>
|
||||
/* Slide-spezifische Anpassungen */
|
||||
.hero {
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 40%, rgba(0, 159, 227, 0.05), transparent 60%),
|
||||
linear-gradient(165deg, #f8f8f8, #ffffff);
|
||||
}
|
||||
.hero.has-image {
|
||||
background: url('../assets/goya1.jpg') center/cover no-repeat;
|
||||
}
|
||||
|
||||
.price {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.uvp-strike {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
padding: 16px 28px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="screen">
|
||||
<article class="slide" data-duration="10000">
|
||||
|
||||
<!-- HEADER -->
|
||||
<header class="header">
|
||||
<div class="brand">
|
||||
<img src="./logo-cabinet-300.png" alt="CABINET" class="brand-logo">
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero has-image">
|
||||
<span class="hero-badge">Einzelstück</span>
|
||||
</section>
|
||||
|
||||
<!-- BOTTOM: Info + QR -->
|
||||
<section class="bottom">
|
||||
<div class="info">
|
||||
<div class="info-content">
|
||||
<p class="eyebrow">Hersteller: Sudbrock</p>
|
||||
<h1 class="title large">GOYA Sideboard</h1>
|
||||
</div>
|
||||
|
||||
<div class="price-block">
|
||||
<div class="price-row">
|
||||
<span class="price">489 €</span>
|
||||
<div class="price-note">
|
||||
statt 4.744 €
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="qr-box">
|
||||
<div class="qr-header">
|
||||
<p class="qr-title">Reservieren</p>
|
||||
<p class="qr-subtitle">QR scannen</p>
|
||||
</div>
|
||||
<div class="qr-code-wrapper">
|
||||
<img id="qr-code" src="" alt="QR Code">
|
||||
</div>
|
||||
<p class="qr-contact">0521 98620100<br>Tel. oder WhatsApp</p>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// QR-Code Konfiguration – GOYA Produkt-/Reservierungsseite
|
||||
const QR_URL = 'https://cabinet-bielefeld.de';
|
||||
|
||||
function generateQR(targetUrl) {
|
||||
const size = '300x300';
|
||||
const color = '000000';
|
||||
const bg = 'ffffff';
|
||||
const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}&color=${color}&bgcolor=${bg}&margin=8&data=${encodeURIComponent(targetUrl)}`;
|
||||
|
||||
const qrImg = document.getElementById('qr-code');
|
||||
if (qrImg) qrImg.src = qrApiUrl;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => generateQR(QR_URL));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
135
public/_cabinet/offers/slide-2-goya-details.html
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CABINET – GOYA Konditionen</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./shared-styles.css">
|
||||
<style>
|
||||
/* Slide-spezifische Anpassungen */
|
||||
.hero {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(245,245,245,0.9) 0%, rgba(250,250,250,0.95) 100%);
|
||||
}
|
||||
.hero.has-image {
|
||||
background: url('../assets/goya2.jpg') center/cover no-repeat;
|
||||
}
|
||||
.bullets {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.bullets li {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.cta-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.cta-text {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.cta-action {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--fg-strong);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
padding: 16px 28px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="screen">
|
||||
<article class="slide" data-duration="12000">
|
||||
|
||||
<!-- HEADER -->
|
||||
<header class="header">
|
||||
<div class="brand">
|
||||
<img src="./logo-cabinet-300.png" alt="CABINET" class="brand-logo">
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero has-image">
|
||||
<span class="hero-badge">Einzelstück</span>
|
||||
</section>
|
||||
|
||||
<!-- BOTTOM: Info + QR -->
|
||||
<section class="bottom">
|
||||
<div class="info">
|
||||
<div class="info-content">
|
||||
<p class="eyebrow">Auf einen Blick<</p>
|
||||
<h1 class="title medium">GOYA Sideboard</h1>
|
||||
|
||||
<ul class="bullets">
|
||||
<li>
|
||||
<span class="dot"></span>
|
||||
<span>Eingelagertes Einzelstück</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="dot"></span>
|
||||
<span>Abholung in Rheda-Wiedenbrück</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="dot"></span>
|
||||
<span>Lieferung optional</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="dot"></span>
|
||||
<span>Deckel weiß matt (neu)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<aside class="qr-box">
|
||||
<div class="qr-header">
|
||||
<p class="qr-title">Reservieren</p>
|
||||
<p class="qr-subtitle">QR scannen</p>
|
||||
</div>
|
||||
<div class="qr-code-wrapper">
|
||||
<img id="qr-code" src="" alt="QR Code">
|
||||
</div>
|
||||
<p class="qr-contact">0521 98620100<br>Tel. oder WhatsApp</p>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// QR-Code Konfiguration – GOYA Details (gleiche URL wie Slide 1)
|
||||
const QR_URL = 'https://cabinet-bielefeld.de';
|
||||
|
||||
function generateQR(targetUrl) {
|
||||
const size = '300x300';
|
||||
const color = '000000';
|
||||
const bg = 'ffffff';
|
||||
const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}&color=${color}&bgcolor=${bg}&margin=8&data=${encodeURIComponent(targetUrl)}`;
|
||||
|
||||
const qrImg = document.getElementById('qr-code');
|
||||
if (qrImg) qrImg.src = qrApiUrl;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => generateQR(QR_URL));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
109
public/_cabinet/offers/slide-3-tando.html
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CABINET – TANDO Spiegel</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./shared-styles.css">
|
||||
<style>
|
||||
/* Slide-spezifische Anpassungen */
|
||||
.hero {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(245,245,245,0.9) 0%, rgba(250,250,250,0.95) 100%);
|
||||
}
|
||||
|
||||
.hero.has-image {
|
||||
background: url('../assets/tango.jpg') center/cover no-repeat;
|
||||
}
|
||||
.impulse-tag {
|
||||
display: inline-block;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
margin-top: 12px;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
padding: 16px 28px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="screen">
|
||||
<article class="slide" data-duration="10000">
|
||||
|
||||
<!-- HEADER -->
|
||||
<header class="header">
|
||||
<div class="brand">
|
||||
<img src="./logo-cabinet-300.png" alt="CABINET" class="brand-logo">
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero has-image">
|
||||
<span class="hero-badge">Ausstellungsstück</span>
|
||||
</section>
|
||||
|
||||
<!-- BOTTOM: Info + QR -->
|
||||
<section class="bottom">
|
||||
<div class="info">
|
||||
<div class="info-content">
|
||||
<p class="eyebrow">Nur 1×</p>
|
||||
<h1 class="title large">TANDO Spiegel</h1>
|
||||
<p class="subline">Heute mitnehmen</p>
|
||||
</div>
|
||||
|
||||
<div class="price-block">
|
||||
<div class="price-row">
|
||||
<span class="price">199 €</span>
|
||||
<div class="price-note">
|
||||
<span class="impulse-tag">Im Store verfügbar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="qr-box">
|
||||
<div class="qr-header">
|
||||
<p class="qr-title">Sichern</p>
|
||||
<p class="qr-subtitle">QR scannen</p>
|
||||
</div>
|
||||
<div class="qr-code-wrapper">
|
||||
<img id="qr-code" src="" alt="QR Code">
|
||||
</div>
|
||||
<p class="qr-contact">0521 98620100<br>Tel. oder WhatsApp</p>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// QR-Code Konfiguration – TANDO Produkt
|
||||
const QR_URL = 'https://cabinet-bielefeld.de';
|
||||
|
||||
function generateQR(targetUrl) {
|
||||
const size = '300x300';
|
||||
const color = '000000';
|
||||
const bg = 'ffffff';
|
||||
const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}&color=${color}&bgcolor=${bg}&margin=8&data=${encodeURIComponent(targetUrl)}`;
|
||||
|
||||
const qrImg = document.getElementById('qr-code');
|
||||
if (qrImg) qrImg.src = qrApiUrl;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => generateQR(QR_URL));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||