diff --git a/packages/web/src/components/modals/ConnectInstanceModal.tsx b/packages/web/src/components/modals/ConnectInstanceModal.tsx new file mode 100644 index 00000000..4676af4e --- /dev/null +++ b/packages/web/src/components/modals/ConnectInstanceModal.tsx @@ -0,0 +1,161 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import { useInstanceConnect } from '../../hooks/useInstanceConnect'; + +interface ConnectInstanceModalProps { + domain: string; + targetDisplayName: string; + isReconnect?: boolean; + actionLabel?: string; + onConnected(result: 'new' | 'reconnect'): void; + onCancel(): void; +} + +export function ConnectInstanceModal({ + domain, + targetDisplayName, + isReconnect = false, + actionLabel, + onConnected, + onCancel, +}: ConnectInstanceModalProps) { + const [password, setPassword] = useState(''); + const { connect, isConnecting, error, clearError } = useInstanceConnect(); + const inputRef = useRef(null); + + const resolvedLabel = actionLabel + ?? (isReconnect ? 'Reconnect & Add Friend' : 'Connect & Add Friend'); + + // Focus password input on mount + useEffect(() => { + const timer = setTimeout(() => inputRef.current?.focus(), 50); + return () => clearTimeout(timer); + }, []); + + // Escape key handler — stop propagation to prevent parent modal from closing + useEffect(() => { + const handleKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.stopPropagation(); + onCancel(); + } + }; + document.addEventListener('keydown', handleKey, true); // capture phase + return () => document.removeEventListener('keydown', handleKey, true); + }, [onCancel]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!password.trim() || isConnecting) return; + + try { + const result = await connect(domain, password.trim()); + onConnected(result); + } catch { + // Error state is managed by the hook — UI updates via `error` + } + }; + + return createPortal( +
+ {/* Lighter backdrop to avoid compounding with parent modal */} +
+ +
+ {/* Header */} +
+
+
+ + + +
+

+ {isReconnect ? 'Reconnect to Instance' : 'Connect to Instance'} +

+
+ +

+ {isReconnect ? ( + <> + Your connection to {domain} was lost. + Re-enter your password to reconnect and send a friend request to{' '} + {targetDisplayName}. + + ) : ( + <> + {targetDisplayName} is on{' '} + {domain}, an instance you + haven't connected to yet. Connect to send a friend request. + + )} +

+ + {/* Instance badge */} +
+
+ + + +
+
+
{domain}
+
Remote Backspace instance
+
+
+
+ + {/* Form */} +
+ { + setPassword(e.target.value); + if (error) clearError(); + }} + disabled={isConnecting} + autoComplete="current-password" + /> + + {/* Error text */} + {error && ( +

{error}

+ )} + +
+ + +
+
+
+
, + document.body + ); +}