175 lines
6.6 KiB
Markdown
175 lines
6.6 KiB
Markdown
# B2in Display – Frontend-Implementation
|
||
|
||
**Ziel:** Frontend-Webapp für das B2in Schaufenster-Display (9:16 Portrait, 43–55 Zoll).
|
||
**Pfad:** `public/_cabinet/b2in/index.html`
|
||
**Konzept-Basis:** `public/_cabinet/_docs/b2in-displays.md`
|
||
|
||
---
|
||
|
||
## Ist-Zustand
|
||
|
||
Es gibt bereits drei Display-Typen unter `public/_cabinet/`:
|
||
|
||
| Typ | Pfad | Status | Beschreibung |
|
||
|-----|------|--------|-------------|
|
||
| **Video-Display** | `index.html` | Live, stabil | CABINET-Branding. Videos + Footer-Rotation. API: `/api/display/config` |
|
||
| **Offers** | `offers/index.html` | Entwickelt, nicht live | CABINET-Branding. iFrame-basierte Slide-Rotation mit `config.json` |
|
||
| **Info-Tablet** | `info/index.html` | Entwickelt | CABINET-Branding. Store-Status/Öffnungszeiten. API: `/api/cabinet-tablet/status` |
|
||
| **B2in Display** | `b2in/` | **Neu – wird jetzt gebaut** | B2in-Branding. Playlist mit Videos/Bildern + Text. API: `/api/b2in-display/playlist` |
|
||
|
||
---
|
||
|
||
## Architektur-Übersicht
|
||
|
||
```
|
||
public/_cabinet/b2in/
|
||
├── index.html ← Haupt-Webapp (Playlist-Engine + UI)
|
||
├── b2in-styles.css ← Display-spezifische Styles
|
||
└── (assets/) ← Medien werden per API/URL referenziert, nicht lokal
|
||
```
|
||
|
||
Shared CSS aus `public/_cabinet/shared/cabinet-base.css` wird **nicht** importiert – das B2in-Display hat ein eigenständiges Branding (dunkel, B2in statt CABINET). Es ist ein komplett eigenes Design-System.
|
||
|
||
---
|
||
|
||
## Schritte
|
||
|
||
### Schritt 1: HTML-Grundgerüst + CSS
|
||
|
||
**Datei:** `public/_cabinet/b2in/index.html` + `b2in-styles.css`
|
||
|
||
Layout gemäß Konzept (Abschnitt 2.1):
|
||
|
||
```
|
||
┌─────────────────────────┐
|
||
│ HEADER │ ← B2in-Logo (links) + Claim (rechts)
|
||
│ B2in · Claim │ Fest, immer sichtbar
|
||
├─────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────┐ │ ← Gradient oben (dunkel → transparent)
|
||
│ │ VIDEO / BILD │ │ 16:9 Content-Bereich
|
||
│ │ (16:9 Media) │ │ object-fit: cover
|
||
│ └─────────────────┘ │ ← Gradient unten (transparent → dunkel)
|
||
│ │
|
||
├─────────────────────────┤
|
||
│ TEXTFELD │ ← Headline (max 40 Zeichen)
|
||
│ Headline + Subline │ Subline (max 80 Zeichen)
|
||
├─────────────────────────┤ Wechselt synchron mit Media
|
||
│ FOOTER │ ← "Marcel Scheibe" + "b2in.de" + QR-Code
|
||
│ Name · URL · QR │ Fest, immer sichtbar
|
||
└─────────────────────────┘
|
||
```
|
||
|
||
**Design-Entscheidungen:**
|
||
- Dunkler Hintergrund (#0a0a0a) – B2in-Branding, nicht CABINET-weiß
|
||
- 9:16 Aspect Ratio Container (wie bestehende Displays)
|
||
- Gradient-Overlays oben/unten über dem Media-Bereich
|
||
- IBM Plex Sans Font (wie alle Cabinet-Displays)
|
||
- Akzentfarbe: B2in-Blau (wird aus Branding-Guide übernommen)
|
||
- Kein CABINET-Logo, kein Accent #009FE3
|
||
|
||
### Schritt 2: Playlist-Engine (JavaScript)
|
||
|
||
**Klasse:** `B2inDisplayApp`
|
||
|
||
Kernfunktionalität:
|
||
1. **API laden** → `GET /api/b2in-display/playlist` (wird später gebaut, erstmal Mock/Fallback)
|
||
2. **Playlist sortieren** → nach `sort_order`, nur `is_active === true`
|
||
3. **Gewichtung anwenden** → 70/30 Immobilien/Möbel-Verteilung berechnen
|
||
4. **Rotation starten** → Item für Item durchspielen
|
||
|
||
Item-Wechsel-Logik:
|
||
- **Video:** Abspielen bis Ende → nächstes Item
|
||
- **Bild:** `duration_seconds` abwarten → nächstes Item
|
||
- **Am Ende der Playlist:** Von vorne beginnen
|
||
|
||
### Schritt 3: Video-Handling
|
||
|
||
Bewährtes Pattern aus dem bestehenden `index.html` übernehmen:
|
||
- `autoplay muted playsinline` für Browser-Autoplay-Policy
|
||
- Memory-Management: `src` leeren + `load()` nach jedem Video
|
||
- Preloading: Nächstes Item im Hintergrund vorladen
|
||
- Start-Timeout (10s) → bei Timeout überspringen
|
||
- Watchdog: Prüft alle 5s ob Video noch läuft
|
||
- Error-Handling: Bei Fehler → Item überspringen, nie schwarzer Screen
|
||
|
||
### Schritt 4: Bild-Handling
|
||
|
||
- `<img>` Element mit `object-fit: cover`
|
||
- Duration aus Item oder globaler `default_image_duration`
|
||
- Ken-Burns-Effekt (optionaler langsamer Zoom per CSS)
|
||
- Preload via `new Image()` im Hintergrund
|
||
|
||
### Schritt 5: Transitions
|
||
|
||
Drei Typen (per CMS-Setting steuerbar):
|
||
|
||
| Typ | Umsetzung |
|
||
|-----|-----------|
|
||
| `fade` | Opacity 1→0, dann 0→1 |
|
||
| `crossfade` | Neues Element über dem alten einblenden (empfohlen, Standard) |
|
||
| `slide` | CSS transform translateX |
|
||
|
||
Text-Synchronisation:
|
||
1. Text fade-out (400ms)
|
||
2. Neuen Text setzen
|
||
3. Text fade-in (400ms)
|
||
4. Startet 200ms vor Media-Wechsel
|
||
|
||
### Schritt 6: Polling + Stabilität
|
||
|
||
Gleicher Ansatz wie Info-Tablet, aber mit 60s Intervall:
|
||
|
||
- **Lightweight Check** alle 60s → `GET /api/b2in-display/check`
|
||
- **Full Fetch** nur bei Timestamp-Änderung
|
||
- Laufendes Video/Bild wird zu Ende gespielt, dann neue Playlist
|
||
- localStorage-Cache als Offline-Fallback
|
||
- Auto-Reload alle 6 Stunden
|
||
- Connection-Recovery: 3 Fehler → 5 Min Pause → Retry
|
||
- 30 Min offline → Page-Reload
|
||
|
||
### Schritt 7: Standby-Modus
|
||
|
||
Wenn `display_active === false`:
|
||
- Nur B2in-Logo auf dunklem Hintergrund
|
||
- Kein Content, kein Textfeld, kein Footer-Text
|
||
- Polling läuft weiter (wartet auf Aktivierung)
|
||
|
||
### Schritt 8: Error-Overlay
|
||
|
||
Bei kritischen Fehlern (keine Playlist, kein Media):
|
||
- Dezentes B2in-Logo auf dunklem Hintergrund
|
||
- Niemals Browser-Fehler oder weißer Screen
|
||
- Automatischer Retry im Hintergrund
|
||
|
||
---
|
||
|
||
## API-Abhängigkeit
|
||
|
||
Das Frontend wird **zunächst mit Mock-Daten** gebaut. Die API (`/api/b2in-display/playlist` + `/check`) wird als separater Schritt im Backend implementiert. Das Frontend erkennt automatisch, ob die API verfügbar ist, und fällt auf eingebettete Demo-Daten zurück.
|
||
|
||
**Mock-Playlist für Entwicklung:**
|
||
```javascript
|
||
const MOCK_PLAYLIST = {
|
||
settings: {
|
||
display_active: true,
|
||
footer_name: "Marcel Scheibe",
|
||
footer_url: "b2in.de",
|
||
transition: { type: "crossfade", duration_ms: 800 },
|
||
default_image_duration: 10
|
||
},
|
||
items: [
|
||
// Demo-Items mit Platzhalter-Medien
|
||
],
|
||
updated_at: new Date().toISOString()
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Abgrenzung (was NICHT in diesem Schritt passiert)
|
||
|
||
- Kein CMS-Backend (Model, Migration, Controller, Admin-UI) → separater Schritt
|
||
- Kein Media-Upload → Videos/Bilder werden per URL referenziert
|
||
- Keine Gewichtungs-Logik im Backend → wird im Frontend berechnet
|
||
- Keine Änderung am bestehenden Video-Display (`index.html`) → bleibt unberührt
|