Add contacts/phone book UI with search integration

New components:
- ContactModal.jsx: Save/edit overlay with form fields and soft delete
- ContactList.jsx: Contacts tab with filter, create, and tap-to-navigate

Modified:
- store.js: Add contacts slice (contacts, activeTab, editingContact)
- api.js: Add contacts API functions (fetch, create, update, delete, nearby)
- config.js: Add has_contacts fallback flag
- Panel.jsx: Routes/Contacts tab bar (only when has_contacts enabled)
- PlaceDetail.jsx: Save button opens ContactModal, proximity annotation
- SearchBar.jsx: Prepend matching contacts before Photon results
- App.jsx: Render ContactModal at top level
- index.css: Modal overlay, tab bar, contact list item styles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-04-22 05:30:19 +00:00
commit 3ce860c1e8
10 changed files with 1087 additions and 66 deletions

View file

@ -6,6 +6,7 @@ import { decodePolyline } from './utils/decode'
import MapView from './components/MapView'
import Panel from './components/Panel'
import PlaceDetail from './components/PlaceDetail'
import ContactModal from './components/ContactModal'
import LayerControl from './components/LayerControl'
import LocateButton from './components/LocateButton'
@ -27,14 +28,12 @@ export default function App() {
const clearRoute = useStore((s) => s.clearRoute)
// Fetch route when stops, mode, gpsOrigin, or geoPermission change (debounced 500ms)
// NOTE: userLocation is NOT a dep read from store inside the callback to avoid re-routing on every GPS update
useEffect(() => {
if (routeDebounceRef.current) clearTimeout(routeDebounceRef.current)
routeDebounceRef.current = setTimeout(async () => {
const { userLocation } = useStore.getState()
// Build effective stop list
let effective = stops.map((s) => ({ lat: s.lat, lon: s.lon }))
if (gpsOrigin && geoPermission === 'granted' && userLocation) {
effective = [{ lat: userLocation.lat, lon: userLocation.lon }, ...effective]
@ -66,7 +65,7 @@ export default function App() {
}
}, [stops, mode, gpsOrigin, geoPermission, clearRoute, setRoute, setRouteLoading, setRouteError])
// Handle maneuver click fly to that point on the map
// Handle maneuver click
const handleManeuverClick = useCallback(
(maneuver) => {
if (!route || !route.legs) return
@ -90,6 +89,7 @@ export default function App() {
<MapView ref={mapViewRef} />
<Panel onManeuverClick={handleManeuverClick} />
<PlaceDetail />
<ContactModal />
<LayerControl mapRef={mapViewRef} />
<LocateButton mapRef={mapViewRef} />
</div>