import * as React from "react"; import { useSearchParams } from "react-router"; import { useColumnsContext } from "../contexts/ColumnsContext"; export function toUrlSafeBase64(input: string): string { const utf8Bytes = new TextEncoder().encode(input); let base64 = btoa(String.fromCharCode(...utf8Bytes)); base64 = base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); return base64; } export function fromUrlSafeBase64(urlSafeBase64: string): string { let base64 = urlSafeBase64.replace(/-/g, "+").replace(/_/g, "/"); while (base64.length % 4 !== 0) { base64 += "="; } const binaryString = atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return new TextDecoder().decode(bytes); } export function brevityReplacer(_key: string, value: any): any { if (value?.__type === "column" && value.key) { return { __type: "columnRef", key: value.key }; } return value; } export function parseState>( search: string, reviver: (key: string, value: any) => any, ): T { const params = new URLSearchParams(search); return Array.from(params.entries()).reduce( (memo, [key, value]) => { try { const decoded = fromUrlSafeBase64(value); const parsed = JSON.parse(decoded, reviver); memo[key] = parsed; return memo; } catch (error) { memo[key] = value; return memo; } }, {} as Record, ) as T; } export function useServerState< T extends Record | null | undefined, >(key: string, initialValue: T) { const [searchParams, setSearchParams] = useSearchParams(); const $$columns = useColumnsContext(); // Read the state from the URL on first render const readFromUrl = React.useCallback((): T => { const encoded = searchParams.get(key); if (encoded) { try { const decoded = fromUrlSafeBase64(encoded); return JSON.parse(decoded, (_key, value) => { if (value?.__type === "columnRef" && value.key) { return $$columns?.[value.key]; } return value; }) as T; } catch (error) { // If decoding or parsing fails, fallback to initialValue return initialValue; } } return initialValue; }, [key, searchParams, initialValue]); const [state, setState] = React.useState(readFromUrl); // Whenever state changes, encode & sync it to the URL React.useEffect(() => { try { const newSearchParams = new URLSearchParams(searchParams); if (!initialValue && (state === null || state === undefined)) { newSearchParams.delete(key); } else { const encoded = toUrlSafeBase64(JSON.stringify(state, brevityReplacer)); // Copy current search params so we don't lose other params if (encoded) { newSearchParams.set(key, encoded); } else if (newSearchParams.has(key)) { newSearchParams.delete(key); } } setSearchParams(newSearchParams, { replace: true, preventScrollReset: true, }); } catch (err) { // Handle error in encoding if necessary console.error("Failed to encode URL state:", err); } }, [key, searchParams, setSearchParams, state, initialValue]); return [state, setState] as const; }