'use client' import { useEffect, useState } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { AlarmClock, Plus, Timer as TimerIcon } from 'lucide-react' import TimerModal, { type TimerModalProps } from './TimerModal' interface Timer { id: string label: string startedAt: string endsAt: string agent?: 'cosmo' | 'lusya' } function formatRemaining(ms: number): { big: string; small?: string } { if (ms <= 0) return { big: '0:00' } const total = Math.ceil(ms / 1000) const h = Math.floor(total / 3600) const m = Math.floor((total % 3600) / 60) const s = total % 60 if (h > 0) { return { big: `${h}:${m.toString().padStart(2, '0')}`, small: `${s.toString().padStart(2, '0')}` } } return { big: `${m}:${s.toString().padStart(2, '0')}` } } export default function TimerHomeWidget() { const [timers, setTimers] = useState([]) const [, setTick] = useState(0) const [modal, setModal] = useState(null) const fetchTimers = async () => { try { const r = await fetch('/api/voice/timer') if (!r.ok) return const d = await r.json() setTimers(d.timers || []) } catch {} } useEffect(() => { let es: EventSource | null = null let retry: ReturnType | null = null let closed = false const connect = () => { es = new EventSource('/api/voice/stream') es.onmessage = (e) => { try { const evt = JSON.parse(e.data) if (evt.event === 'timer_start' || evt.event === 'timer_cancel') { fetchTimers() } } catch {} } es.onerror = () => { if (closed) return es?.close() retry = setTimeout(connect, 3000) } } fetchTimers() connect() return () => { closed = true if (retry) clearTimeout(retry) es?.close() } }, []) useEffect(() => { const t = setInterval(() => setTick(x => x + 1), 500) return () => clearInterval(t) }, []) const now = Date.now() const active = timers .filter(t => new Date(t.endsAt).getTime() > now - 30_000) .sort((a, b) => new Date(a.endsAt).getTime() - new Date(b.endsAt).getTime()) // Keep reference to the currently-open timer updated as it ticks down // (so modal shows fresh countdown without reopening) const modalTimer = modal?.type === 'control' ? active.find(t => t.id === modal.timer.id) || modal.timer : null const resolvedMode: TimerModalProps['mode'] = modal ? modal.type === 'create' ? modal : modalTimer ? { type: 'control', timer: modalTimer } : null // timer исчез (отменили / прозвенел >30с) — закроем : null useEffect(() => { // Если таймер пропал из active, автоматом закрыть модалку if (modal?.type === 'control' && !modalTimer) { setModal(null) } }, [modal, modalTimer]) return ( <>
Таймеры {active.length > 0 && ( {active.length} )}
{active.length === 0 ? ( ) : (
{active.map(t => { const end = new Date(t.endsAt).getTime() const start = new Date(t.startedAt).getTime() const total = Math.max(1, end - start) const remain = Math.max(0, end - now) const progress = Math.max(0, Math.min(1, 1 - remain / total)) const imminent = remain > 0 && remain < 10_000 const expired = remain <= 0 const time = formatRemaining(remain) const accent = expired ? '#f87171' : imminent ? '#fb923c' : '#818cf8' const accentBg = expired ? 'linear-gradient(135deg, rgba(239,68,68,0.14), rgba(239,68,68,0.06))' : imminent ? 'linear-gradient(135deg, rgba(251,146,60,0.14), rgba(251,146,60,0.06))' : 'var(--surface-2)' return ( setModal({ type: 'control', timer: t })} style={{ position: 'relative', overflow: 'hidden', padding: '16px 18px', borderRadius: 16, background: accentBg, border: `1px solid ${expired || imminent ? accent + '55' : 'var(--border-subtle)'}`, display: 'flex', alignItems: 'center', gap: 14, cursor: 'pointer', textAlign: 'left', minHeight: 68, width: '100%', }} >
{/* Big countdown */}
{time.big}
{time.small && (
:{time.small}
)}
{/* Label */}
{t.label}
{expired ? 'прозвенел — тап чтобы закрыть' : 'тап чтобы изменить'}
) })}
)}
setModal(null)} /> ) }