import { useEffect, useState, useCallback } from "react"; import * as Y from "yjs"; import { useTranslation } from "react-i18next"; import type { YjsState } from "~/lib/use-yjs"; import type { DayStage } from "@trails-cool/gpx"; import { setOvernight, isOvernight } from "~/lib/overnight"; import { DayBreakdown } from "./DayBreakdown"; interface WaypointData { lat: number; lon: number; name?: string; overnight: boolean; } function getWaypointsFromYjs(waypoints: Y.Array>): WaypointData[] { return waypoints.toArray().map((yMap) => ({ lat: yMap.get("lat") as number, lon: yMap.get("lon") as number, name: yMap.get("name") as string | undefined, overnight: isOvernight(yMap), })); } interface WaypointSidebarProps { yjs: YjsState; routeStats?: { distance?: number; elevationGain?: number; elevationLoss?: number; }; days: DayStage[]; onWaypointHover?: (index: number | null) => void; } export function WaypointSidebar({ yjs, routeStats, days, onWaypointHover }: WaypointSidebarProps) { const { t } = useTranslation("planner"); const [waypoints, setWaypoints] = useState([]); useEffect(() => { const update = () => setWaypoints(getWaypointsFromYjs(yjs.waypoints)); yjs.waypoints.observeDeep(update); update(); return () => yjs.waypoints.unobserveDeep(update); }, [yjs.waypoints]); const deleteWaypoint = useCallback( (index: number) => { yjs.doc.transact(() => yjs.waypoints.delete(index, 1), "local"); }, [yjs.doc, yjs.waypoints], ); const moveWaypoint = useCallback( (from: number, to: number) => { if (from === to || from < 0 || to < 0) return; const item = yjs.waypoints.get(from); if (!item) return; const data = { lat: item.get("lat") as number, lon: item.get("lon") as number, name: item.get("name") as string | undefined, overnight: isOvernight(item), }; yjs.doc.transact(() => { yjs.waypoints.delete(from, 1); const yMap = new Y.Map(); yMap.set("lat", data.lat); yMap.set("lon", data.lon); if (data.name) yMap.set("name", data.name); if (data.overnight) yMap.set("overnight", true); yjs.waypoints.insert(to, [yMap]); }, "local"); }, [yjs.waypoints, yjs.doc], ); const toggleOvernight = useCallback( (index: number) => { const wp = waypoints[index]; if (!wp) return; setOvernight(yjs, index, !wp.overnight); }, [yjs, waypoints], ); const hasMultipleDays = days.length > 1; const renderWaypointRow = (wp: WaypointData, i: number) => (
  • onWaypointHover?.(i)} onMouseLeave={() => onWaypointHover?.(null)} > {i + 1}

    {wp.name ?? `${wp.lat.toFixed(4)}, ${wp.lon.toFixed(4)}`}

    {wp.overnight && ( {t("multiDay.overnight")} )}
    {/* Overnight toggle — not on first or last waypoint */} {i > 0 && i < waypoints.length - 1 && ( )} {i > 0 && ( )} {i < waypoints.length - 1 && ( )}
  • ); return (
    {/* Header with route summary */}

    {t("sidebar.waypoints")} ({waypoints.length})

    {routeStats && routeStats.distance !== undefined && (

    {(routeStats.distance / 1000).toFixed(1)} km {routeStats.elevationGain !== undefined && ` · ↑${routeStats.elevationGain} m`} {hasMultipleDays && ` · ${t("multiDay.dayCount", { count: days.length })}`}

    )}
    {/* Waypoint list */}
    {waypoints.length === 0 ? (

    Click on the map to add waypoints

    ) : hasMultipleDays ? ( {(_day, { start, end }) => (
      {waypoints.slice(start, end + 1).map((wp, offset) => renderWaypointRow(wp, start + offset))}
    )}
    ) : (
      {waypoints.map((wp, i) => renderWaypointRow(wp, i))}
    )}
    {/* Stats footer — only shown for single-day view (multi-day shows per-day stats inline) */} {!hasMultipleDays && routeStats && routeStats.distance !== undefined && (

    {(routeStats.distance / 1000).toFixed(1)} km

    {t("multiDay.distance")}

    {routeStats.elevationGain !== undefined && (

    ↑ {routeStats.elevationGain} m

    {t("multiDay.ascent")}

    )} {routeStats.elevationLoss !== undefined && (

    ↓ {routeStats.elevationLoss} m

    {t("multiDay.descent")}

    )}
    )}
    ); }