import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { Crown, User, UserMinus, ShieldOff } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useOrbitStore } from '../store/orbitStore'; import { kickOrbitParticipant, removeOrbitParticipant } from '../utils/orbit'; import ConfirmModal from './ConfirmModal'; interface Props { /** Anchor — we position the popover directly below its bottom-right. */ anchorRef: React.RefObject; onClose: () => void; } function joinedFor(fromMs: number, nowMs: number): string { const sec = Math.max(0, Math.round((nowMs - fromMs) / 1000)); if (sec < 60) return `${sec}s`; const m = Math.floor(sec / 60); if (m < 60) return `${m}m`; const h = Math.floor(m / 60); const rm = m % 60; return `${h}h${rm.toString().padStart(2, '0')}`; } export default function OrbitParticipantsPopover({ anchorRef, onClose }: Props) { const { t } = useTranslation(); const state = useOrbitStore(s => s.state); const role = useOrbitStore(s => s.role); const popRef = useRef(null); const [confirm, setConfirm] = useState<{ user: string; mode: 'remove' | 'ban' } | null>(null); const nowMs = Date.now(); // Close on outside click / Escape — unless a confirm dialog is open // (otherwise outside-clicking the modal would dismiss the popover too, // and re-opening would lose the in-flight confirm context). useEffect(() => { const onDown = (e: MouseEvent) => { if (confirm) return; const t = e.target as Node | null; if (popRef.current?.contains(t)) return; if (anchorRef.current?.contains(t)) return; onClose(); }; const onKey = (e: KeyboardEvent) => { if (confirm) return; if (e.key === 'Escape') onClose(); }; document.addEventListener('mousedown', onDown); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); }; }, [anchorRef, onClose, confirm]); if (!state) return null; const anchor = anchorRef.current?.getBoundingClientRect(); const style: React.CSSProperties = anchor ? { position: 'fixed', top: anchor.bottom + 12, left: Math.max(8, anchor.left - 100), zIndex: 9999, } : { display: 'none' }; const onConfirm = async () => { if (!confirm) return; const { user, mode } = confirm; setConfirm(null); if (mode === 'remove') await removeOrbitParticipant(user); else await kickOrbitParticipant(user); }; return createPortal( <>
{t('orbit.participantsCountLabel', { count: state.participants.length + 1 })}
{state.host} {t('orbit.participantsHost')}
{state.participants.length === 0 && (
{t('orbit.participantsEmpty')}
)} {state.participants.map(p => (
{p.user} {joinedFor(p.joinedAt, nowMs)} {role === 'host' && (
)}
))}
{ void onConfirm(); }} onCancel={() => setConfirm(null)} /> , document.body, ); }