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

@ -1,10 +1,12 @@
import { useRef, useCallback, useEffect, useState } from 'react'
import { Sun, Moon } from 'lucide-react'
import { useStore } from '../store'
import { hasFeature } from '../config'
import SearchBar from './SearchBar'
import StopList from './StopList'
import ModeSelector from './ModeSelector'
import ManeuverList from './ManeuverList'
import ContactList from './ContactList'
import { requestOptimizedRoute } from '../api'
export default function Panel({ onManeuverClick }) {
@ -24,6 +26,8 @@ export default function Panel({ onManeuverClick }) {
const setThemeOverride = useStore((s) => s.setThemeOverride)
const gpsOrigin = useStore((s) => s.gpsOrigin)
const geoPermission = useStore((s) => s.geoPermission)
const activeTab = useStore((s) => s.activeTab)
const setActiveTab = useStore((s) => s.setActiveTab)
const [isMobile, setIsMobile] = useState(false)
const [optimizing, setOptimizing] = useState(false)
@ -31,6 +35,8 @@ export default function Panel({ onManeuverClick }) {
const dragStartY = useRef(0)
const dragStartState = useRef('half')
const showContacts = hasFeature('has_contacts')
// Responsive detection
useEffect(() => {
const check = () => setIsMobile(window.innerWidth < 768)
@ -60,7 +66,6 @@ export default function Panel({ onManeuverClick }) {
}
const data = await requestOptimizedRoute(locations, mode)
if (data.trip) {
// If GPS origin was prepended, skip it from the result waypoints
const wpOrder = hasGpsOrigin && userLocation
? (data.trip.locations || []).slice(1)
: data.trip.locations
@ -116,7 +121,7 @@ export default function Panel({ onManeuverClick }) {
const showOptimize = effectiveCount >= 3
const content = (
const routesContent = (
<>
<SearchBar />
@ -153,6 +158,29 @@ export default function Panel({ onManeuverClick }) {
</>
)
const content = (
<>
{showContacts && (
<div className="navi-tab-bar mb-3">
<button
className={`navi-tab ${activeTab === 'routes' ? 'navi-tab-active' : ''}`}
onClick={() => setActiveTab('routes')}
>
Routes
</button>
<button
className={`navi-tab ${activeTab === 'contacts' ? 'navi-tab-active' : ''}`}
onClick={() => setActiveTab('contacts')}
>
Contacts
</button>
</div>
)}
{(!showContacts || activeTab === 'routes') ? routesContent : <ContactList />}
</>
)
const header = (
<div className="flex items-center justify-between mb-3">
<h1 className="text-md font-semibold" style={{ color: 'var(--accent)' }}>Navi</h1>