Relaunch: Ablauf der Station Öffnungszeiten live
Reine Logik (zeiten/ablauf.ts): statusFuer/formatUhr für die Bäckerei am Montag (6 bis 18 Uhr), useZeitenAblauf mit Uhr 05:50 bis 18:10, Tipp auf „Hinweis-Leiste anlegen“ und Hinweis-Leiste; Bedienen stoppt den Ablauf, ohne Bewegung steht der Endzustand. Logo, Stempel und Pfeil der Bäckerei-Seite unverändert nach baeckerei/teile.tsx verschoben. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4139f31925
commit
f7147fcd30
5 files changed
+619
-41
No files matched your search
@@ -0,0 +1,191 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useAblauf } from "../gemeinsam";
|
||||
|
||||
/**
|
||||
* Ablauf der Station „Öffnungszeiten live“ (Bäckerei Sonnenschein), Spezifikation 10.
|
||||
* Reine Logik, kein CSS und keine Adresse: die Komponenten werden später per React.lazy nachgeladen.
|
||||
*
|
||||
* Geschichte (Demo-Datum: Montag 5. Oktober 2026, die Uhr zählt Minuten ab Mo 00:00):
|
||||
* start (kurz Ruhe bei 05:50, „Geschlossen · öffnet heute um 6 Uhr“)
|
||||
* -> lauf (die Uhr läuft in Zehn-Minuten-Schritten bis 18:10; um 06:00 springt der Status auf „Jetzt geöffnet“,
|
||||
* um 18:00 auf „Geschlossen · öffnet morgen um 6 Uhr“)
|
||||
* -> tippen (auf dem Handy gleitet der Finger zu „Hinweis-Leiste anlegen“ und tippt)
|
||||
* -> leiste (die Hinweis-Leiste „Betriebsurlaub 12.–26. Oktober“ gleitet auf der Website ein, bleibt stehen) -> von vorn.
|
||||
*
|
||||
* Bedienen: Wer den Uhr-Regler anfasst (`setzeUhr(minute)`), stoppt den Ablauf. Die Demo zeigt dann den ganzen Stand
|
||||
* (mit Leiste) bei der gewählten Uhrzeit, ohne Timer. `setzeUhr(null)` gibt an den Ablauf zurück (von vorn).
|
||||
*/
|
||||
|
||||
export type ZeitenPhase = "start" | "lauf" | "tippen" | "leiste";
|
||||
|
||||
export type ZeitenStatus = {
|
||||
offen: boolean;
|
||||
/** Langtext der Status-Pille, z. B. „Jetzt geöffnet · schließt um 18 Uhr“ (nie „24“). */
|
||||
text: string;
|
||||
/** Kurztext: „Geöffnet“ oder „Geschlossen“. */
|
||||
kurz: string;
|
||||
};
|
||||
|
||||
export type ZeitenZustand = {
|
||||
phase: ZeitenPhase;
|
||||
/** Minuten ab Montag, 5. Oktober 2026, 00:00. */
|
||||
uhr: number;
|
||||
status: ZeitenStatus;
|
||||
/** Ob die Hinweis-Leiste auf der Website steht (Phase „leiste“, im Endzustand und beim Bedienen). */
|
||||
leiste: boolean;
|
||||
/**
|
||||
* Zähler der Tipps für DemoZeiger: 0 solange der Finger nur gleitet oder nicht zu sehen ist, ab dem Tipp eine je
|
||||
* Durchlauf wachsende Zahl (so erscheint die Klickwelle in jeder Runde neu). Der Finger ist nur in Phase „tippen“ zu sehen.
|
||||
*/
|
||||
klick: number;
|
||||
/** true = der Regler wurde angefasst: der Ablauf steht, die Uhr zeigt den gewählten Wert. */
|
||||
bedient: boolean;
|
||||
/** true = der Automatiklauf spielt gerade (nicht beim Bedienen, nicht ohne Bewegung). */
|
||||
laeuft: boolean;
|
||||
/** true = keine Bewegung erlaubt: die Demo zeigt ihren Endzustand (18:10 mit Leiste). */
|
||||
statisch: boolean;
|
||||
/** Uhr setzen (Regler bedient) oder mit `null` zurück an den Ablauf geben. Die Funktion bleibt über Renderings gleich. */
|
||||
setzeUhr: (min: number | null) => void;
|
||||
};
|
||||
|
||||
// ── Konstanten ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Reglerbereich in Minuten ab Mo 00:00: 05:00 bis 22:00 in Schritten von 10 Minuten. */
|
||||
export const ZEIT_MIN = 300;
|
||||
export const ZEIT_MAX = 1320;
|
||||
export const ZEIT_SCHRITT = 10;
|
||||
/** Uhrzeit, mit der der Automatiklauf beginnt (05:50) und bei der er endet (18:10). */
|
||||
export const START_UHR = 350;
|
||||
export const ENDE_UHR = 1090;
|
||||
|
||||
/** Text der Hinweis-Leiste (der Demo-Tag liegt im 7-Tage-Vorlauf des Urlaubs). */
|
||||
export const HINWEIS_TEXT = "Betriebsurlaub 12.–26. Oktober";
|
||||
|
||||
/**
|
||||
* Platzhalter: Mitte des Knopfes „Hinweis-Leiste anlegen“ im Portal-Screenshot (Designpunkte der Handy-Fläche 402 x 874).
|
||||
* Der Einbau setzt später die echte Position aus der Aufnahme ein (Prop `punkt` der Handy-Ebene).
|
||||
*/
|
||||
export const HINWEIS_ANLEGEN_PUNKT = { x: 201, y: 740 } as const;
|
||||
|
||||
// ── Status: Bäckerei Sonnenschein, Montag 6 bis 18 Uhr ──────────────────────────
|
||||
|
||||
const OEFFNET = 6 * 60;
|
||||
const SCHLIESST = 18 * 60;
|
||||
|
||||
const ZU_HEUTE: ZeitenStatus = Object.freeze({ offen: false, text: "Geschlossen · öffnet heute um 6 Uhr", kurz: "Geschlossen" });
|
||||
const OFFEN: ZeitenStatus = Object.freeze({ offen: true, text: "Jetzt geöffnet · schließt um 18 Uhr", kurz: "Geöffnet" });
|
||||
const ZU_MORGEN: ZeitenStatus = Object.freeze({ offen: false, text: "Geschlossen · öffnet morgen um 6 Uhr", kurz: "Geschlossen" });
|
||||
|
||||
/** Status am Montag (Minute 0 bis 1439 ab Mo 00:00): vor 06:00 zu, bis 17:59 offen, ab 18:00 zu bis morgen früh. */
|
||||
export const statusFuer = (minute: number): ZeitenStatus =>
|
||||
minute < OEFFNET ? ZU_HEUTE : minute < SCHLIESST ? OFFEN : ZU_MORGEN;
|
||||
|
||||
/** Minuten ab Mitternacht als „05:50“ (die Stunde läuft 00 bis 23, nie „24:00“). */
|
||||
export const formatUhr = (min: number): string => {
|
||||
const tag = ((Math.round(min) % 1440) + 1440) % 1440;
|
||||
return `${String(Math.floor(tag / 60)).padStart(2, "0")}:${String(tag % 60).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
// ── Schritte ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Alle Dauern sind Vielfache von 50 ms (die Tests spulen in 50-ms-Schritten vor). */
|
||||
const START_HALTEN = 1000;
|
||||
/** Dauer eines Uhr-Schrittes (10 Minuten) im Lauf. */
|
||||
const TAKT = 100;
|
||||
/** Wie lange 18:10 („Geschlossen · öffnet morgen um 6 Uhr“) stehen bleibt, bevor der Finger kommt. */
|
||||
const ENDE_HALTEN = 1500;
|
||||
/** Der Finger gleitet zum Knopf (Phase „tippen“), dann folgt der Tipp (ebenfalls Phase „tippen“). */
|
||||
const GLEITEN = 1000;
|
||||
const TIPPEN = 450;
|
||||
/** Die Leiste gleitet ein und bleibt stehen. */
|
||||
const LEISTE_HALTEN = 5000;
|
||||
|
||||
type Schritt = { phase: ZeitenPhase; uhr: number; dauer: number; tipp?: boolean };
|
||||
|
||||
const SCHRITTE: readonly Schritt[] = [
|
||||
{ phase: "start", uhr: START_UHR, dauer: START_HALTEN },
|
||||
...Array.from({ length: (ENDE_UHR - START_UHR) / ZEIT_SCHRITT }, (_, i): Schritt => {
|
||||
const uhr = START_UHR + (i + 1) * ZEIT_SCHRITT;
|
||||
return { phase: "lauf", uhr, dauer: uhr === ENDE_UHR ? ENDE_HALTEN : TAKT };
|
||||
}),
|
||||
{ phase: "tippen", uhr: ENDE_UHR, dauer: GLEITEN },
|
||||
{ phase: "tippen", uhr: ENDE_UHR, dauer: TIPPEN, tipp: true },
|
||||
{ phase: "leiste", uhr: ENDE_UHR, dauer: LEISTE_HALTEN },
|
||||
];
|
||||
const DAUERN = SCHRITTE.map((s) => s.dauer);
|
||||
const TIPP_SCHRITT = SCHRITTE.findIndex((s) => s.tipp);
|
||||
|
||||
/** Gesamtdauer eines Durchlaufs in Millisekunden (für Tests und die Feinabstimmung der Bühne). */
|
||||
export const ZEITEN_RUNDE_MS = DAUERN.reduce((a, b) => a + b, 0);
|
||||
|
||||
/** Anfangszustand (Phase „start“, 05:50, ohne Leiste): die Bühne zeigt die Website damit sofort, bevor der Taktgeber geladen ist. */
|
||||
export const ZEITEN_START: ZeitenZustand = {
|
||||
phase: "start",
|
||||
uhr: START_UHR,
|
||||
status: statusFuer(START_UHR),
|
||||
leiste: false,
|
||||
klick: 0,
|
||||
bedient: false,
|
||||
laeuft: false,
|
||||
statisch: false,
|
||||
setzeUhr: () => {},
|
||||
};
|
||||
|
||||
// ── Hook ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Taktgeber der Station. `laeuft` = Station aktiv, im Bild, Tab sichtbar, Bewegung erlaubt.
|
||||
* Läuft sie nicht, steht die Demo bei „start“ (05:50, nichts bewegt sich, kein Timer); eine Bedienung verfällt dann.
|
||||
* Mit `statisch` (keine Bewegung erlaubt) zeigt sie sofort den Endzustand (18:10 mit Leiste), ohne Timer;
|
||||
* der Regler bleibt auch dann bedienbar (eine Eingabe ist keine Animation).
|
||||
*/
|
||||
export const useZeitenAblauf = (laeuft: boolean, statisch: boolean): ZeitenZustand => {
|
||||
const [manuell, setManuell] = useState<number | null>(null);
|
||||
// Eine Bedienung gilt nur, solange die Station läuft (oder statisch steht); sonst verfällt sie
|
||||
const gewaehlt = laeuft || statisch ? manuell : null;
|
||||
const spielt = laeuft && !statisch && gewaehlt === null;
|
||||
const { schritt, runde } = useAblauf(spielt, DAUERN, true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!laeuft && !statisch && manuell !== null) setManuell(null);
|
||||
}, [laeuft, statisch, manuell]);
|
||||
|
||||
const setzeUhr = useCallback((min: number | null) => {
|
||||
if (min === null) {
|
||||
setManuell(null);
|
||||
return;
|
||||
}
|
||||
if (!Number.isFinite(min)) return;
|
||||
setManuell(Math.min(ZEIT_MAX, Math.max(ZEIT_MIN, Math.round(min))));
|
||||
}, []);
|
||||
|
||||
return useMemo<ZeitenZustand>(() => {
|
||||
if (statisch || gewaehlt !== null) {
|
||||
// Endzustand bzw. bedient: die Demo steht, die Leiste ist da
|
||||
const uhr = gewaehlt ?? ENDE_UHR;
|
||||
return {
|
||||
phase: "leiste",
|
||||
uhr,
|
||||
status: statusFuer(uhr),
|
||||
leiste: true,
|
||||
klick: 0,
|
||||
bedient: gewaehlt !== null,
|
||||
laeuft: false,
|
||||
statisch,
|
||||
setzeUhr,
|
||||
};
|
||||
}
|
||||
const aktuell = SCHRITTE[spielt ? Math.min(schritt, SCHRITTE.length - 1) : 0];
|
||||
return {
|
||||
phase: aktuell.phase,
|
||||
uhr: aktuell.uhr,
|
||||
status: statusFuer(aktuell.uhr),
|
||||
leiste: aktuell.phase === "leiste",
|
||||
klick: spielt && schritt >= TIPP_SCHRITT ? runde + 1 : 0,
|
||||
bedient: false,
|
||||
laeuft: spielt,
|
||||
statisch: false,
|
||||
setzeUhr,
|
||||
};
|
||||
}, [statisch, gewaehlt, spielt, schritt, runde, setzeUhr]);
|
||||
};
|
||||
Reference in new issue
Block a user