diff --git a/src/components/ContactList.jsx b/src/components/ContactList.jsx
index 432b33b..6a75dee 100644
--- a/src/components/ContactList.jsx
+++ b/src/components/ContactList.jsx
@@ -1,138 +1,279 @@
-import { useEffect, useState, useCallback } from 'react'
-import { Plus, MapPin, User, Phone, Radio, LogIn } from 'lucide-react'
-import { useStore } from '../store'
-import { fetchContacts } from '../api'
-
-export default function ContactList() {
- const contacts = useStore((s) => s.contacts)
- const contactsLoaded = useStore((s) => s.contactsLoaded)
- const setContacts = useStore((s) => s.setContacts)
- const setEditingContact = useStore((s) => s.setEditingContact)
- const setSelectedPlace = useStore((s) => s.setSelectedPlace)
- const auth = useStore((s) => s.auth)
-
- const [filter, setFilter] = useState('')
-
- const loadContacts = useCallback(async () => {
- // Skip fetch entirely if not authenticated
- if (!auth.authenticated) return
- const data = await fetchContacts()
- if (Array.isArray(data)) {
- setContacts(data)
- }
- }, [setContacts, auth.authenticated])
-
- useEffect(() => {
- if (auth.loaded && auth.authenticated && !contactsLoaded) {
- loadContacts()
- }
- }, [auth.loaded, auth.authenticated, contactsLoaded, loadContacts])
-
- // Show login prompt if not authenticated
- if (auth.loaded && !auth.authenticated) {
- return (
-
-
- Sign in to save and sync your contacts
-
-
-
- )
- }
-
- const q = filter.toLowerCase()
- const filtered = q
- ? contacts.filter((c) =>
- (c.label || '').toLowerCase().includes(q) ||
- (c.name || '').toLowerCase().includes(q) ||
- (c.call_sign || '').toLowerCase().includes(q) ||
- (c.phone || '').includes(q)
- )
- : contacts
-
- const handleClick = (c) => {
- if (c.lat != null && c.lon != null) {
- setSelectedPlace({
- lat: c.lat,
- lon: c.lon,
- name: c.label,
- address: c.address || null,
- type: 'contact',
- source: 'contacts',
- matchCode: null,
- raw: { osm_type: c.osm_type, osm_id: c.osm_id, contact: c },
- })
- } else {
- setEditingContact(c)
- }
- }
-
- return (
-
- {/* Search + add */}
-
-
setFilter(e.target.value)}
- />
-
-
-
- {/* List */}
- {filtered.length === 0 ? (
-
- {contacts.length === 0 ? 'No contacts yet' : 'No matches'}
-
- ) : (
-
- {filtered.map((c) => (
-
handleClick(c)}
- >
-
- {c.lat != null ? : c.call_sign ? : }
-
-
-
{c.label}
-
- {c.name || c.address || c.phone || ''}
-
-
- {c.phone && (
-
-
-
- )}
- {c.show_proximity && c.lat != null && (
-
- prox
-
- )}
-
- ))}
-
- )}
-
- )
-}
+import { useEffect, useState, useCallback, useRef } from 'react'
+import { Plus, MapPin, User, Phone, Radio, LogIn, MoreVertical, Navigation, Eye, Pencil, Trash2 } from 'lucide-react'
+import toast from 'react-hot-toast'
+import { useStore } from '../store'
+import { fetchContacts, deleteContact } from '../api'
+
+export default function ContactList() {
+ const contacts = useStore((s) => s.contacts)
+ const contactsLoaded = useStore((s) => s.contactsLoaded)
+ const setContacts = useStore((s) => s.setContacts)
+ const setEditingContact = useStore((s) => s.setEditingContact)
+ const setSelectedPlace = useStore((s) => s.setSelectedPlace)
+ const setClickMarker = useStore((s) => s.setClickMarker)
+ const startDirections = useStore((s) => s.startDirections)
+ const setActiveTab = useStore((s) => s.setActiveTab)
+ const auth = useStore((s) => s.auth)
+
+ const [filter, setFilter] = useState('')
+ const [menuOpen, setMenuOpen] = useState(null) // contact id or null
+ const menuRef = useRef(null)
+
+ const loadContacts = useCallback(async () => {
+ if (!auth.authenticated) return
+ const data = await fetchContacts()
+ if (Array.isArray(data)) {
+ setContacts(data)
+ }
+ }, [setContacts, auth.authenticated])
+
+ useEffect(() => {
+ if (auth.loaded && auth.authenticated && !contactsLoaded) {
+ loadContacts()
+ }
+ }, [auth.loaded, auth.authenticated, contactsLoaded, loadContacts])
+
+ // Close menu on outside click or Escape
+ useEffect(() => {
+ if (!menuOpen) return
+ const handleClick = (e) => {
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
+ setMenuOpen(null)
+ }
+ }
+ const handleKey = (e) => {
+ if (e.key === 'Escape') setMenuOpen(null)
+ }
+ document.addEventListener('mousedown', handleClick)
+ document.addEventListener('keydown', handleKey)
+ return () => {
+ document.removeEventListener('mousedown', handleClick)
+ document.removeEventListener('keydown', handleKey)
+ }
+ }, [menuOpen])
+
+ // Show login prompt if not authenticated
+ if (auth.loaded && !auth.authenticated) {
+ return (
+
+
+ Sign in to save and sync your contacts
+
+
+
+ )
+ }
+
+ const q = filter.toLowerCase()
+ const filtered = q
+ ? contacts.filter((c) =>
+ (c.label || '').toLowerCase().includes(q) ||
+ (c.name || '').toLowerCase().includes(q) ||
+ (c.call_sign || '').toLowerCase().includes(q) ||
+ (c.phone || '').includes(q)
+ )
+ : contacts
+
+ const handleClick = (c) => {
+ if (c.lat != null && c.lon != null) {
+ setSelectedPlace({
+ lat: c.lat,
+ lon: c.lon,
+ name: c.label,
+ address: c.address || null,
+ type: 'contact',
+ source: 'contacts',
+ matchCode: null,
+ raw: { osm_type: c.osm_type, osm_id: c.osm_id, contact: c },
+ })
+ } else {
+ setEditingContact(c)
+ }
+ }
+
+ const handleMenuClick = (e, contactId) => {
+ e.stopPropagation()
+ setMenuOpen(menuOpen === contactId ? null : contactId)
+ }
+
+ const handleDirections = (c) => {
+ setMenuOpen(null)
+ startDirections({ lat: c.lat, lon: c.lon, name: c.label })
+ }
+
+ const handleViewOnMap = (c) => {
+ setMenuOpen(null)
+ // Set click marker at location
+ setClickMarker({ lat: c.lat, lon: c.lon })
+ // Set selected place to trigger map fly and place card
+ setSelectedPlace({
+ lat: c.lat,
+ lon: c.lon,
+ name: c.label,
+ address: c.address || null,
+ type: 'contact',
+ source: 'contacts',
+ matchCode: null,
+ raw: { osm_type: c.osm_type, osm_id: c.osm_id, contact: c },
+ })
+ // Switch to routes tab to close contacts panel
+ setActiveTab('routes')
+ }
+
+ const handleEdit = (c) => {
+ setMenuOpen(null)
+ setEditingContact(c)
+ }
+
+ const handleDelete = async (c) => {
+ setMenuOpen(null)
+ if (!confirm(`Delete "${c.label}"? You can restore it from the dashboard.`)) return
+ try {
+ const result = await deleteContact(c.id)
+ if (result?.auth === false) {
+ toast.error('Sign in to delete contacts')
+ return
+ }
+ toast.success('Contact deleted')
+ await loadContacts()
+ } catch (e) {
+ toast.error(e.message)
+ }
+ }
+
+ return (
+
+ {/* Search + add */}
+
+
setFilter(e.target.value)}
+ />
+
+
+
+ {/* List */}
+ {filtered.length === 0 ? (
+
+ {contacts.length === 0 ? 'No contacts yet' : 'No matches'}
+
+ ) : (
+
+ {filtered.map((c) => {
+ const hasLocation = c.lat != null && c.lon != null
+ const isMenuOpen = menuOpen === c.id
+
+ return (
+
handleClick(c)}
+ >
+
+ {hasLocation ? : c.call_sign ? : }
+
+
+
{c.label}
+
+ {c.name || c.address || c.phone || ''}
+
+
+ {c.phone && (
+
+
+
+ )}
+ {c.show_proximity && hasLocation && (
+
+ prox
+
+ )}
+
+ {/* Three-dot menu button */}
+
+
+ {/* Dropdown menu */}
+ {isMenuOpen && (
+
e.stopPropagation()}
+ >
+ {hasLocation && (
+ <>
+
+
+ >
+ )}
+
+
+
+ )}
+
+ )
+ })}
+
+ )}
+
+ )
+}