/** * UserSearchModal Component * * Modal for searching and connecting with other users. * Features: * - Fuzzy search by username/display name * - Shows connection status * - One-click connect/disconnect * - Shows mutual connections count */ import React, { useState, useCallback, useEffect, useRef } from 'react'; import { searchUsers, type UserSearchResult } from '../../lib/networking'; // ============================================================================= // Types // ============================================================================= interface UserSearchModalProps { isOpen: boolean; onClose: () => void; onConnect: (userId: string) => Promise; onDisconnect?: (userId: string) => Promise; currentUserId?: string; } // ============================================================================= // Styles // ============================================================================= const styles = { overlay: { position: 'fixed' as const, top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000, }, modal: { backgroundColor: 'var(--color-background, #fff)', borderRadius: '12px', width: '90%', maxWidth: '480px', maxHeight: '70vh', display: 'flex', flexDirection: 'column' as const, boxShadow: '0 4px 24px rgba(0, 0, 0, 0.2)', overflow: 'hidden', }, header: { padding: '16px 20px', borderBottom: '1px solid var(--color-border, #e0e0e0)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', }, title: { fontSize: '18px', fontWeight: 600, margin: 0, color: 'var(--color-text, #1a1a2e)', }, closeButton: { background: 'none', border: 'none', fontSize: '24px', cursor: 'pointer', color: 'var(--color-text-secondary, #666)', padding: '4px', lineHeight: 1, }, searchContainer: { padding: '16px 20px', borderBottom: '1px solid var(--color-border, #e0e0e0)', }, searchInput: { width: '100%', padding: '12px 16px', fontSize: '16px', border: '1px solid var(--color-border, #e0e0e0)', borderRadius: '8px', outline: 'none', backgroundColor: 'var(--color-surface, #f5f5f5)', color: 'var(--color-text, #1a1a2e)', }, results: { flex: 1, overflowY: 'auto' as const, padding: '8px 0', }, resultItem: { display: 'flex', alignItems: 'center', padding: '12px 20px', gap: '12px', cursor: 'pointer', transition: 'background-color 0.15s', }, resultItemHover: { backgroundColor: 'var(--color-surface, #f5f5f5)', }, avatar: { width: '40px', height: '40px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '16px', fontWeight: 600, color: '#fff', flexShrink: 0, }, userInfo: { flex: 1, minWidth: 0, }, username: { fontSize: '15px', fontWeight: 500, color: 'var(--color-text, #1a1a2e)', whiteSpace: 'nowrap' as const, overflow: 'hidden', textOverflow: 'ellipsis', }, displayName: { fontSize: '13px', color: 'var(--color-text-secondary, #666)', whiteSpace: 'nowrap' as const, overflow: 'hidden', textOverflow: 'ellipsis', }, mutualBadge: { fontSize: '11px', color: 'var(--color-text-tertiary, #999)', marginTop: '2px', }, connectButton: { padding: '8px 16px', fontSize: '13px', fontWeight: 500, borderRadius: '6px', border: 'none', cursor: 'pointer', transition: 'all 0.15s', flexShrink: 0, }, connectButtonConnect: { backgroundColor: 'var(--color-primary, #4f46e5)', color: '#fff', }, connectButtonConnected: { backgroundColor: 'var(--color-success, #22c55e)', color: '#fff', }, connectButtonMutual: { backgroundColor: 'var(--color-accent, #8b5cf6)', color: '#fff', }, emptyState: { padding: '40px 20px', textAlign: 'center' as const, color: 'var(--color-text-secondary, #666)', }, loadingState: { padding: '40px 20px', textAlign: 'center' as const, color: 'var(--color-text-secondary, #666)', }, }; // ============================================================================= // Component // ============================================================================= export function UserSearchModal({ isOpen, onClose, onConnect, onDisconnect, currentUserId, }: UserSearchModalProps) { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const [hoveredId, setHoveredId] = useState(null); const [connectingId, setConnectingId] = useState(null); const inputRef = useRef(null); const searchTimeoutRef = useRef(null); // Focus input when modal opens useEffect(() => { if (isOpen && inputRef.current) { inputRef.current.focus(); } }, [isOpen]); // Debounced search useEffect(() => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } if (query.length < 2) { setResults([]); return; } searchTimeoutRef.current = setTimeout(async () => { setIsLoading(true); try { const users = await searchUsers(query); // Filter out current user setResults(users.filter(u => u.id !== currentUserId)); } catch (error) { console.error('Search failed:', error); setResults([]); } finally { setIsLoading(false); } }, 300); return () => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } }; }, [query, currentUserId]); // Handle connect/disconnect const handleConnect = useCallback(async (user: UserSearchResult) => { setConnectingId(user.id); try { if (user.isConnected && onDisconnect) { await onDisconnect(user.id); // Update local state setResults(prev => prev.map(u => u.id === user.id ? { ...u, isConnected: false } : u )); } else { await onConnect(user.id); // Update local state setResults(prev => prev.map(u => u.id === user.id ? { ...u, isConnected: true } : u )); } } catch (error) { console.error('Connection action failed:', error); } finally { setConnectingId(null); } }, [onConnect, onDisconnect]); // Handle escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { onClose(); } }; if (isOpen) { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); } }, [isOpen, onClose]); if (!isOpen) return null; const getButtonStyle = (user: UserSearchResult) => { if (user.isConnected && user.isConnectedBack) { return { ...styles.connectButton, ...styles.connectButtonMutual }; } if (user.isConnected) { return { ...styles.connectButton, ...styles.connectButtonConnected }; } return { ...styles.connectButton, ...styles.connectButtonConnect }; }; const getButtonText = (user: UserSearchResult) => { if (connectingId === user.id) return '...'; if (user.isConnected && user.isConnectedBack) return 'Mutual'; if (user.isConnected) return 'Connected'; return 'Connect'; }; return (
e.stopPropagation()}>

Find People

setQuery(e.target.value)} style={styles.searchInput} />
{isLoading ? (
Searching...
) : results.length === 0 ? (
{query.length < 2 ? 'Type at least 2 characters to search' : 'No users found' }
) : ( results.map(user => (
setHoveredId(user.id)} onMouseLeave={() => setHoveredId(null)} >
{(user.displayName || user.username).charAt(0).toUpperCase()}
@{user.username}
{user.displayName && user.displayName !== user.username && (
{user.displayName}
)} {user.isConnectedBack && !user.isConnected && (
Follows you
)} {user.mutualConnections > 0 && (
{user.mutualConnections} mutual connection{user.mutualConnections !== 1 ? 's' : ''}
)}
)) )}
); } export default UserSearchModal;