Eve/src/routes/Profile.ts
Danny Morabito dc9abee715
🎨 🚀 Overhaul UI/UX with comprehensive design system improvements
 Features added:

- 🔍 Implement functional search in header navigation
- ⚙️ Add basic user settings page
- 📱 Make dashboard fully responsive

🔧 Enhancements:

- 🎭 Standardize CSS with consistent theming across components
- 🧹 Remove unused CSS for better performance
- 📊 Improve dashboard layout and visual hierarchy
- 📦 Redesign last block widget for better usability

💅 This commit introduces a cohesive design system with functional design-token components for a more  polished user experience.
2025-03-20 09:46:47 +01:00

364 lines
8.9 KiB
TypeScript

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>
`;
}
}