import { useState, useEffect, useCallback, useRef } from 'react' import { X, Trash2, MapPin, Crosshair } from 'lucide-react' import toast from 'react-hot-toast' import { useStore } from '../store' import { createContact, updateContact, deleteContact, fetchContacts, fetchReverse, searchGeocode } from '../api' const CATEGORIES = ['family', 'friend', 'business', 'emergency', 'ham', 'bug-out', 'favorite'] export default function ContactModal() { const editingContact = useStore((s) => s.editingContact) const clearEditingContact = useStore((s) => s.clearEditingContact) const setPickingLocationFor = useStore((s) => s.setPickingLocationFor) const setContacts = useStore((s) => s.setContacts) const [form, setForm] = useState({}) const [saving, setSaving] = useState(false) // Geocode search state const [searchResults, setSearchResults] = useState([]) const [searchLoading, setSearchLoading] = useState(false) const [showDropdown, setShowDropdown] = useState(false) const debounceRef = useRef(null) const inputRef = useRef(null) useEffect(() => { if (editingContact) { setForm({ id: editingContact.id, label: editingContact.label || '', name: editingContact.name || '', call_sign: editingContact.call_sign || '', phone: editingContact.phone || '', email: editingContact.email || '', category: editingContact.category || '', notes: editingContact.notes || '', show_proximity: editingContact.show_proximity || false, lat: editingContact.lat ?? null, lon: editingContact.lon ?? null, osm_type: editingContact.osm_type || null, osm_id: editingContact.osm_id || null, address: editingContact.address || '', }) setSearchResults([]) setShowDropdown(false) } }, [editingContact]) // Auto-populate address from reverse geocode when lat/lon exist but address is empty useEffect(() => { if (!editingContact) return const hasGeo = form.lat != null && form.lon != null const addressEmpty = !form.address || form.address.trim() === '' if (hasGeo && addressEmpty) { let cancelled = false fetchReverse(form.lat, form.lon).then((place) => { if (!cancelled && place) { const addr = place.address || place.name || '' if (addr) { setForm((f) => ({ ...f, address: addr })) } } }) return () => { cancelled = true } } }, [editingContact, form.lat, form.lon]) // Cleanup debounce on unmount useEffect(() => { return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, []) const close = useCallback(() => clearEditingContact(), [clearEditingContact]) useEffect(() => { if (!editingContact) return const onKey = (e) => { if (e.key === 'Escape') { if (showDropdown) { setShowDropdown(false) } else { close() } } } document.addEventListener('keydown', onKey) return () => document.removeEventListener('keydown', onKey) }, [editingContact, close, showDropdown]) if (!editingContact) return null const isEdit = editingContact.id != null const hasGeo = form.lat != null && form.lon != null const setField = (key, val) => setForm((f) => ({ ...f, [key]: val })) // Handle address input change with debounced geocode search const handleAddressChange = (e) => { const query = e.target.value setField('address', query) // Clear previous debounce if (debounceRef.current) clearTimeout(debounceRef.current) if (!query || query.length < 3) { setSearchResults([]) setShowDropdown(false) return } debounceRef.current = setTimeout(async () => { setSearchLoading(true) try { const results = await searchGeocode(query, 5) setSearchResults(results || []) setShowDropdown(true) } catch (err) { console.error('Geocode search error:', err) setSearchResults([]) } finally { setSearchLoading(false) } }, 300) } // Handle selecting a geocode result const handleSelectResult = (result) => { setForm((f) => ({ ...f, address: result.display_name || result.name || '', lat: result.lat, lon: result.lon, osm_type: result.osm_type || null, osm_id: result.osm_id || null, })) setShowDropdown(false) setSearchResults([]) } // Handle "Set on map" button const handleSetOnMap = () => { // Save current form state to store for map pick mode setPickingLocationFor({ ...form }) clearEditingContact() toast('Click the map to set location', { icon: '📍', duration: 3000 }) } const refreshContacts = async () => { const data = await fetchContacts() if (!data?.auth && Array.isArray(data)) setContacts(data) else if (Array.isArray(data)) setContacts(data) } const handleSave = async () => { if (!form.label?.trim()) { toast.error('Label is required') return } setSaving(true) try { const payload = { ...form, label: form.label.trim() } delete payload.id // Don't send id in payload if (payload.show_proximity === false) payload.show_proximity = false const result = isEdit ? await updateContact(editingContact.id, payload) : await createContact(payload) if (result?.auth === false) { toast.error('Sign in to save contacts') setSaving(false) return } if (result?._status === 409 || result?.error?.includes('Home/Work')) { toast.error('You already have a Home/Work contact') setSaving(false) return } toast.success(isEdit ? 'Contact updated' : 'Contact saved') await refreshContacts() close() } catch (e) { toast.error(e.message) } finally { setSaving(false) } } const handleDelete = async () => { if (!confirm('Delete this contact? You can restore it from the dashboard.')) return setSaving(true) try { await deleteContact(editingContact.id) toast('Contact deleted') await refreshContacts() close() } catch (e) { toast.error(e.message) } finally { setSaving(false) } } // Format result for display const formatResult = (r) => { const parts = [] if (r.name) parts.push(r.name) if (r.address?.city) parts.push(r.address.city) if (r.address?.state) parts.push(r.address.state) return parts.length > 0 ? parts.join(', ') : r.display_name || 'Unknown location' } return (
{ if (e.target === e.currentTarget) close() }}>
{/* Header */}

{isEdit ? 'Edit Contact' : 'Save Contact'}

{/* Label with quick-fill */}
{['Home', 'Work'].map((l) => ( ))}
setField('label', e.target.value)} placeholder="e.g. Home, Work, Mom, Bug Out" />
{/* Address with geocode search */}
searchResults.length > 0 && setShowDropdown(true)} onBlur={() => setTimeout(() => setShowDropdown(false), 200)} placeholder="Type to search or enter address..." /> {searchLoading && (
...
)} {/* Dropdown results */} {showDropdown && searchResults.length > 0 && (
{searchResults.map((r, i) => ( ))}
)}
{/* Location display + Set on map button */}
{hasGeo ? (
{form.lat.toFixed(6)}, {form.lon.toFixed(6)}
) : (
No location set
)}
{/* Category */}
setField('category', e.target.value)} placeholder="family, friend, emergency..." /> {CATEGORIES.map((c) =>
{/* Name + Call Sign */}
setField('name', e.target.value)} />
setField('call_sign', e.target.value)} />
{/* Phone + Email */}
setField('phone', e.target.value)} type="tel" />
setField('email', e.target.value)} type="email" />
{/* Notes */}