feat: task 2.7 event picker (subscription driver)

- src/data/events.ts: useUserEvents() TanStack Query (5-min stale,
  sort -starts_at). EventSummary type is a Pick of EventRow.
- src/live/active-event.ts: useActiveEventOrchestration() returns the
  swap fn — unsubscribe previous + clearForEvent + subscribe new +
  applySnapshot on success + persist to localStorage. Out-of-order
  safety via per-call version counter. Plus readSavedActiveEventId().
- src/ui/components/event-picker.tsx: <EventPicker> dropdown.
  useState + click-outside; rows show name + date + discipline.
- src/live/index.ts: re-exports active-event helpers.
- src/routes/_authed/monitor.tsx: auto-select effect (one-shot via
  initializedRef, gated on events loaded + WS connected); renders
  <EventPicker> wired to setActiveEvent.

Deviations:
1. Vanilla div + useState dropdown instead of shadcn Popover —
   no new shadcn primitive add; easy to swap later for keyboard nav.
2. Auto-select gated on connectionStatus === 'connected' so the
   subscribe call gets the snapshot path (not 'not-connected').
3. Logout-clears-saved-event-id deferred to a small Phase 1.8
   follow-up; documented in task risks.

Bundle: 395KB / 120KB gz (~1KB up from 2.6).
This commit is contained in:
2026-05-02 22:31:37 +02:00
parent 5cb859b403
commit 564b7881bd
8 changed files with 316 additions and 3 deletions
+36
View File
@@ -1,15 +1,45 @@
import { useEffect, useRef } from 'react';
import { createFileRoute } from '@tanstack/react-router';
import { useUserEvents } from '@/data/events';
import {
readSavedActiveEventId,
useActiveEventOrchestration,
useConnectionStore,
usePositionStore,
} from '@/live';
import { BasemapSwitcher } from '@/map/core/basemap-switcher';
import { MapView } from '@/map/core/map-view';
import { TrailsToggle } from '@/map/core/trails-toggle';
import { MapPositions } from '@/map/layers/map-positions';
import { MapTrails } from '@/map/layers/map-trails';
import { EventPicker } from '@/ui/components/event-picker';
export const Route = createFileRoute('/_authed/monitor')({
component: MonitorPage,
});
function MonitorPage() {
const activeEventId = usePositionStore((s) => s.activeEventId);
const setActiveEvent = useActiveEventOrchestration();
const events = useUserEvents();
const connectionStatus = useConnectionStore((s) => s.status);
const initializedRef = useRef(false);
// Auto-select on first mount: prefer the previously-saved event id;
// otherwise the most recent. Gated on the WS being connected so the
// subscribe call doesn't immediately resolve with `not-connected`.
useEffect(() => {
if (initializedRef.current) return;
if (!events.data || events.data.length === 0) return;
if (connectionStatus !== 'connected') return;
initializedRef.current = true;
const saved = readSavedActiveEventId();
const target =
saved && events.data.some((e) => e.id === saved) ? saved : (events.data[0]?.id ?? null);
if (target) void setActiveEvent(target);
}, [events.data, connectionStatus, setActiveEvent]);
return (
// 3.5rem ≈ 56 px reserved for a future top-bar (Phase 3 chrome).
// For now the parent _authed layout has no top-bar, so the map can
@@ -20,6 +50,12 @@ function MonitorPage() {
UI controls. Floating cards positioned absolutely inside
<MapView>'s wrapper.
*/}
<EventPicker
activeEventId={activeEventId}
onChange={(id) => {
void setActiveEvent(id);
}}
/>
<BasemapSwitcher />
<TrailsToggle />
{/*