import { useState, useMemo, useEffect } from 'react'; import type { Transaction } from '../data/mockData'; import type { TransactionArc } from '../data/arcData'; export interface TimeFrame { label: string; from: number; // Unix ms to: number; // Unix ms } function buildFrames(periodDays: number): TimeFrame[] { const now = Date.now(); const start = now - periodDays * 24 * 60 * 60 * 1000; const fmt = (ms: number, opts: Intl.DateTimeFormatOptions) => new Date(ms).toLocaleDateString('fr-FR', opts); if (periodDays === 1) { return Array.from({ length: 24 }, (_, i) => { const from = start + i * 3_600_000; const to = from + 3_600_000; const h = new Date(from).getHours(); return { label: `${fmt(from, { weekday: 'short', day: 'numeric', month: 'short' })} · ${h}h – ${h + 1}h`, from, to, }; }); } if (periodDays === 7) { return Array.from({ length: 7 }, (_, i) => { const from = start + i * 86_400_000; const to = from + 86_400_000; return { label: fmt(from, { weekday: 'long', day: 'numeric', month: 'short' }), from, to, }; }); } // 30 days → half-week frames (3.5 days ≈ 9–10 frames) const HALF_WEEK = 3.5 * 86_400_000; const frames: TimeFrame[] = []; let cursor = start; while (cursor < now) { const from = cursor; const to = Math.min(cursor + HALF_WEEK, now); frames.push({ label: `${fmt(from, { weekday: 'short', day: 'numeric', month: 'short' })} – ${fmt(to - 1, { weekday: 'short', day: 'numeric', month: 'short' })}`, from, to, }); cursor = to; } return frames; } export function useAnimation(transactions: Transaction[], arcs: TransactionArc[], periodDays: number, allTimestamps: number[] = []) { const [active, setActive] = useState(false); const [currentIndex, setCurrentIndex] = useState(0); const [playing, setPlaying] = useState(false); const [speed, setSpeed] = useState<1 | 2 | 4>(2); const frames = useMemo(() => buildFrames(periodDays), [periodDays]); // Reset cursor when period or activation changes. // Stop playback only on deactivation — not on activation, so activate() can // start playing immediately without being overridden by this effect. useEffect(() => { setCurrentIndex(0); if (!active) setPlaying(false); }, [periodDays, active]); // Auto-advance: one step every (2000 / speed) ms useEffect(() => { if (!playing || !active) return; const delay = 1500 / speed; // ×1=1500ms, ×2=750ms, ×4=375ms const t = setTimeout(() => { setCurrentIndex((i) => { if (i >= frames.length - 1) { setPlaying(false); return i; } return i + 1; }); }, delay); return () => clearTimeout(t); }, [playing, active, currentIndex, speed, frames.length]); const visibleTransactions = useMemo(() => { if (!active || frames.length === 0) return transactions; const frame = frames[currentIndex]; if (!frame) return transactions; return transactions.filter((t) => t.timestamp >= frame.from && t.timestamp < frame.to); }, [active, transactions, frames, currentIndex]); const visibleArcs = useMemo(() => { if (!active || frames.length === 0) return arcs; const frame = frames[currentIndex]; if (!frame) return arcs; return arcs.filter((a) => a.timestamp >= frame.from && a.timestamp < frame.to); }, [active, arcs, frames, currentIndex]); // Nombre total de transfers (géo + non-géo) dans la frame courante const frameTotalCount = useMemo(() => { if (!active || frames.length === 0 || allTimestamps.length === 0) return null; const frame = frames[currentIndex]; if (!frame) return null; return allTimestamps.filter((ts) => ts >= frame.from && ts < frame.to).length; }, [active, allTimestamps, frames, currentIndex]); return { active, activate: () => { setActive(true); setSpeed(1); setPlaying(true); }, deactivate: () => { setActive(false); }, playing, play: () => setPlaying(true), pause: () => setPlaying(false), currentIndex, seek: (i: number) => { setCurrentIndex(i); setPlaying(false); }, speed, setSpeed, frames, currentFrame: frames[currentIndex] ?? null, visibleTransactions, visibleArcs, frameTotalCount, }; }