import { useEffect, useState } from "react"; import { Play, Pause, X, Gauge } from "lucide-react"; import { cn } from "@/lib/utils"; import { SPEED_OPTIONS, type Speed, useDemoEnd, useDemoState, useDemoTick, } from "@/lib/demo"; import { Gem } from "@/components/brand/Gem"; import { DemoEmailSlide } from "./DemoEmailSlide"; /** * Horloge virtuelle de démo — visible top-right de _app, uniquement * quand `org.demoMode = true`. * * Anatomie : * ┌─────────────────────────────────────┐ * │ vendredi 18 mai 2026 │ * │ ◆────●───────────── J+5 / →prochain│ * │ [▶] 1x 2x 5x [↻] [×] │ * └─────────────────────────────────────┘ * * - Date pleine, font display, mise à jour live à chaque frame * - Rail rubis-glow avec une pastille qui glisse de virtualNow vers le * prochain event (proportion calculée backend → SPA) * - Play/Pause + sélecteur de vitesse 1x/2x/5x * - Bouton fermer = `/demo/end` * * Quand un event est déclenché, la slide-over droite s'ouvre avec * l'email capturé. L'horloge est en pause tant que tous les events * en attente n'ont pas été acquittés (clic "Continuer"). */ const FR_DATE = new Intl.DateTimeFormat("fr-FR", { weekday: "long", day: "numeric", month: "long", year: "numeric", }); export function DemoClock() { const { data: state } = useDemoState(); const endMutation = useDemoEnd(); const enabled = state?.demoMode === true; const tick = useDemoTick({ enabled, initialVirtualNow: state?.virtualNow, }); // Re-sync local virtualNow quand le backend change (start/reset) useEffect(() => { if (state?.virtualNow) tick.resetTo(state.virtualNow); // eslint-disable-next-line react-hooks/exhaustive-deps }, [state?.virtualNow, enabled]); if (!enabled) return null; const dateStr = FR_DATE.format(tick.virtualNow); // Progression vers le prochain event (0..1) — sert au rail visuel. const progress = computeProgress({ virtualNow: tick.virtualNow, nextEventAt: state?.nextEventAt ?? null, }); const hasPending = tick.pendingEvents.length > 0; return ( <>
{/* En-tête : date + tag DÉMO */}

{dateStr}

Mode démo · horloge virtuelle

{/* Rail rubis-glow avec pastille qui glisse */}
{/* Controls */}

{state?.nextEventAt ? `→ ${shortNextLabel(tick.virtualNow, state.nextEventAt)}` : "aucun event en file"}

{/* Slide-over : empile les events fired à acquitter un par un */} {hasPending && ( )} ); } function SpeedSelector({ value, onChange, }: { value: Speed; onChange: (s: Speed) => void; }) { return (
); } function computeProgress({ virtualNow, nextEventAt, }: { virtualNow: Date; nextEventAt: string | null; }): number { if (!nextEventAt) return 0; const next = new Date(nextEventAt).getTime(); // On affiche la progression sur une fenêtre de 30 jours autour du prochain event // (évite que la pastille soit collée à 0% ou 100% en permanence). const start = next - 30 * 86400000; const now = virtualNow.getTime(); if (now <= start) return 0; if (now >= next) return 1; return (now - start) / (next - start); } function shortNextLabel(now: Date, iso: string): string { const next = new Date(iso).getTime(); const diffMs = next - now.getTime(); if (diffMs <= 0) return "imminent"; const days = Math.round(diffMs / 86400000); if (days <= 0) return "aujourd'hui"; return `dans ${days} j`; }