import '@components/InitialSetup'; import '@routes/404Page'; import '@routes/Arbor/Home'; import '@routes/Arbor/NewPost'; import '@routes/Arbor/NewTopic'; import '@routes/Arbor/TopicView'; import '@routes/Home'; import '@routes/Profile'; import '@routes/Settings'; import { spread } from '@open-wc/lit-helpers'; import { LitElement, css } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { keyed } from 'lit/directives/keyed.js'; import { type StaticValue, html, literal } from 'lit/static-html.js'; export interface RouteParams { [key: string]: string; } interface Route { pattern: string; params: RouteParams; component: StaticValue; // component: typeof LitElement | ((params: RouteParams) => typeof LitElement); title?: string; meta?: Record; } @customElement('arx-eve-router') export default class EveRouter extends LitElement { private static routes: Route[] = [ { pattern: 'home', params: {}, component: literal`arx-eve-home`, }, { pattern: 'profile/:npub', params: {}, component: literal`arx-profile-route`, }, { pattern: 'arbor', params: {}, component: literal`arx-arbor-home`, }, { pattern: 'arbor/new-topic/:categoryId', params: {}, component: literal`arx-arbor-topic-creator`, }, { pattern: 'arbor/topics/:topicId', params: {}, component: literal`arx-arbor-topic-view`, }, { pattern: 'arbor/new-post/:topicId', params: {}, component: literal`arx-arbor-post-creator`, }, { pattern: 'settings', params: {}, component: literal`arx-settings`, }, { pattern: '404', params: {}, component: literal`arx-404-page`, }, ]; @state() private history: string[] = []; @state() private currentIndex = -1; @property() public ccnSetup = false; private beforeEachGuards: ((to: Route, from: Route | null) => boolean)[] = []; private afterEachHooks: ((to: Route, from: Route | null) => void)[] = []; static override styles = css` :host { height: 100vh; display: grid; grid-template-rows: auto 1fr; overflow: hidden; } ::-webkit-scrollbar { width: 12px; height: 12px; } ::-webkit-scrollbar-track { background: var(--color-base-200); border-radius: var(--radius-field); } ::-webkit-scrollbar-thumb { background: var(--color-base-300); border-radius: var(--radius-field); border: 2px solid var(--color-base-200); transition: var(--transition); } ::-webkit-scrollbar-thumb:hover { background: var(--color-neutral); } .window { overflow: auto; } .window-content { max-width: 1200px; overflow: visible; height: 100%; margin: 0 auto; padding: 1rem; } .hide-overflow { overflow: hidden; } `; constructor() { super(); this.initializeRouter(); if (this.ccnSetup) window.relay.start(localStorage.getItem('encryption_key')!); } override connectedCallback(): void { super.connectedCallback(); this.setupEventListeners(); } override disconnectedCallback(): void { super.disconnectedCallback(); window.removeEventListener('hashchange', this.handleHashChange); window.removeEventListener('popstate', this.handlePopState); } private initializeRouter(): void { const initialPath = this.currentPath; this.history = [initialPath]; this.currentIndex = 0; this.navigate(initialPath); } private setupEventListeners(): void { window.addEventListener('hashchange', this.handleHashChange.bind(this)); window.addEventListener('popstate', this.handlePopState.bind(this)); } private handleHashChange(): void { const newPath = this.currentPath; if (newPath !== this.history[this.currentIndex]) { this.updateHistory(newPath); this.requestUpdate(); } } private handlePopState(): void { this.requestUpdate(); } private updateHistory(newPath: string): void { this.history = this.history.slice(0, this.currentIndex + 1); this.history.push(newPath); this.currentIndex = this.history.length - 1; } private matchRoute(pattern: string, path: string): { isMatch: boolean; params: RouteParams } { const patternParts = pattern.split('/').filter(Boolean); const pathParts = path.split('/').filter(Boolean); const params: RouteParams = {}; if (patternParts.length !== pathParts.length) { return { isMatch: false, params }; } const isMatch = patternParts.every((patternPart, index) => { const pathPart = pathParts[index]; if (patternPart.startsWith(':')) { const paramName = patternPart.slice(1); params[paramName] = decodeURIComponent(pathPart); return true; } return patternPart === pathPart; }); return { isMatch, params }; } get currentPath(): string { const hash = window.location.hash?.slice(1); return hash === '' ? 'home' : hash; } get currentRoute(): Route { const route = EveRouter.routes.find((route) => { const { isMatch, params } = this.matchRoute(route.pattern, this.currentPath); if (isMatch) { route.params = params; return true; } return false; }); return route || this.getNotFoundRoute(); } private getNotFoundRoute(): Route { return { pattern: '404', params: {}, component: literal`arx-404-page`, }; } beforeEach(guard: (to: Route, from: Route | null) => boolean): void { this.beforeEachGuards.push(guard); } afterEach(hook: (to: Route, from: Route | null) => void): void { this.afterEachHooks.push(hook); } async navigate(path: string): Promise { const from = this.currentRoute; window.location.hash = path; const to = this.currentRoute; const canProceed = this.beforeEachGuards.every((guard) => guard(to, from)); if (canProceed) { this.requestUpdate(); for (const hook of this.afterEachHooks) { hook(to, from); } if (to.title) { document.title = to.title; } } } goBack(): void { if (this.currentIndex > 0) { this.currentIndex--; this.navigate(this.history[this.currentIndex]); } } goForward(): void { if (this.currentIndex < this.history.length - 1) { this.currentIndex++; this.navigate(this.history[this.currentIndex]); } } finishSetup() { this.ccnSetup = true; } renderSetup() { return html`
this.finishSetup()} >
`; } override render() { if (!this.ccnSetup) return this.renderSetup(); return html` 0} ?canGoForward=${this.currentIndex < this.history.length - 1} url="eve://${this.currentPath}" @navigate=${(e: CustomEvent) => this.navigate(e.detail)} @go-back=${this.goBack} @go-forward=${this.goForward} title="Eve" >
${keyed( this.currentRoute.params, html` <${this.currentRoute.component} ${spread(this.currentRoute.params)} path=${this.currentPath} @navigate=${this.navigate} @go-back=${this.goBack} @go-forward=${this.goForward} > `, )}
`; } }