import type { NDKUserProfile } from '@nostr-dev-kit/ndk'; import { LitElement, css, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; import { getUserProfile } from '../ndk'; import '@components/General/Card'; @customElement('arx-profile-route') export class NostrProfile extends LitElement { @property({ type: String }) npub = ''; @state() private profile: NDKUserProfile | undefined; @state() private error: string | null = null; static override styles = css` :host { display: block; } .banner-container { position: relative; height: 20rem; overflow: hidden; border-radius: var(--radius-box) var(--radius-box) 0 0; border: var(--border) solid var(--color-base-300); border-bottom: none; } .banner-image { position: absolute; inset: 0; transform: scale(1); transition: transform 700ms cubic-bezier(0.4, 0, 0.2, 1); } .banner-image:hover { transform: scale(1.05); } .banner-image img { width: 100%; height: 100%; object-fit: cover; } .banner-overlay { position: absolute; inset: 0; background: linear-gradient( to bottom, transparent, oklch(from var(--color-base-content) l c h / 0.2), oklch(from var(--color-base-content) l c h / 0.4) ); } .profile-container { max-width: 64rem; margin: 0 auto; padding: 0 1rem; position: relative; z-index: 10; } .profile-container.with-banner { margin-top: -8rem; } .profile-container.no-banner { margin-top: 4rem; } .profile-content { display: flex; flex-direction: column; gap: 1.5rem; } @media (min-width: 768px) { .profile-content { flex-direction: row; align-items: flex-start; } } .profile-image-container { position: relative; } .profile-image { width: 10rem; height: 10rem; border-radius: 50%; object-fit: cover; border: calc(var(--border) * 2) solid var(--color-base-100); 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); transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1); } .profile-image:hover { transform: scale(1.05); } .profile-image-placeholder { width: 10rem; height: 10rem; border-radius: 50%; background: linear-gradient( 135deg, var(--color-base-200), var(--color-base-300) ); display: flex; align-items: center; justify-content: center; border: calc(var(--border) * 2) solid var(--color-base-100); 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); } .placeholder-icon { width: 5rem; height: 5rem; color: var(--color-secondary); } .profile-info { flex: 1; } .profile-header { display: flex; flex-direction: column; justify-content: space-between; } @media (min-width: 768px) { .profile-header { flex-direction: row; align-items: center; } } .display-name { font-size: 1.875rem; font-weight: bold; color: var(--color-base-content); display: flex; align-items: center; gap: 0.5rem; margin: 0; } .verified-icon { color: var(--color-accent); } .nip05 { color: var(--color-secondary); display: flex; align-items: center; gap: 0.25rem; margin-top: 0.25rem; } .action-buttons { display: flex; gap: 0.75rem; margin-top: 1rem; } @media (min-width: 768px) { .action-buttons { margin-top: 0; } } .links-section { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; margin-top: 2rem; padding-top: 1.5rem; border-top: var(--border) solid var(--color-base-300); } .link-item { display: flex; align-items: center; gap: 0.5rem; padding: 1rem; border-radius: var(--radius-field); background-color: var(--color-base-200); transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1); text-decoration: none; color: var(--color-base-content); } .link-item:hover { background-color: var(--color-base-300); 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.1); } .link-icon { width: 1.5rem; height: 1.5rem; } .link-icon.website { color: var(--color-accent); } .link-icon.lightning { color: var(--color-warning); } .bio { white-space: pre-line; font-size: 0.9rem; color: var(--color-secondary); margin: 1rem 0; padding: 1rem; line-height: 1.6; background-color: var(--color-base-200); border-radius: var(--radius-field); border-left: 3px solid var(--color-accent); } `; protected override async firstUpdated() { try { this.profile = await getUserProfile(this.npub); } catch (err) { this.error = 'Failed to load profile'; console.error(err); } } private get displayName() { if (!this.profile) return this.npub; return this.profile.displayName || this.profile.name || this.npub.substring(0, 8); } override render() { if (this.error) { return html`<arx-error-view .error=${this.error}></arx-error-view>`; } if (!this.profile) { return html`<arx-loading-view></arx-loading-view>`; } return html` ${when( this.profile.banner, () => html` <div class="banner-container"> <div class="banner-image"> <img src=${this.profile!.banner} alt="Banner" /> </div> <div class="banner-overlay"></div> </div> `, )} <div class=${this.profile.banner ? 'profile-container with-banner' : 'profile-container no-banner'} > <arx-card> <div class="profile-content"> <div class="profile-image-container"> ${when( this.profile.image, () => html`<img src=${this.profile!.image} alt="Profile" class="profile-image" />`, () => html` <div class="profile-image-placeholder"> <svg-icon icon="mdi:account" class="placeholder-icon" ></svg-icon> </div> `, )} </div> <div class="profile-info"> <div class="profile-header"> <div> <h1 class="display-name"> ${this.displayName} ${when( this.profile.verified, () => html` <span class="verified-icon"> <svg-icon icon="mdi:check-decagram"></svg-icon> </span> `, )} </h1> ${when( this.profile.nip05, () => html` <p class="nip05"> <svg-icon icon="mdi:at"></svg-icon> ${this.profile!.nip05} </p> `, )} </div> </div> ${when(this.profile.about, () => html` <p class="bio">${this.profile!.about}</p> `)} </div> </div> <div class="links-section"> ${when( this.profile.website, () => html` <a href=${this.profile!.website} target="_blank" rel="noopener noreferrer" class="link-item" > <svg-icon icon="mdi:web" class="link-icon website"></svg-icon> <span>${this.profile!.website}</span> </a> `, )} ${when( this.profile.lud16, () => html` <a href="lightning:${this.profile!.lud16}" class="link-item"> <svg-icon icon="mdi:lightning-bolt" class="link-icon lightning" ></svg-icon> <span>${this.profile!.lud16}</span> </a> `, )} </div> </arx-card> </div> `; } }