import { useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { X, Check, Copy, Orbit as OrbitIcon, Dices, AlertTriangle, Globe2, } from 'lucide-react'; import { startOrbitSession, buildOrbitShareLink, generateSessionId, slugifyOrbitName, } from '../utils/orbit'; import { randomOrbitSessionName } from '../utils/orbitNames'; import { useAuthStore } from '../store/authStore'; import { usePlayerStore } from '../store/playerStore'; import { isLanUrl } from '../hooks/useConnectionStatus'; import { ORBIT_DEFAULT_MAX_USERS } from '../api/orbit'; interface Props { onClose: () => void; } /** * Orbit — start-session modal. * * One-screen flow: a share-link is shown immediately (built from a * pre-generated session id + a slug derived from the live name). The host * can copy it any time; pressing "Start" creates the session under that * same id and auto-copies the link if it hasn't been copied yet. */ export default function OrbitStartModal({ onClose }: Props) { const { t } = useTranslation(); const [sid] = useState(() => generateSessionId()); const [name, setName] = useState(() => randomOrbitSessionName()); const [maxUsers, setMaxUsers] = useState(ORBIT_DEFAULT_MAX_USERS); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); const [hasCopied, setHasCopied] = useState(false); const [clearQueue, setClearQueue] = useState(false); const server = useAuthStore.getState().getActiveServer(); const serverBase = server?.url ?? ''; const serverName = server?.name ?? server?.url ?? t('orbit.fallbackServer'); const onLan = isLanUrl(serverBase); const shareLink = useMemo( () => buildOrbitShareLink(serverBase, sid, slugifyOrbitName(name)), [serverBase, sid, name], ); const writeLinkToClipboard = async (): Promise => { try { await navigator.clipboard.writeText(shareLink); return true; } catch { return false; } }; const onCopy = async () => { const ok = await writeLinkToClipboard(); if (ok) { setCopied(true); setHasCopied(true); window.setTimeout(() => setCopied(false), 1500); } }; const onStart = async () => { setError(null); const trimmed = name.trim(); if (!trimmed) { setError(t('orbit.errNameRequired')); return; } if (!hasCopied) { const ok = await writeLinkToClipboard(); if (ok) setHasCopied(true); } setBusy(true); try { if (clearQueue) usePlayerStore.getState().clearQueue(); await startOrbitSession({ name: trimmed, maxUsers, sid }); onClose(); } catch (e) { setError(e instanceof Error ? e.message : t('orbit.errStartFailed')); } finally { setBusy(false); } }; const heroSubParts = t('orbit.heroSub', { server: serverName }).split(String(serverName)); return createPortal(
{ if (e.target === e.currentTarget) onClose(); }} role="dialog" aria-modal="true" aria-labelledby="orbit-start-title" >

{t('orbit.heroTitlePrefix')}{' '} {t('orbit.heroTitleBrand')}

{heroSubParts[0]} {serverName} {heroSubParts[1] ?? ''}

{onLan ? : } {onLan ? t('orbit.tipLan') : t('orbit.tipRemote')}
{ setName(e.target.value); setHasCopied(false); }} onKeyDown={e => { if (e.key !== 'Enter') return; if (busy || !name.trim()) return; e.preventDefault(); void onStart(); }} placeholder={t('orbit.namePlaceholder')} maxLength={40} className="orbit-start-modal__input" />
{t('orbit.helperName')}
setMaxUsers(Number(e.target.value))} className="orbit-start-modal__range" />
{t('orbit.helperMax')}
{shareLink}
{t('orbit.helperLink')}
{error &&
{error}
}
, document.body, ); }