314 lines
7.7 KiB
TypeScript
314 lines
7.7 KiB
TypeScript
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<string, string>;
|
|
}
|
|
|
|
@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<void> {
|
|
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`
|
|
<div class="window">
|
|
<div class="window-content">
|
|
<arx-initial-setup
|
|
@finish=${() => this.finishSetup()}
|
|
></arx-initial-setup>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
override render() {
|
|
if (!this.ccnSetup) return this.renderSetup();
|
|
return html`
|
|
<arx-header
|
|
?canGoBack=${this.currentIndex > 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"
|
|
></arx-header>
|
|
<div class="window ${this.currentRoute.pattern === 'home' ? 'hide-overflow' : ''}">
|
|
<div class="window-content">
|
|
${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}
|
|
></${this.currentRoute.component}>
|
|
`,
|
|
)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|