diff --git a/docs/relaunch/assets/portal/phone-10-analyse.webp b/docs/relaunch/assets/portal/phone-10-analyse.webp new file mode 100644 index 0000000..e30001e Binary files /dev/null and b/docs/relaunch/assets/portal/phone-10-analyse.webp differ diff --git a/docs/relaunch/assets/portal/phone-11-news-musterbach.webp b/docs/relaunch/assets/portal/phone-11-news-musterbach.webp new file mode 100644 index 0000000..af95ba4 Binary files /dev/null and b/docs/relaunch/assets/portal/phone-11-news-musterbach.webp differ diff --git a/docs/relaunch/assets/portal/phone-9-termine.webp b/docs/relaunch/assets/portal/phone-9-termine.webp new file mode 100644 index 0000000..f81a025 Binary files /dev/null and b/docs/relaunch/assets/portal/phone-9-termine.webp differ diff --git a/docs/superpowers/specs/2026-10-04-funktionen-demos.md b/docs/superpowers/specs/2026-10-04-funktionen-demos.md new file mode 100644 index 0000000..101db5c --- /dev/null +++ b/docs/superpowers/specs/2026-10-04-funktionen-demos.md @@ -0,0 +1,137 @@ +# Funktions-Demos im Abschnitt Kundenportal (2026-10-04) + +Auftrag des Inhabers (2026-10-04, sinngemäß): In der Handy-Sektion (Kundenportal-Bühne) die eine Station +„Funktionen“ herausnehmen und stattdessen die einzelnen Funktionen zeigen, die WEBklar anbietet: +**Terminplaner**, **Website-Analyse (Heatmap)** und **News-Pinnwand**. Je Funktion eine erfundene, interessante +Kunden-Website, bei der die Funktion Sinn ergibt. Auf dem **Laptop die richtige Website groß**, auf dem **Handy +das Kundenportal** (die Einrichtung/Verwaltung). Die Heatmap soll „fancy“ wirken: simulierte Klicks, die Heatmap +baut sich auf; **beim Hovern** sieht man, wo viel geklickt wurde, **darunter eine kleine Diagnose**. Beim +Terminplaner soll ein **Kalender (Google Kalender)** mit den Einträgen im Bild sein. Bei der Pinnwand eine +Website wie eine Gemeinde/Stadtverwaltung – „irgendwas Interessantes“. + +Dieses Dokument ergänzt `2026-10-01-relaunch-design.md` (Abschnitt 7.2 Kundenportal). Wo es abweicht, gilt es. + +## 1. Stationen der Bühne (Reihenfolge) + +1. Live-Vorschau · 2. Fortschritt · 3. Chat · 4. Rechnungen (unverändert) +5. **Terminplaner** · 6. **Website-Analyse** · 7. **News-Pinnwand** – Gruppe „Funktionen zum Dazubuchen“ +8. Als App (unverändert, bleibt die letzte Station mit dem langen Screenshot) + +Texte (Sie-Form, keine Erfolgsversprechen, keine Zahlen ohne Quelle): + +| Station | Titel | Unterschrift | +|---|---|---| +| 5 | Terminplaner | Gäste buchen auf Ihrer Website – die Termine sehen Sie im Portal und auf Wunsch im Google Kalender. | +| 6 | Website-Analyse | Sehen Sie, wohin Ihre Besucher klicken. Zeigen Sie mit der Maus auf die Seite. | +| 7 | News-Pinnwand | Neuigkeiten mit Text und Foto, vom Handy direkt auf Ihre Website. | + +Wahrheit der Aussagen (geprüft im Portal-Code und in den Spezifikationen): +- Terminplaner: Buchung im Widget, Gast bestätigt per Link in der E-Mail, Betrieb bekommt eine Mail + „Neue Buchung: {Leistung} am {Datum} um {Zeit}“ (K20), Termine stehen im Portal (Reiter Termine). Kalender: + **Kalender-Abo** je Person/Platz (geheimer .ics-Link), den man in Google Kalender, Apple Kalender oder Outlook + einfügt; Google aktualisiert nur alle paar Stunden. Es gibt **keinen** Zwei-Wege-Sync. Kalendereinträge für den + Betrieb heißen „{Leistung}: {Gastname}“. +- Website-Analyse: Heatmap der Klicks (Farbskala blau → grün → gelb → rot, wie `analytics/client/h.js`), + Seitenaufrufe, Besucher, Herkunft, Geräte, Kontakt-Klicks; nur mit Einwilligung; im Wartungs-Abo enthalten. +- News-Pinnwand: kurze Neuigkeiten mit Text und/oder Foto, „hängen wie Zettel an einer Pinnwand auf deiner Website“. + +## 2. Erfundene Beispiel-Kunden + +Alle drei sind erfunden und werden als Beispiel gekennzeichnet (kleiner Hinweis „Erfundenes Beispiel“ an der +Bühne). Adressen in der Safari-Leiste mit der reservierten Endung `.example` (RFC 2606), damit kein echter +Betrieb gemeint sein kann. Keine Fotos von Menschen, keine Stockfotos, keine fremden Logos: Bilder sind +selbst gezeichnete SVG/CSS-Grafiken. Keine Anfragen an fremde Server. + +| Funktion | Beispiel | Adresse | Warum | +|---|---|---|---| +| Terminplaner | **Salon Schnittstelle** – Friseur, drei Personen (Mia, Can, Lea) | `salon-schnittstelle.example` | Klassischer Fall: Leistung, Person, Zeit | +| Website-Analyse | **Boulderhalle Überhang** – Kletterhalle | `boulderhalle-ueberhang.example` | Viele Klickziele (Tageskarte, Live-Auslastung, Kurse, Preise, Öffnungszeiten, Routenfoto) | +| News-Pinnwand | **Ortsgemeinde Musterbach** – Gemeinde-Website | `musterbach.example` | Ständig Neues: Kerwe, Straßensperrung, Bücherzelle, Sprechstunde | + +## 3. Aufbau je Station + +Ab 900 px (Bühne): **MacBook** = die Website als Live-Komponente (keine Bilddatei), gesetzt in 1512 x 893, +der Rahmen verkleinert sie (bei 1440 px Fensterbreite ca. Faktor 0,41). Darum große Schrift (Fließtext +mindestens 22 Designpunkte, Überschriften 64+), große Flächen, wenig Kleinkram. **iPhone** = Kundenportal-Ansicht +der Funktion als Screenshot aus dem echten Portal-Code mit erfundenen Daten (Fixture-Server), Format wie die +übrigen Handy-Bilder (804 x 1748 @2x, ausgeliefert 390/780 px webp). Darüber dürfen kleine Live-Ebenen liegen +(Mitteilung, Tipp-Welle), die mit dem Ablauf der Website synchron laufen. + +Unter 900 px (Wisch-Reihe): je Funktion eine Karte; im iPhone-Rahmen läuft die **Website** in ihrer +Handy-Fassung (402 x 874, oben 59 / unten 34 frei für Statusleiste und Home-Balken). Die Heatmap-Karte +reagiert auf Tippen (Zone antippen ⇒ Diagnose unter der Karte). + +### 3.1 Terminplaner (Salon Schnittstelle) +- Website: Kopf mit Name/Navigation, große Überschrift, rechts das **Buchungs-Widget** im Stil des echten + Widgets (`kundenbereich…/build/public/embed/termine.js`, `termine-demo.html`): Leistung wählen → Person + („Egal“ möglich) → Tag und freie Zeit → Daten → „Termin buchen“ → Hinweis „Bitte bestätigen Sie den Termin + über den Link in Ihrer E-Mail.“ (der Gast bestätigt per Link). +- Kalender-Fenster über der Website (rechts oben/unten, im Stil einer Kalender-App, **ohne Google-Logo und ohne + Google-Farben/-Schrift als Marke**): Wochenansicht mit 6–9 Einträgen „{Leistung}: {Gastname}“, Kopfzeile + „Kalender-Abo · Salon Schnittstelle – Mia“, kleiner Hinweis „z. B. in Google Kalender“. Nach der Buchung + erscheint der neue Eintrag (weich eingeblendet). +- Ablauf (ca. 9–11 s, Schleife): Zeiger wählt Leistung → Person → Zeit → klickt „Termin buchen“ → Bestätigung + → Eintrag erscheint im Kalender; auf dem iPhone gleitet eine Mitteilung herein „Neue Buchung: Damenhaarschnitt + am Do., 8. Okt. um 14:30“ (Absender WEBklar Termine). Ohne Bewegung: Endzustand (gebucht, Eintrag da). +- iPhone: Portal-Ansicht Terminplaner, Reiter „Termine“ (Liste der Woche mit Status, ein Eintrag „angefragt“). + +### 3.2 Website-Analyse (Boulderhalle Überhang) +- Website: Navigation (Routen, Kurse, Preise, Öffnungszeiten), Held „Heute 14 neue Boulder“ mit großem Knopf + „Tageskarte kaufen“, Kachel **Live-Auslastung** (z. B. 42 %), Karten Schnupperkurs / Preise / Öffnungszeiten, + ein großes **Routenbild** (SVG-Wand mit bunten Griffen) – das ist kein Link, wird aber oft angeklickt + („toter Klick“), Telefonnummer im Kopf. 8–10 Zonen mit `data-zone`. +- Heatmap: Leinwand über der Website, Zeichnung mit `heat.ts` (gleicher Algorithmus wie das echte Overlay). + Klickpunkte sind Beispieldaten aus einer festen Saat (`zufall`), je Zone ein Gewicht; Summe z. B. 1.284 Klicks. +- Ablauf (Schleife): Zeiger fliegt Zonen an, Klickwelle, die Heatmap wird mit jedem Klick dichter (je Klick ein + Schub Punkte), nach ca. 10 s ist sie voll, bleibt ca. 6 s stehen, blendet aus, neu. Ohne Bewegung: volle Heatmap. +- **Hover (nur Maus)**: Sobald die Maus über der Website ist, stoppt der simulierte Zeiger, die Heatmap ist voll + sichtbar; die Zone unter der Maus bekommt einen Rahmen und ein Schildchen „Tageskarte kaufen · 402 Klicks“. + **Diagnose darunter** (unter dem MacBook, links neben dem iPhone, normaler Seitenmaßstab, Seitenfarben hell/dunkel): + Kopf „Diagnose · letzte 30 Tage · Beispieldaten“, Rangliste der 4 meistgeklickten Zonen mit Balken und Prozent, + dazu ein Satz zur gewählten Zone (ohne Hover: zur meistgeklickten). Beispiel-Sätze: „Am meisten geklickt: + ‚Tageskarte kaufen‘ – 31 % aller Klicks.“ / „Auffällig: 12 % klicken auf das Routenbild. Es ist kein Link – + hier lohnt sich ein Link zur Routenliste.“ Keine Erfolgsversprechen („mehr Umsatz“ o. Ä. verboten). +- Tastatur/Screenreader: die Ranglisten-Zeilen sind Knöpfe; Fokus/Klick wählt die Zone (Rahmen auf der Website). + Die Website selbst ist `aria-hidden` und enthält keine fokussierbaren Elemente. +- Handy-Karte: die Website in Handy-Fassung mit Heatmap; Tippen auf eine Zone zeigt die Diagnose unter der Karte. +- iPhone (Bühne): Portal-Ansicht Analyse (Kennzahlen + Diagramm). + +### 3.3 News-Pinnwand (Ortsgemeinde Musterbach) +- Website: Kopf mit erfundenem, schlichtem Wappen (SVG), Navigation (Rathaus, Leben in Musterbach, + Veranstaltungen, Pinnwand), Bereich „Neues aus Musterbach“ als **Pinnwand** (Kork o. ä. per CSS) mit 4–5 + leicht gedrehten Zetteln im Stil der echten News-Pinnwand (`…/public/embed/news.js`, `news-demo.html`): + „Kerwe 2026: Freitag bis Montag auf dem Dorfplatz“, „Hauptstraße ab 13.10. halbseitig gesperrt (Kanalbau)“, + „Neue Bücherzelle am Spielplatz“, „Sprechstunde des Ortsbürgermeisters: donnerstags 17–19 Uhr“ usw. + Mindestens ein Zettel mit „Foto“ (gezeichnete Grafik, z. B. Kerwe-Karussell oder Bücherzelle). +- Ablauf (Schleife): auf dem iPhone (Portal-Screenshot „News-Pinnwand“ mit dem neuen Zettel) tippt eine + Tipp-Welle auf „Veröffentlichen“ (Koordinaten aus der Aufnahme), kurz danach fällt der neue Zettel mit Nadel + auf die Pinnwand der Website (transform/opacity), Plakette „Neu“. Ohne Bewegung: Zettel hängt schon. +- iPhone: Portal-Ansicht News-Pinnwand (Musterbach-Daten). + +## 4. Technik + +- Ordner `src/components/home/demos/`: `gemeinsam.tsx/.css` (Taktgeber `useAblauf`, `useDemoLauf`, + `useBewegungErlaubt`, `useFeinerZeiger`, `useImBild`, `DemoZeiger`, `zufall`, `zahl`), `heat.ts` + (Heatmap-Zeichnung wie h.js). Je Demo ein Unterordner `termin/`, `heatmap/`, `pinnwand/`. +- Schnittstelle je Demo (eine Quelle der Wahrheit für Website und Handy-Ebene): + - `useAblauf(laeuft: boolean, statisch: boolean)` liefert den Zustand (Phase usw.). + - `Seite` (`{ zustand, variante: "desktop" | "handy" }`) = die Website auf der Fläche. + - optional `Handy` (`{ zustand }`) = Live-Ebene über dem Portal-Screenshot im iPhone. + - Heatmap zusätzlich `HeatmapDiagnose` (`{ zustand }`) und Auswahl/Hover im Zustand. +- `laeuft` = Station aktiv **und** Bühne/Karte im Bild **und** Tab sichtbar **und** Bewegung erlaubt. + Nichts läuft im Hintergrund (Lehre aus dem Leistungs-Audit 2026-09-25). +- Bewegung nur `transform`/`opacity` hinter `prefers-reduced-motion: no-preference`; Ausnahme (Auftrag des + Inhabers, ergänzt 8.4): das Neuzeichnen der Heatmap-Leinwand (nur bei Klicks, nicht je Frame). +- Hover-Effekte nur bei `(hover: hover) and (pointer: fine)`. +- Die Demos werden erst geladen, wenn die Bühne die Station fast erreicht (wie die Bilder, `eingehaengt`), + möglichst als eigener Chunk (`React.lazy`), damit die Startseite nicht schwerer wird. +- Inaktive Ebenen der Bühne bekommen `pointer-events: none`, damit nur die sichtbare Website auf die Maus reagiert. +- Tests: Vitest je Demo (Rendern Desktop/Handy, statischer Endzustand ohne Bewegung, Diagnose-Logik, + keine fokussierbaren Elemente in der Website); `relaunch-kundenportal.test.tsx` auf 8 Stationen angepasst. + `sofort-paket.test.tsx` (Scans gegen verbotene Aussagen) bleibt grün. + +## 5. Offen für den Inhaber + +- Terminplaner ist erst im Pilotbetrieb (TP2 „Verkauf“ noch nicht live). Vor dem Livegang des Relaunchs: + entweder TP2 live oder die Station kennzeichnen („bald buchbar“). +- Namen der Beispiel-Kunden und ihre Texte sind Vorschläge. diff --git a/public/relaunch/portal/desktop-7-news-pinnwand-1200.webp b/public/relaunch/portal/desktop-7-news-pinnwand-1200.webp deleted file mode 100644 index 301810d..0000000 Binary files a/public/relaunch/portal/desktop-7-news-pinnwand-1200.webp and /dev/null differ diff --git a/public/relaunch/portal/desktop-7-news-pinnwand-1600.webp b/public/relaunch/portal/desktop-7-news-pinnwand-1600.webp deleted file mode 100644 index 43d6966..0000000 Binary files a/public/relaunch/portal/desktop-7-news-pinnwand-1600.webp and /dev/null differ diff --git a/public/relaunch/portal/desktop-7-news-pinnwand-800.webp b/public/relaunch/portal/desktop-7-news-pinnwand-800.webp deleted file mode 100644 index fcd087e..0000000 Binary files a/public/relaunch/portal/desktop-7-news-pinnwand-800.webp and /dev/null differ diff --git a/public/relaunch/portal/phone-10-analyse-390.webp b/public/relaunch/portal/phone-10-analyse-390.webp new file mode 100644 index 0000000..0b8a41b Binary files /dev/null and b/public/relaunch/portal/phone-10-analyse-390.webp differ diff --git a/public/relaunch/portal/phone-10-analyse-780.webp b/public/relaunch/portal/phone-10-analyse-780.webp new file mode 100644 index 0000000..982d498 Binary files /dev/null and b/public/relaunch/portal/phone-10-analyse-780.webp differ diff --git a/public/relaunch/portal/phone-11-news-musterbach-390.webp b/public/relaunch/portal/phone-11-news-musterbach-390.webp new file mode 100644 index 0000000..39b4ebf Binary files /dev/null and b/public/relaunch/portal/phone-11-news-musterbach-390.webp differ diff --git a/public/relaunch/portal/phone-11-news-musterbach-780.webp b/public/relaunch/portal/phone-11-news-musterbach-780.webp new file mode 100644 index 0000000..4e6284f Binary files /dev/null and b/public/relaunch/portal/phone-11-news-musterbach-780.webp differ diff --git a/public/relaunch/portal/phone-8-news-vorschau-390.webp b/public/relaunch/portal/phone-8-news-vorschau-390.webp deleted file mode 100644 index f1c50e0..0000000 Binary files a/public/relaunch/portal/phone-8-news-vorschau-390.webp and /dev/null differ diff --git a/public/relaunch/portal/phone-8-news-vorschau-780.webp b/public/relaunch/portal/phone-8-news-vorschau-780.webp deleted file mode 100644 index 46027d1..0000000 Binary files a/public/relaunch/portal/phone-8-news-vorschau-780.webp and /dev/null differ diff --git a/public/relaunch/portal/phone-9-termine-390.webp b/public/relaunch/portal/phone-9-termine-390.webp new file mode 100644 index 0000000..eafaaa0 Binary files /dev/null and b/public/relaunch/portal/phone-9-termine-390.webp differ diff --git a/public/relaunch/portal/phone-9-termine-780.webp b/public/relaunch/portal/phone-9-termine-780.webp new file mode 100644 index 0000000..6dca1c9 Binary files /dev/null and b/public/relaunch/portal/phone-9-termine-780.webp differ diff --git a/scripts/relaunch-bilder.py b/scripts/relaunch-bilder.py index 6ddc7c0..61cb218 100644 --- a/scripts/relaunch-bilder.py +++ b/scripts/relaunch-bilder.py @@ -5,7 +5,10 @@ Relaunch: verkleinert die Rohexporte der Kundenportal-Screenshots für die Websi docs/relaunch/assets/portal/*.webp (Rohdateien, doppelte Auflösung) -> public/relaunch/portal/-.webp -Aufruf (aus dem Projektverzeichnis): python3 scripts/relaunch-bilder.py +Aufruf (aus dem Projektverzeichnis): python3 scripts/relaunch-bilder.py [NAME ...] + Ohne Argument werden alle Bilder neu geschrieben (alte Ausgaben vorher entfernt). Mit einem oder mehreren NAME-Filtern + (Dateiname ohne .webp, auch als Anfang, z. B. "phone-9" oder "phone-10-analyse") entstehen nur die passenden Bilder; + alle anderen Dateien in public/relaunch/portal bleiben unangetastet. Nur die Bilder, die der Abschnitt "Kundenportal" (Spezifikation 7.2) wirklich zeigt, landen in public/. Format und Budgets laut Spezifikation 9.5: @@ -45,13 +48,15 @@ BILDER = { "desktop-2-fortschritt": ("desktop", (800, 1200, 1600)), "desktop-3b-chat-foto": ("desktop", (800, 1200, 1600)), "desktop-4-rechnungen": ("desktop", (800, 1200, 1600)), - "desktop-7-news-pinnwand": ("desktop", (800, 1200, 1600)), # Handy (Roh 804 x 1748 = iPhone 16 Pro @2x, Safe Area 59/34; das lange Bild 804 x 5340, beschnitten auf 5131) "phone-1-projekte": ("handy", (390, 780)), "phone-3-projektdetails": ("handy", (390, 780)), "phone-4b-chat-foto": ("handy", (390, 780)), "phone-5-rechnungen": ("handy", (390, 780)), - "phone-8-news-vorschau": ("handy", (390, 780)), + # Funktions-Stationen (Spezifikation 2026-10-04-funktionen-demos.md): Handy-Ansicht der Funktion im Kundenportal + "phone-9-termine": ("handy", (390, 780)), + "phone-10-analyse": ("handy", (390, 780)), + "phone-11-news-musterbach": ("handy", (390, 780)), # langer Screenshot: behält sein hohes Seitenverhältnis (eigenes Budget, siehe BUDGET_LANG) "phone-2-projekte-lang": ("handy", (390, 780)), } @@ -68,16 +73,33 @@ def speichern(bild: Image.Image, ziel: Path, budget: int) -> tuple[int, int]: qualitaet -= SCHRITT -def main() -> int: +def waehle(filter_: list[str]) -> dict: + """Alle Bilder ohne Filter, sonst die, deren Name genau so heißt oder mit einem Filter beginnt (z. B. "phone-9").""" + if not filter_: + return BILDER + # "phone-1" soll nicht auch "phone-10-analyse" treffen: Anfang nur bis zum nächsten Bindestrich-Segment + treffer = { + name: wert for name, wert in BILDER.items() + if any(name == f or name.startswith(f + "-") for f in filter_) + } + unbekannt = [f for f in filter_ if not any(n == f or n.startswith(f + "-") for n in BILDER)] + if unbekannt: + raise SystemExit(f"Unbekannter Bildname: {', '.join(unbekannt)}") + return treffer + + +def main(argv: list[str]) -> int: ZIEL.mkdir(parents=True, exist_ok=True) - # alte Ausgaben entfernen, damit nichts Verwaistes liegen bleibt - for alt in ZIEL.glob("*.webp"): - alt.unlink() + bilder = waehle(argv) + if not argv: + # alte Ausgaben entfernen, damit nichts Verwaistes liegen bleibt (nur bei einem Lauf über alle Bilder) + for alt in ZIEL.glob("*.webp"): + alt.unlink() fehler = 0 summe = 0 print(f"{'Datei':44} {'Pixel':>11} {'KB':>7} {'Q':>3} Budget") - for name, (art, breiten) in BILDER.items(): + for name, (art, breiten) in bilder.items(): quelle = QUELLE / f"{name}.webp" if not quelle.exists(): print(f"FEHLT: {quelle}", file=sys.stderr) @@ -106,4 +128,4 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) + sys.exit(main(sys.argv[1:])) diff --git a/src/components/home/Kundenportal.tsx b/src/components/home/Kundenportal.tsx index e0c4b2b..4e2a984 100644 --- a/src/components/home/Kundenportal.tsx +++ b/src/components/home/Kundenportal.tsx @@ -1,6 +1,10 @@ -import { useEffect, useRef, useState, useSyncExternalStore } from "react"; +import { lazy, Suspense, useEffect, useRef, useState, useSyncExternalStore, version, type ComponentType, type ReactNode, type RefObject } from "react"; import WischReihe from "@/components/WischReihe"; import { DesktopFenster, HandyRahmen, RahmenBild, type RahmenBildDaten } from "@/components/GeraeteRahmen"; +import { useDemoLauf, useFeinerZeiger, useImBild } from "./demos/gemeinsam"; +import type { TerminZustand } from "./demos/termin/ablauf"; +import type { HeatmapZustand } from "./demos/heatmap/ablauf"; +import type { PinnwandZustand } from "./demos/pinnwand/ablauf"; import "./kundenportal.css"; /** @@ -8,6 +12,9 @@ import "./kundenportal.css"; * Ab 900 px: festgepinnte Bühne mit Funktionsliste links und Desktop-Fenster plus Handy-Rahmen rechts. * Darunter (unter 900 px): keine Bühne, sondern eine Wisch-Reihe mit je einem Handy-Screenshot. * Danach die Schrittzeile (ersetzt "So arbeiten wir"). + * Die Stationen 5 bis 7 (Terminplaner, Website-Analyse, News-Pinnwand) zeigen statt eines Bildes eine + * Live-Demo: auf dem MacBook die erfundene Kunden-Website, auf dem iPhone das Portal (Spezifikation + * 2026-10-04-funktionen-demos). Die Demos werden per React.lazy erst geladen, wenn die Bühne ihre Station fast erreicht. */ const EINLEITUNG = @@ -43,17 +50,32 @@ const handyBild = (name: string, alt: string, sizes: string, hoehe = 1696): Rahm alt, }); +type DemoArt = "termin" | "heatmap" | "pinnwand"; + type Funktion = { id: string; titel: string; unterschrift: string; - /** Desktop-Bild; Schritt 6 hat keines, dort blendet das Fenster aus. */ + /** Abweichende Unterschrift in der Wisch-Reihe (dort gibt es keine Maus). */ + unterschriftHandy?: string; + /** Desktop-Bild; Demo-Stationen zeigen stattdessen die Website, "Als App" hat keines (dort blendet das Fenster aus). */ desktop?: { name: string; alt: string }; handy: { name: string; alt: string }; /** Der lange Screenshot, der sich im Handy-Rahmen nach oben schiebt. */ lang?: boolean; + /** Live-Demo statt Desktop-Bild: welche erfundene Kunden-Website im MacBook läuft. */ + demo?: DemoArt; + /** Adresse in der Safari-Leiste (reservierte Endung .example, es ist kein echter Betrieb gemeint). */ + adresse?: string; + /** Beschreibung der Beispiel-Website für Screenreader (die Website selbst ist aria-hidden). */ + beschreibung?: string; + /** Hinweis unter dem MacBook (Terminplaner und Pinnwand; bei der Website-Analyse steht dort die Diagnose). */ + hinweis?: string; }; +/** Mitte des Knopfes "Aufhängen" im Portal-Screenshot der News-Pinnwand (Designpunkte der iPhone-Fläche 402 x 874). */ +const AUFHAENGEN_PUNKT = { x: 201, y: 613 }; + // Reihenfolge und Texte laut Spezifikation 7.2. Platzhalter des Portals ("kommt bald") werden nicht beworben, // weitere Ansichten nur nach Freigabe des Inhabers (O9 = nein). const FUNKTIONEN: Funktion[] = [ @@ -92,16 +114,45 @@ const FUNKTIONEN: Funktion[] = [ handy: { name: "phone-5-rechnungen", alt: "Kundenportal auf dem Handy: Rechnungen zum Ansehen, Bezahlen und Herunterladen" }, }, { - id: "funktionen", - titel: "Funktionen", - unterschrift: "Bausteine zum Dazubuchen, zum Beispiel eine News-Pinnwand für Ihre Website.", - desktop: { - name: "desktop-7-news-pinnwand", - alt: "Kundenportal: News-Pinnwand mit Vorschau, wie die Meldung auf der Website aussieht", - }, + id: "terminplaner", + titel: "Terminplaner", + unterschrift: "Gäste buchen auf Ihrer Website – die Termine sehen Sie im Portal und auf Wunsch im Google Kalender.", + demo: "termin", + adresse: "salon-schnittstelle.example", + beschreibung: + "Beispiel-Website eines Friseursalons mit Buchungsformular und einem Kalender, in dem der neue Termin erscheint", + hinweis: + "Kalender-Abo: Den Link einmal in Google Kalender, Apple Kalender oder Outlook einfügen. Neue Termine erscheinen dort mit etwas Verzögerung.", handy: { - name: "phone-8-news-vorschau", - alt: "Kundenportal auf dem Handy: Vorschau der News-Pinnwand für die Website", + name: "phone-9-termine", + alt: "Kundenportal von WEBklar auf dem Handy: Der Terminplaner zeigt die Termine des Donnerstags, darunter eine bestätigte Buchung, eine neue Anfrage, die Sie annehmen, ablehnen oder verschieben können, und einen weiteren bestätigten Termin.", + }, + }, + { + id: "website-analyse", + titel: "Website-Analyse", + unterschrift: "Sehen Sie, wohin Ihre Besucher klicken. Zeigen Sie mit der Maus auf die Seite.", + unterschriftHandy: "Sehen Sie, wohin Ihre Besucher klicken. Tippen Sie auf eine Stelle der Seite.", + demo: "heatmap", + adresse: "boulderhalle-ueberhang.example", + beschreibung: + "Beispiel-Website einer Boulderhalle, darüber eine Heatmap: Je häufiger eine Stelle angeklickt wird, desto wärmer ihre Farbe", + handy: { + name: "phone-10-analyse", + alt: "Kundenportal von WEBklar auf dem Handy: Die Website-Analyse zeigt für die letzten 30 Tage Seitenaufrufe, Besucher, Kontakt-Klicks und die Zustimmungsquote, darunter ein Diagramm mit den Seitenaufrufen pro Tag.", + }, + }, + { + id: "news-pinnwand", + titel: "News-Pinnwand", + unterschrift: "Neuigkeiten mit Text und Foto, vom Handy direkt auf Ihre Website.", + demo: "pinnwand", + adresse: "musterbach.example", + beschreibung: "Beispiel-Website einer Ortsgemeinde mit einer Pinnwand, an der ein neuer Zettel aufgehängt wird", + hinweis: "Neuer Zettel: Er erscheint nach dem Aufhängen auf der Website.", + handy: { + name: "phone-11-news-musterbach", + alt: "Kundenportal von WEBklar auf dem Handy: Ein neuer Zettel für die News-Pinnwand mit Überschrift und Text ist ausgefüllt, darunter der Knopf zum Aufhängen auf der Website.", }, }, { @@ -117,6 +168,51 @@ const FUNKTIONEN: Funktion[] = [ ]; const LETZTER = FUNKTIONEN.length - 1; +/** Stationen vor "Als App": normale Schritte und längere Demo-Schritte (bestimmen die Scrollstrecke der Hülle). */ +const ANZAHL_DEMO = FUNKTIONEN.filter((f) => f.demo).length; +const ANZAHL_NORMAL = LETZTER - ANZAHL_DEMO; +const GRUPPE_TITEL = "Funktionen zum Dazubuchen"; + +// ── Demos (eigener Chunk, erst geladen, wenn die Bühne die Station fast erreicht) ────────────────────── + +/** + * React.lazy mit `vorladen`: Der Chunk wird beim Einhängen der Station angestoßen, nicht erst, wenn die Komponente + * zum ersten Mal gerendert wird. So laden Website, Handy-Ebene und Taktgeber gleichzeitig (kein Wasserfall). + */ +const lazyMitVorladen = (laden: () => Promise<{ default: ComponentType

}>) => { + let versprechen: ReturnType | undefined; + const lade = () => (versprechen ??= laden()); + const Komponente = lazy(lade); + return Object.assign(Komponente, { vorladen: () => void lade().catch(() => {}) }); +}; + +const TerminSteuerung = lazyMitVorladen(() => import("./demos/steuerung").then((m) => ({ default: m.TerminSteuerung }))); +const HeatmapSteuerung = lazyMitVorladen(() => import("./demos/steuerung").then((m) => ({ default: m.HeatmapSteuerung }))); +const PinnwandSteuerung = lazyMitVorladen(() => import("./demos/steuerung").then((m) => ({ default: m.PinnwandSteuerung }))); +const TerminSeite = lazyMitVorladen(() => import("./demos/termin/TerminSeite")); +const TerminHandy = lazyMitVorladen(() => import("./demos/termin/TerminHandy")); +const HeatmapSeite = lazyMitVorladen(() => import("./demos/heatmap/HeatmapSeite")); +const HeatmapDiagnose = lazyMitVorladen(() => import("./demos/heatmap/HeatmapDiagnose")); +const PinnwandSeite = lazyMitVorladen(() => import("./demos/pinnwand/PinnwandSeite")); +const PinnwandHandy = lazyMitVorladen(() => import("./demos/pinnwand/PinnwandHandy")); + +/** Chunks je Demo; die Website-Chunks (Seite, Diagnose) und der Taktgeber laden gleichzeitig. */ +const VORLADEN: Record void)[]> = { + termin: [TerminSteuerung.vorladen, TerminSeite.vorladen, TerminHandy.vorladen], + heatmap: [HeatmapSteuerung.vorladen, HeatmapSeite.vorladen, HeatmapDiagnose.vorladen], + pinnwand: [PinnwandSteuerung.vorladen, PinnwandSeite.vorladen, PinnwandHandy.vorladen], +}; + +/** Hintergrund der Beispiel-Websites, solange ihr Chunk lädt (statt einer schwarzen Fläche im MacBook). */ +const PLATZHALTER: Record = { termin: "#f6f1ea", heatmap: "#141414", pinnwand: "#f7f2e6" }; +const Platzhalter = ({ art }: { art: DemoArt }) => ( +