import { useRef, useCallback, useEffect, useState } from 'react' import { Sun, Moon, Sparkles, LogIn, LogOut } from 'lucide-react' import { themeList } from '../themes/registry' import { useStore, usePanelState } 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 { PlaceCard } from './PlaceCard' import { requestOptimizedRoute } from '../api' export default function Panel({ onManeuverClick }) { const selectedPlace = useStore((s) => s.selectedPlace) const pendingDestination = useStore((s) => s.pendingDestination) const clearSelectedPlace = useStore((s) => s.clearSelectedPlace) const clearPendingDestination = useStore((s) => s.clearPendingDestination) const stops = useStore((s) => s.stops) const mode = useStore((s) => s.mode) const route = useStore((s) => s.route) const routeLoading = useStore((s) => s.routeLoading) const routeError = useStore((s) => s.routeError) const setStops = useStore((s) => s.setStops) const setRoute = useStore((s) => s.setRoute) const setRouteError = useStore((s) => s.setRouteError) const setRouteLoading = useStore((s) => s.setRouteLoading) const sheetState = useStore((s) => s.sheetState) const setSheetState = useStore((s) => s.setSheetState) const theme = useStore((s) => s.theme) const themeOverride = useStore((s) => s.themeOverride) 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 auth = useStore((s) => s.auth) const setActiveTab = useStore((s) => s.setActiveTab) const panelState = usePanelState() const [isMobile, setIsMobile] = useState(false) const [optimizing, setOptimizing] = useState(false) const sheetRef = useRef(null) const dragStartY = useRef(0) const dragStartState = useRef('half') // Show contacts tab only if feature enabled AND user is authenticated const showContacts = hasFeature('has_contacts') && auth.authenticated // Responsive detection useEffect(() => { const check = () => setIsMobile(window.innerWidth < 768) check() window.addEventListener('resize', check) return () => window.removeEventListener('resize', check) }, []) // Theme toggle - cycles through all available themes const toggleTheme = () => { const themes = themeList() const currentIdx = themes.findIndex(t => t.id === theme) const nextIdx = (currentIdx + 1) % themes.length setThemeOverride(themes[nextIdx].id) } // Get theme icon based on current theme const getThemeIcon = () => { switch (theme) { case 'dark': return case 'light': return case 'clean': return default: return } } // Get next theme name for tooltip const getNextThemeName = () => { const themes = themeList() const currentIdx = themes.findIndex(t => t.id === theme) const nextIdx = (currentIdx + 1) % themes.length return themes[nextIdx].name } // Auth handlers const handleLogin = () => { window.location.href = '/outpost.goauthentik.io/start?rd=%2F' } const handleLogout = () => { window.location.href = 'https://auth.echo6.co/if/flow/default-invalidation-flow/?next=https://navi.echo6.co/' } // Optimize stops const hasGpsOrigin = gpsOrigin && geoPermission === 'granted' const effectiveCount = stops.length + (hasGpsOrigin ? 1 : 0) const handleOptimize = useCallback(async () => { if (effectiveCount < 3 || optimizing) return setOptimizing(true) try { const { userLocation } = useStore.getState() let locations = stops.map((s) => ({ lat: s.lat, lon: s.lon })) if (hasGpsOrigin && userLocation) { locations = [{ lat: userLocation.lat, lon: userLocation.lon }, ...locations] } const data = await requestOptimizedRoute(locations, mode) if (data.trip) { const wpOrder = hasGpsOrigin && userLocation ? (data.trip.locations || []).slice(1) : data.trip.locations if (wpOrder && wpOrder.length === stops.length) { const reordered = wpOrder.map((wp) => { let closest = stops[0] let minDist = Infinity for (const s of stops) { const d = Math.abs(s.lat - wp.lat) + Math.abs(s.lon - wp.lon) if (d < minDist) { minDist = d closest = s } } return closest }) const seen = new Set() const unique = reordered.filter((s) => { if (seen.has(s.id)) return false seen.add(s.id) return true }) if (unique.length === stops.length) { setStops(unique) } } setRoute(data.trip) } } catch (e) { setRouteError(e.message) } finally { setOptimizing(false) } }, [stops, mode, optimizing, effectiveCount, hasGpsOrigin, setStops, setRoute, setRouteError]) // Mobile sheet drag handling const handleTouchStart = useCallback((e) => { dragStartY.current = e.touches[0].clientY dragStartState.current = sheetState }, [sheetState]) const handleTouchEnd = useCallback((e) => { const deltaY = e.changedTouches[0].clientY - dragStartY.current if (Math.abs(deltaY) < 30) return if (deltaY < 0) { if (dragStartState.current === 'collapsed') setSheetState('half') else if (dragStartState.current === 'half') setSheetState('full') } else { if (dragStartState.current === 'full') setSheetState('half') else if (dragStartState.current === 'half') setSheetState('collapsed') } }, [setSheetState]) const showOptimize = effectiveCount >= 3 // Determine what to show based on panel state const showPreviewCard = panelState.startsWith('PREVIEW') const showRouteSection = ['ROUTING', 'ROUTE_CALCULATED', 'PREVIEW_ROUTING', 'PREVIEW_CALCULATED'].includes(panelState) || !!pendingDestination const showManeuvers = panelState === 'ROUTE_CALCULATED' || panelState === 'PREVIEW_CALCULATED' const showEmptyState = panelState === 'IDLE' && !pendingDestination // Routes tab content - now state-driven const routesContent = ( <> {/* Preview card when place is selected */} {showPreviewCard && selectedPlace && (
)} {/* Route section with stops */} {showRouteSection && ( <>
{showOptimize && ( )} {pendingDestination && stops.length === 0 && ( )}
)} {/* Maneuvers when route is calculated */} {showManeuvers && (route || routeLoading || routeError) && (
)} {/* Empty state */} {showEmptyState && (

Search or tap the map to explore

)} ) const content = ( <> {showContacts && (
)} {(!showContacts || activeTab === 'routes') ? routesContent : } ) const header = (

Navi

{auth.loaded && ( auth.authenticated ? ( ) : ( ) )}
) // Desktop: side panel (now 360px to accommodate PlaceCard) if (!isMobile) { return (
{header} {content}
) } // Mobile: bottom sheet const sheetHeights = { collapsed: 'h-12', half: 'h-[45vh]', full: 'h-[85vh]', } return (
{/* Drag handle */}
{ if (sheetState === 'collapsed') setSheetState('half') else if (sheetState === 'half') setSheetState('full') else setSheetState('half') }} >
{sheetState !== 'collapsed' && (
{header} {content}
)}
) }