import { createContext, useCallback, useContext, useEffect, useState, } from 'react'; const NavigationContext = createContext(() => {}); /** * Path routing over the History API. The site record serves index.html for * every unmatched path, so a reload or a shared link lands on the same screen a * click does. * * The query string is tracked alongside the path, because a space is addressed * by its AT-URI and that URI holds slashes. Carrying it as a parameter keeps * every screen a real, shareable page without encoding a path inside a path. */ export function useLocation() { const [href, setHref] = useState( () => window.location.pathname + window.location.search, ); useEffect(() => { const onPop = () => setHref(window.location.pathname + window.location.search); window.addEventListener('popstate', onPop); return () => window.removeEventListener('popstate', onPop); }, []); const navigate = useCallback((next) => { window.history.pushState({}, '', next); setHref(next); window.scrollTo(0, 0); }, []); const url = new URL(href, window.location.origin); return { path: url.pathname, params: url.searchParams, navigate }; } export function NavigationProvider({ navigate, children }) { return ( {children} ); } export function useNavigate() { return useContext(NavigationContext); } /** * The href for a screen: a path and the parameters that address what it shows. * @param {string} path * @param {Record} params * @returns {string} */ export function href(path, params = {}) { const search = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value) search.set(key, value); } const query = search.toString(); return query ? `${path}?${query}` : path; }