import * as Signal from "@preact/signals"; import * as React from "react"; ///////// Metas type MetaFields = {title?:string, description?:string} type MetaRecord = MetaFields&{id:string, baked?:MetaFields} const Stack = [] as MetaRecord[]; const StackPush =(m:MetaRecord)=> { m.baked = {...(Stack.at(-1)?.baked || {}), ...m}; /* const previous = Stack.at(-1)?.baked; if(previous) { Object.entries(previous).forEach(([key, prevValue], i)=> { const fields = m.baked as Record; const currValue = fields[key]; if(currValue) { } fields[key] = prevValue + concat + currValue; }) } */ Stack.push(m); Update(); } const StackPop =(id:string)=> {Stack.splice(Stack.findIndex( item => item.id === id ), 1); Update();} const Update =()=> document.title = Stack.at(-1)?.baked?.title || "---"; Update(); export const useMeta=(fields:MetaFields)=> { const id = React.useId(); React.useEffect(()=>{ StackPush({...fields, id}); return ()=>StackPop(id); }, []); } export const Meta =(props:MetaFields)=> { useMeta(props); return null; } //////// Router type RouteContextData = { /** Current nested depth of the router */ nestedDepth:number, /** Index into the page path to start matching routes */ pathIndex:number, /** Collection of keys from matched routes ("page/:key/") */ keys: Record, blocked: Signal.Signal } const blocked = Signal.signal(false as false|string) const context = React.createContext({nestedDepth:0, pathIndex:0, keys:{}, blocked} as RouteContextData); //// Create Signals export const pageURL = Signal.signal(new URL(globalThis?.location.href || "")); export const pagePath = Signal.signal([] as string[]); Signal.effect(()=>{ blocked.value = false; pagePath.value = pageURL.value.pathname.split("/").filter(part=>part!=""); }); //// Add handlers globalThis.addEventListener("click", e=> { (e.composedPath() as HTMLAnchorElement[]).find((step)=>{ if(step.href) { const url = new URL(step.href); if(url.origin == document.location.origin) { e.preventDefault(); history.pushState({}, "", url); pageURL.value = url; } return true; } }) }); globalThis.addEventListener("popstate", _=> pageURL.value = new URL(globalThis.location.href) ); //// Rendering context type PathFields = {pathAfter:string[], pathBefore:[], compare:typeof compare} const compare =(pathA:string[], pathB:string[])=> { const emptyMatch = !pathA.length && !pathB.length; const comparisonSize = Math.min(pathA.length, pathB.length); if(comparisonSize == 0 && emptyMatch === false) // one of the arrays is empty and one is not { return null; } const keys = {} as Record; // not doing anything with this currently for(let i=0; i { const routeContext = React.useContext(context); const pathFull = pagePath.value; const pathAfter = pathFull.slice(routeContext.pathIndex); const pathBefore = pathFull.slice(0, Math.max(0, routeContext.pathIndex-1)); return { ...routeContext, pathAfter, pathBefore, compare } as PathFields&RouteContextData; } export const Route =(props:{path?:string[], children:React.ReactNode|React.ReactNode[]}):React.JSX.Element|null=> { const {nestedDepth, pathIndex, pathAfter, compare, keys, blocked} = useRoute(); if(blocked.peek()){ return null; } const passOn:RouteContextData = { nestedDepth:nestedDepth+1, pathIndex, keys, blocked:Signal.signal(false) } if(props.path) { const check = compare(props.path, pathAfter); if (!check){ return null; } blocked.value = "/"+props.path.join("/"); passOn.pathIndex += check.comparisonSize; passOn.keys = {...keys, ...check.keys}; } return {props.children}; }