Eve/src/components/Header.ts
Danny Morabito bf3c950da0
🧭 Add navigation sidebar and 📏 enforce minimum window dimensions
- Add sidebar component to enhance site navigation and improve user experience
- Implement window size constraints (min 1366x768) to ensure proper display across devices
2025-04-08 18:42:38 +02:00

304 lines
8.9 KiB
TypeScript

import { ndk } from '@/ndk';
import type { ArxInputChangeEvent } from '@components/General/Input';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import * as nip19 from '@nostr/tools/nip19';
import { LitElement, css, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { keyed } from 'lit/directives/keyed.js';
import { map } from 'lit/directives/map.js';
import { when } from 'lit/directives/when.js';
import '@components/General/Input';
import '@components/HeaderSugestion';
@customElement('arx-header')
export class Header extends LitElement {
@property({ type: String }) override title = 'Eve';
@property({ type: String }) url = 'eve://home';
@property({ type: Boolean }) canGoBack = false;
@property({ type: Boolean }) canGoForward = false;
private searchQuery = '';
private _debounceTimeout?: ReturnType<typeof setTimeout>;
@state() private showSuggestions = false;
@state() private events: NDKEvent[] = [];
@state() private suggestions: NDKEvent[] = [];
static override styles = css`
:host {
display: block;
z-index: 999999;
}
header {
background: var(--color-primary);
backdrop-filter: blur(10px);
height: var(--font-2xl, 3rem);
font-size: var(--font-md, 1rem);
transition: all 0.3s ease;
display: flex;
align-items: space-between;
padding: 0 var(--space-md, 1rem);
}
.nav-buttons {
display: flex;
gap: var(--space-xs, 0.5rem);
padding: 0 var(--space-xs, 0.5rem);
}
button {
text-decoration: none;
color: var(--color-primary-content);
background: oklch(from var(--color-primary-content) l c h / 0.1);
border: var(--border) solid
oklch(from var(--color-primary-content) l c h / 0.2);
padding: var(--space-xs, 0.5rem);
border-radius: 50%;
font-size: var(--font-md, 1rem);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&:hover {
background: oklch(from var(--color-primary-content) l c h / 0.2);
transform: translateY(-2px);
box-shadow: calc(var(--depth) * 2px) calc(var(--depth) * 2px)
calc(var(--depth) * 4px)
oklch(from var(--color-base-content) l c h / 0.15);
}
&:active {
transform: translateY(1px);
}
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
.search-container {
flex: 1;
position: relative;
}
arx-input {
transform: translateY(3px);
}
.suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: oklch(from var(--color-base-100) l c h / 0.9);
backdrop-filter: blur(10px);
border: var(--border) solid var(--color-base-200);
border-top: none;
border-radius: 0 0 var(--radius-field) var(--radius-field);
box-shadow: calc(var(--depth) * 4px) calc(var(--depth) * 4px)
calc(var(--depth) * 8px)
oklch(from var(--color-base-content) l c h / 0.2);
z-index: 9999;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
}
`;
override firstUpdated() {
ndk
.fetchEvents({
limit: 50_000,
})
.then((events) => {
this.events = [...events];
});
}
override render() {
return html`
<header>
<div class="nav-buttons">
<button
class=${classMap({ disabled: !this.canGoBack })}
@click=${this._handleGoBack}
aria-label="Go back"
>
<iconify-icon icon="material-symbols:arrow-back"></iconify-icon>
</button>
<button
class=${classMap({ disabled: !this.canGoForward })}
@click=${this._handleGoForward}
aria-label="Go forward"
>
<iconify-icon icon="material-symbols:arrow-forward"></iconify-icon>
</button>
<button @click=${this._handleGoHome} aria-label="Home">
<iconify-icon icon="material-symbols:home"></iconify-icon>
</button>
</div>
<div class="search-container">
<arx-input
type="text"
.value=${this.searchQuery}
placeholder=${this.url}
@keyup=${this._handleSearch}
@focus=${this._handleFocus}
@change=${this._handleInput}
></arx-input>
${when(
this.showSuggestions,
() => html`
<div class="suggestions">
${map(this.suggestions, (suggestion: NDKEvent) =>
keyed(
suggestion.id,
html`
<arx-header-suggestion
.event=${suggestion}
.allEvents=${this.events}
@click=${() => this._handleSuggestionClick(suggestion)}
></arx-header-suggestion>
`,
),
)}
</div>
`,
)}
</div>
<div class="nav-buttons">
<button @click=${this._goToWallet}>
<iconify-icon icon="material-symbols:wallet"></iconify-icon>
</button>
</div>
</header>
`;
}
private _goToWallet() {
window.location.hash = 'wallet';
}
private _handleFocus() {
this.showSuggestions = true;
}
private _handleInput(e: ArxInputChangeEvent) {
this.searchQuery = e.detail.value;
if (this._debounceTimeout) {
clearTimeout(this._debounceTimeout);
}
if (!this.searchQuery.trim()) {
this.suggestions = [];
this.showSuggestions = false;
return;
}
this._debounceTimeout = setTimeout(() => {
const query = this.searchQuery.toLowerCase();
const isNoteSearch = query.startsWith('note1');
const isEventSearch = query.startsWith('nevent1');
const isPubkeySearch = query.startsWith('npub1');
this.suggestions = this.events
.filter((event: NDKEvent) => {
if (event.kind === 11 || event.kind === 1111) return false; // hide old forum events
if (event.kind === 30890 || event.kind === 30891) return false; // ignore old-in-dev events
if (event.kind === 60890) return false; // don't include the actual forum
if (event.kind === 60891) {
const categoryId = event.tags.find((tag) => tag[0] === 'd')?.[1] || '';
const forum = this.events.find(
(e) => e.kind === 60890 && e.tags.some((tag) => tag[0] === 'forum' && tag[1] === `60891:${categoryId}`),
);
if (!forum) return false; // ignore orphan forum categories
}
if (event.id.includes(query)) return true;
if (isNoteSearch) {
const noteId = nip19.noteEncode(event.id);
return noteId.includes(query);
}
if (isEventSearch) {
const eventId = event.encode();
return eventId.includes(query);
}
if (isPubkeySearch && event.kind === 0) {
const pubkey = nip19.npubEncode(event.pubkey);
return pubkey.includes(query);
}
if (event.content.toLowerCase().includes(query)) return true;
return event.tags.some((tag) => tag.length > 1 && tag[1].toLowerCase().includes(query));
})
.slice(0, 20);
this.showSuggestions = this.suggestions.length > 0;
}, 50);
this.showSuggestions = true;
}
private _handleSearch(e: KeyboardEvent) {
if (e.key !== 'Enter') return;
this.showSuggestions = false;
if (this.searchQuery.startsWith('npub1')) {
try {
const { type } = nip19.decode(this.searchQuery);
if (type === 'npub') {
window.location.hash = `profile/${this.searchQuery}`;
return;
}
} catch (e) {}
}
const hash = this.searchQuery.replace('eve://', '#');
window.location.hash = hash;
}
private _handleGoBack() {
this.dispatchEvent(new CustomEvent('go-back'));
}
private _handleGoForward() {
this.dispatchEvent(new CustomEvent('go-forward'));
}
private _handleGoHome() {
window.location.hash = '#';
}
private _handleSuggestionClick(suggestion: NDKEvent) {
window.location.hash = '#';
this.showSuggestions = false;
console.log(suggestion);
switch (suggestion.kind) {
case 0:
window.location.hash = `profile/${suggestion.pubkey}`;
break;
case 60890:
case 60891:
window.location.hash = 'arbor';
break;
case 892:
window.location.hash = `arbor/topics/${suggestion.id}`;
break;
case 893: {
const threadId = suggestion.tags.find((tag) => tag[0] === 'E')?.[1]!;
window.location.hash = `arbor/topics/${threadId}`;
break;
}
default:
window.location.hash = `event/${suggestion.id}`;
}
}
}