feat: add auth-state awareness and graceful degradation

- Add /api/auth/whoami endpoint check on app load
- Store auth state in Zustand (authenticated, username, loaded)
- Hide Contacts tab when unauthenticated
- Gate fetchNearbyContacts calls on auth.authenticated
- Replace Save button with Log in affordance when unauthenticated
- Add Login/Logout buttons to panel header
- Prevent any /api/contacts/* requests from firing when unauthenticated

Public functionality (search, routing, place details) remains
fully functional for unauthenticated users.
This commit is contained in:
Matt 2026-04-27 01:26:05 +00:00
commit 0d4a807a05
29 changed files with 13091 additions and 317 deletions

View file

@ -1,6 +1,6 @@
import { useEffect, useState, useRef, useCallback } from 'react'
import {
X, Navigation, Plus, Bookmark, ChevronDown, ChevronUp, Copy,
X, Navigation, Plus, Bookmark, ChevronDown, ChevronUp, Copy, LogIn,
Clock, Phone, Globe, Mail, BookOpen, Info, Trees,
} from 'lucide-react'
import OpeningHours from 'opening_hours'
@ -416,6 +416,7 @@ export default function PlaceDetail() {
const userLocation = useStore((s) => s.userLocation)
const contacts = useStore((s) => s.contacts)
const setEditingContact = useStore((s) => s.setEditingContact)
const auth = useStore((s) => s.auth)
const [elevResult, setElevResult] = useState({ lat: null, lon: null, value: null })
const [isMobile, setIsMobile] = useState(false)
@ -742,18 +743,30 @@ export default function PlaceDetail() {
</button>
)}
<button
onClick={handleSave}
className="p-2 rounded-lg"
style={{
background: savedContact ? 'var(--accent-muted)' : 'var(--tan-muted)',
color: savedContact ? 'var(--accent)' : 'var(--tan)',
border: '1px solid var(--border)',
}}
aria-label={savedContact ? 'Edit saved contact' : 'Save place'}
>
<Bookmark size={14} fill={savedContact ? 'currentColor' : 'none'} />
</button>
{auth.authenticated ? (
<button
onClick={handleSave}
className="p-2 rounded-lg"
style={{
background: savedContact ? 'var(--accent-muted)' : 'var(--tan-muted)',
color: savedContact ? 'var(--accent)' : 'var(--tan)',
border: '1px solid var(--border)',
}}
aria-label={savedContact ? 'Edit saved contact' : 'Save place'}
>
<Bookmark size={14} fill={savedContact ? 'currentColor' : 'none'} />
</button>
) : (
<button
onClick={() => { window.location.href = '/api/auth/whoami' }}
className="flex items-center gap-1 px-2 py-1.5 rounded-lg text-xs"
style={{ background: 'var(--accent-muted)', color: 'var(--accent)', border: '1px solid var(--border)' }}
title="Log in to save places"
>
<LogIn size={12} />
<span>Save</span>
</button>
)}
{/* Copy dropdown */}
<div className="relative">