23-01-2026
This commit is contained in:
parent
07959c0ba2
commit
854ce02bf6
166 changed files with 32909 additions and 1262 deletions
371
public/_cabinet/index-dynamic.html
Normal file
371
public/_cabinet/index-dynamic.html
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
<!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">
|
||||
<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;
|
||||
}
|
||||
|
||||
/* --- 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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<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>
|
||||
/* ==============================================
|
||||
KONFIGURATION WIRD DYNAMISCH GELADEN
|
||||
============================================== */
|
||||
|
||||
let videoPlaylist = [];
|
||||
let footerContent = [];
|
||||
|
||||
// API-URL für die Konfiguration
|
||||
const API_URL = '/api/display/config';
|
||||
|
||||
/* ==============================================
|
||||
KONFIGURATION LADEN
|
||||
============================================== */
|
||||
|
||||
async function loadConfiguration() {
|
||||
try {
|
||||
const response = await fetch(API_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Laden der Konfiguration');
|
||||
}
|
||||
|
||||
const config = await response.json();
|
||||
videoPlaylist = config.videoPlaylist || [];
|
||||
footerContent = config.footerContent || [];
|
||||
|
||||
console.log('Konfiguration geladen:', config);
|
||||
|
||||
// Überprüfe, ob Videos vorhanden sind
|
||||
if (videoPlaylist.length === 0) {
|
||||
console.warn('Keine Videos in der Playlist vorhanden');
|
||||
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');
|
||||
footerContent = [{
|
||||
headline: 'WILLKOMMEN',
|
||||
subline: 'Bitte fügen Sie Footer-Inhalte im CMS hinzu',
|
||||
url: 'https://cabinet.b2in.eu'
|
||||
}];
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Konfiguration:', error);
|
||||
document.getElementById('headline').innerText = 'FEHLER';
|
||||
document.getElementById('subline').innerText = 'Konfiguration konnte nicht geladen werden';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
PROGRAMM-LOGIK
|
||||
============================================== */
|
||||
|
||||
// --- VIDEO PLAYER LOGIC ---
|
||||
const videoElement = document.getElementById('video-player');
|
||||
let currentVideoIndex = 0;
|
||||
|
||||
function playNextVideo() {
|
||||
if (videoPlaylist.length === 0) return;
|
||||
|
||||
const video = videoPlaylist[currentVideoIndex];
|
||||
videoElement.src = video.src;
|
||||
videoElement.style.objectPosition = `center ${video.position}%`;
|
||||
videoElement.play().catch(e => console.log("Autoplay blocked/failed", e));
|
||||
|
||||
currentVideoIndex++;
|
||||
if (currentVideoIndex >= videoPlaylist.length) {
|
||||
currentVideoIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
videoElement.addEventListener('ended', playNextVideo);
|
||||
|
||||
// --- 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];
|
||||
|
||||
// Text setzen
|
||||
headlineEl.innerText = content.headline;
|
||||
sublineEl.innerText = content.subline;
|
||||
|
||||
// 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;
|
||||
|
||||
// Index weiterschalten
|
||||
currentFooterIndex++;
|
||||
if (currentFooterIndex >= footerContent.length) {
|
||||
currentFooterIndex = 0;
|
||||
}
|
||||
|
||||
// 3. Einblenden
|
||||
qrImageEl.onload = () => {
|
||||
textArea.classList.remove('fade-out');
|
||||
qrArea.classList.remove('fade-out');
|
||||
};
|
||||
// Fallback falls Bild sofort da ist (Cache)
|
||||
setTimeout(() => {
|
||||
textArea.classList.remove('fade-out');
|
||||
qrArea.classList.remove('fade-out');
|
||||
}, 100);
|
||||
|
||||
}, 1000); // 1 Sekunde für Fade-Out Animation
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
INITIALISIERUNG
|
||||
============================================== */
|
||||
|
||||
async function initialize() {
|
||||
const success = await loadConfiguration();
|
||||
|
||||
if (success && videoPlaylist.length > 0) {
|
||||
// Start Video
|
||||
playNextVideo();
|
||||
|
||||
// Start Footer Loop
|
||||
if (footerContent.length > 0) {
|
||||
updateFooter();
|
||||
setInterval(updateFooter, 30000); // Alle 30.000 ms (30 sek) wechseln
|
||||
|
||||
// Progress Bar initial starten
|
||||
restartProgressBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Beim Laden der Seite initialisieren
|
||||
initialize();
|
||||
|
||||
// Optional: Auto-Reload alle 5 Minuten, um neue Inhalte zu laden
|
||||
setInterval(async () => {
|
||||
console.log('Prüfe auf neue Konfiguration...');
|
||||
await loadConfiguration();
|
||||
}, 5 * 60 * 1000); // 5 Minuten
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue