import React, { ReactNode, useState, useCallback, useEffect, useMemo, } from "react"; import { Router, RouterContext } from "./context.js"; import { PageInfo } from "../viewer/types.js"; import { CoID, RawCoValue } from "cojson"; export function HashRouterProvider({ children, defaultPath, }: { children: ReactNode; defaultPath?: PageInfo[]; }) { const [path, setPath] = useState(() => { if (typeof window === "undefined") return defaultPath || []; const hash = window.location.hash.slice(2); // Remove '#/' const defaultEncoded = encodePathToHash(defaultPath || []); if (defaultPath) { window.history.pushState({}, "", `#/${defaultEncoded}`); return defaultPath; } if (hash) { const path = decodePathFromHash(hash); return path; } window.history.pushState({}, "", `#/${encodePathToHash([])}`); return []; }); const updatePath = useCallback((newPath: PageInfo[]) => { setPath(newPath); if (typeof window !== "undefined") { const hash = encodePathToHash(newPath); window.history.pushState({}, "", `#/${hash}`); } }, []); useEffect(() => { if (typeof window === "undefined") return; const handleHashChange = () => { const hash = window.location.hash.slice(2); const currentPath = encodePathToHash(path); if (hash === currentPath) return; if (hash) { try { const newPath = decodePathFromHash(hash); setPath(newPath); } catch (e) { console.error("Failed to parse hash:", e); } } else if (defaultPath) { setPath(defaultPath); } }; window.addEventListener("hashchange", handleHashChange); return () => window.removeEventListener("hashchange", handleHashChange); }, [path, defaultPath]); useEffect(() => { if (defaultPath) { updatePath(defaultPath); } }, [defaultPath]); const router: Router = useMemo(() => { const addPages = (newPages: PageInfo[]) => { updatePath([...path, ...newPages]); }; const goToIndex = (index: number) => { updatePath(path.slice(0, index + 1)); }; const setPage = (coId: CoID) => { updatePath([{ coId, name: "Root" }]); }; const goBack = () => { updatePath(path.slice(0, path.length - 1)); }; return { path, addPages, goToIndex, setPage, goBack, }; }, [path, updatePath]); return ( {children} ); } function encodePathToHash(path: PageInfo[]): string { return path .map((page) => { if (page.name && page.name !== "Root") { return `${page.coId}:${encodeURIComponent(page.name)}`; } return page.coId; }) .join("/"); } function decodePathFromHash(hash: string): PageInfo[] { return hash.split("/").map((segment) => { const [coId, encodedName] = segment.split(":"); return { coId, name: encodedName ? decodeURIComponent(encodedName) : undefined, } as PageInfo; }); }