Files
egorin/src/components/RadioMiniWindow.tsx
T

124 lines
7.9 KiB
TypeScript

import { createPortal } from 'react-dom';
import { Radio, X, Volume2, VolumeX, Play, Megaphone, ListChecks, Trash2, CheckSquare, Square, Volume1 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useRadio, speak, beep } from '@/core/radio';
const tm = (iso: string) => new Date(iso).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const DOT: Record<string, string> = { critical: 'bg-crit', warning: 'bg-warn', info: 'bg-info', ok: 'bg-ok' };
// Мини-окно «Рация»: включение/отключение оповещения + эфир. Открывается кнопкой в шапке.
export function RadioMiniWindow({ open, onClose }: { open: boolean; onClose: () => void }) {
const { on, ether, toggle, board, readList, toggleTask, removeBoard, enqueueRead, removeRead, clearRead, readAll } = useRadio();
if (!open) return null;
const openTasks = board.filter((b) => b.kind === 'task' && !b.done).length;
const test = () => {
beep(on ? 880 : 440, 0.14, 0.25);
if (on) speak('Проверка связи. Рация в эфире.');
};
return createPortal(
<div className="fixed bottom-4 right-4 z-[3500] w-[300px] overflow-hidden rounded-2xl border border-border bg-surface shadow-card">
<div className="flex items-center justify-between border-b border-border px-3 py-2.5">
<div className="flex items-center gap-2">
<span className={cn('grid h-7 w-7 place-items-center rounded-lg', on ? 'bg-accent/20 text-accent' : 'bg-muted text-fg-muted')}>
<Radio className="h-4 w-4" />
</span>
<div>
<div className="text-sm font-semibold leading-tight">Рация</div>
<div className="text-[10px] text-fg-muted">{on ? 'оповещение включено' : 'оповещение выключено'}</div>
</div>
</div>
<button onClick={onClose} className="grid h-7 w-7 place-items-center rounded-lg text-fg-muted hover:bg-muted hover:text-fg"><X className="h-4 w-4" /></button>
</div>
{/* Вкл/выкл оповещение */}
<div className="flex items-center gap-2 px-3 py-3">
<button
onClick={toggle}
className={cn('flex flex-1 items-center justify-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold transition-colors', on ? 'bg-accent text-[#06210f] hover:opacity-90' : 'border border-border text-fg hover:bg-muted')}
>
{on ? <Volume2 className="h-4 w-4" /> : <VolumeX className="h-4 w-4" />}
{on ? 'Выключить оповещение' : 'Включить оповещение'}
</button>
<button onClick={test} title="Проверка связи" className="grid h-10 w-10 shrink-0 place-items-center rounded-xl border border-border text-fg-muted hover:bg-muted hover:text-fg">
<Play className="h-4 w-4" />
</button>
</div>
{/* Эфир */}
<div className="border-t border-border">
<div className="px-3 py-1.5 text-[10px] uppercase tracking-wide text-fg-muted">Эфир · последние события</div>
<div className="max-h-48 space-y-0.5 overflow-y-auto px-2 pb-2">
{ether.length === 0 && <div className="px-2 py-4 text-center text-xs text-fg-muted">Пока тихо. События появятся здесь.</div>}
{ether.map((e, i) => (
<div key={i} className="flex items-start gap-2 rounded-lg px-2 py-1.5 hover:bg-muted/50">
<span className={cn('mt-1 h-2 w-2 shrink-0 rounded-full', DOT[e.severity] ?? 'bg-fg-muted')} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5 text-[11px]">
<span className="truncate font-medium text-fg">{e.callsign}</span>
<span className="tnum shrink-0 text-fg-muted">{tm(e.ts)}</span>
</div>
<div className="truncate text-[11px] text-fg-muted">{e.text}</div>
</div>
</div>
))}
</div>
</div>
{/* Доска объявлений */}
<div className="border-t border-border">
<div className="flex items-center justify-between px-3 py-1.5">
<span className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-fg-muted"><Megaphone className="h-3 w-3" /> Доска объявлений</span>
{openTasks > 0 && <span className="rounded-full bg-warn/20 px-1.5 text-[10px] font-semibold text-warn">{openTasks} задач</span>}
</div>
<div className="max-h-52 space-y-1 overflow-y-auto px-2 pb-2">
{board.map((b) => (
<div key={b.id} className={cn('group flex items-start gap-2 rounded-lg px-2 py-1.5', b.urgent && !b.done ? 'sm-blink bg-crit/10' : 'hover:bg-muted/50')}>
{b.kind === 'task' ? (
<button onClick={() => toggleTask(b.id)} title={b.done ? 'Открыть задачу' : 'Закрыть задачу'} className={cn('mt-0.5 shrink-0', b.done ? 'text-ok' : 'text-fg-muted hover:text-fg')}>
{b.done ? <CheckSquare className="h-4 w-4" /> : <Square className="h-4 w-4" />}
</button>
) : (
<span className={cn('mt-1 h-2 w-2 shrink-0 rounded-full', b.urgent ? 'bg-crit' : 'bg-info')} />
)}
<div className="min-w-0 flex-1">
<div className={cn('text-[11px] leading-snug', b.done ? 'text-fg-muted line-through' : 'text-fg')}>{b.text}</div>
</div>
<div className="flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<button onClick={() => enqueueRead(b.text)} title="В список чтения (озвучить)" className="text-fg-muted hover:text-accent"><Volume1 className="h-3.5 w-3.5" /></button>
<button onClick={() => removeBoard(b.id)} title="Удалить" className="text-fg-muted hover:text-crit"><Trash2 className="h-3.5 w-3.5" /></button>
</div>
</div>
))}
</div>
</div>
{/* Список чтения — голосовые уведомления */}
<div className="border-t border-border">
<div className="flex items-center justify-between px-3 py-1.5">
<span className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-fg-muted"><ListChecks className="h-3 w-3" /> Список чтения · {readList.length}</span>
<div className="flex items-center gap-1">
<button onClick={readAll} disabled={readList.length === 0} className="flex items-center gap-1 rounded-md bg-accent/15 px-2 py-0.5 text-[10px] font-semibold text-accent hover:bg-accent/25 disabled:opacity-40">
<Volume2 className="h-3 w-3" /> Читать
</button>
<button onClick={clearRead} disabled={readList.length === 0} title="Очистить" className="text-fg-muted hover:text-crit disabled:opacity-40"><Trash2 className="h-3.5 w-3.5" /></button>
</div>
</div>
<div className="max-h-36 space-y-0.5 overflow-y-auto px-2 pb-2">
{readList.length === 0 && <div className="px-2 py-3 text-center text-[11px] text-fg-muted">Список пуст. Добавьте объявление кнопкой 🔈.</div>}
{readList.map((t, i) => (
<div key={i} className="flex items-start gap-2 rounded-lg px-2 py-1 hover:bg-muted/50">
<button onClick={() => speak(t)} title="Прочитать" className="mt-0.5 shrink-0 text-fg-muted hover:text-accent"><Play className="h-3.5 w-3.5" /></button>
<div className="min-w-0 flex-1 text-[11px] leading-snug text-fg-muted">{t}</div>
<button onClick={() => removeRead(i)} title="Убрать" className="shrink-0 text-fg-muted hover:text-crit"><X className="h-3.5 w-3.5" /></button>
</div>
))}
</div>
</div>
</div>,
document.body,
);
}