Core Planner experience: - Full-screen Leaflet map with OSM/OpenTopoMap/CyclOSM layers - Click to add waypoints, drag to move, right-click to delete - Waypoint sidebar with reorder, delete, route stats - Real-time Yjs sync between participants - Live cursors showing other users' mouse positions on the map Routing: - Host election via Yjs awareness (lowest client ID) - Auto-failover when host disconnects - Debounced BRouter route computation (500ms) - Route polyline display from BRouter GeoJSON - Route stats (distance, elevation gain/loss) Client-side architecture: - useYjs hook: WebSocket connection, awareness, document management - useRouting hook: host election, route computation, stats parsing - PlannerMap component: Leaflet + Yjs integration - WaypointSidebar component: list with reorder/delete - Lazy-loaded components for code splitting Remaining: elevation chart, profile selector, GPX export (tasks 6.7-6.9) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
73 lines
1.9 KiB
TypeScript
73 lines
1.9 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import * as Y from "yjs";
|
|
import { WebsocketProvider } from "y-websocket";
|
|
|
|
const COLORS = [
|
|
"#ef4444", "#f97316", "#eab308", "#22c55e",
|
|
"#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899",
|
|
];
|
|
|
|
const NAMES = [
|
|
"Hiker", "Cyclist", "Runner", "Explorer",
|
|
"Wanderer", "Pathfinder", "Trailblazer", "Navigator",
|
|
];
|
|
|
|
function randomItem<T>(arr: T[]): T {
|
|
return arr[Math.floor(Math.random() * arr.length)]!;
|
|
}
|
|
|
|
export interface YjsState {
|
|
doc: Y.Doc;
|
|
provider: WebsocketProvider;
|
|
waypoints: Y.Array<Y.Map<unknown>>;
|
|
routeData: Y.Map<unknown>;
|
|
awareness: WebsocketProvider["awareness"];
|
|
connected: boolean;
|
|
}
|
|
|
|
export function useYjs(sessionId: string): YjsState | null {
|
|
const [state, setState] = useState<YjsState | null>(null);
|
|
const [connected, setConnected] = useState(false);
|
|
const initialized = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (initialized.current) return;
|
|
initialized.current = true;
|
|
|
|
const doc = new Y.Doc();
|
|
const wsUrl = `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/sync/${sessionId}`;
|
|
const provider = new WebsocketProvider(wsUrl, sessionId, doc);
|
|
|
|
const waypoints = doc.getArray<Y.Map<unknown>>("waypoints");
|
|
const routeData = doc.getMap("routeData");
|
|
|
|
// Set local awareness state
|
|
const color = randomItem(COLORS);
|
|
const name = randomItem(NAMES);
|
|
provider.awareness.setLocalStateField("user", { color, name });
|
|
|
|
provider.on("status", ({ status }: { status: string }) => {
|
|
setConnected(status === "connected");
|
|
});
|
|
|
|
setState({
|
|
doc,
|
|
provider,
|
|
waypoints,
|
|
routeData,
|
|
awareness: provider.awareness,
|
|
connected: false,
|
|
});
|
|
|
|
return () => {
|
|
provider.destroy();
|
|
doc.destroy();
|
|
};
|
|
}, [sessionId]);
|
|
|
|
if (state) {
|
|
state.connected = connected;
|
|
}
|
|
|
|
return state;
|
|
}
|