7 changed files with 309 additions and 108 deletions
@ -0,0 +1,107 @@
@@ -0,0 +1,107 @@
|
||||
<script lang="ts"> |
||||
import { onMount } from 'svelte'; |
||||
import { NostrClient } from '../services/nostr/nostr-client.js'; |
||||
import { DEFAULT_NOSTR_RELAYS } from '../config.js'; |
||||
import { KIND } from '../types/nostr.js'; |
||||
import { nip19 } from 'nostr-tools'; |
||||
|
||||
interface Props { |
||||
pubkey: string; |
||||
} |
||||
|
||||
let { pubkey }: Props = $props(); |
||||
|
||||
let userProfile = $state<{ name?: string; picture?: string } | null>(null); |
||||
let loading = $state(true); |
||||
|
||||
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); |
||||
|
||||
onMount(async () => { |
||||
await loadUserProfile(); |
||||
}); |
||||
|
||||
async function loadUserProfile() { |
||||
try { |
||||
// Fetch user profile (kind 0 - metadata) |
||||
const profileEvents = await nostrClient.fetchEvents([ |
||||
{ |
||||
kinds: [0], |
||||
authors: [pubkey], |
||||
limit: 1 |
||||
} |
||||
]); |
||||
|
||||
if (profileEvents.length > 0) { |
||||
try { |
||||
const profile = JSON.parse(profileEvents[0].content); |
||||
userProfile = { |
||||
name: profile.name, |
||||
picture: profile.picture |
||||
}; |
||||
} catch { |
||||
// Invalid JSON, ignore |
||||
} |
||||
} |
||||
} catch (err) { |
||||
console.warn('Failed to load user profile:', err); |
||||
} finally { |
||||
loading = false; |
||||
} |
||||
} |
||||
|
||||
function getShortNpub(): string { |
||||
try { |
||||
const npub = nip19.npubEncode(pubkey); |
||||
return `${npub.slice(0, 8)}...${npub.slice(-4)}`; |
||||
} catch { |
||||
return `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`; |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<div class="user-badge"> |
||||
{#if userProfile?.picture} |
||||
<img src={userProfile.picture} alt="Profile" class="user-badge-avatar" /> |
||||
{:else} |
||||
<img src="/favicon.png" alt="Profile" class="user-badge-avatar user-badge-avatar-fallback" /> |
||||
{/if} |
||||
<span class="user-badge-name">{userProfile?.name || getShortNpub()}</span> |
||||
</div> |
||||
|
||||
<style> |
||||
.user-badge { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 0.5rem; |
||||
padding: 0.25rem 0.75rem; |
||||
border-radius: 1.5rem; |
||||
background: var(--card-bg); |
||||
border: 1px solid var(--border-color); |
||||
transition: all 0.2s ease; |
||||
} |
||||
|
||||
.user-badge:hover { |
||||
border-color: var(--accent); |
||||
background: var(--bg-secondary); |
||||
} |
||||
|
||||
.user-badge-avatar { |
||||
width: 24px; |
||||
height: 24px; |
||||
border-radius: 50%; |
||||
object-fit: cover; |
||||
flex-shrink: 0; |
||||
} |
||||
|
||||
.user-badge-avatar-fallback { |
||||
filter: grayscale(100%); |
||||
opacity: 0.7; |
||||
} |
||||
|
||||
.user-badge-name { |
||||
font-size: 0.875rem; |
||||
color: var(--text-primary); |
||||
font-weight: 500; |
||||
white-space: nowrap; |
||||
} |
||||
</style> |
||||
@ -1,22 +1,67 @@
@@ -1,22 +1,67 @@
|
||||
/** |
||||
* Pino logger service |
||||
* Provides structured logging with pino-pretty for development |
||||
* Browser-safe: falls back to console in browser environments |
||||
*/ |
||||
|
||||
import pino from 'pino'; |
||||
|
||||
const logger = pino({ |
||||
level: process.env.LOG_LEVEL || 'info', |
||||
...(process.env.NODE_ENV === 'development' && { |
||||
transport: { |
||||
target: 'pino-pretty', |
||||
options: { |
||||
colorize: true, |
||||
translateTime: 'HH:MM:ss Z', |
||||
ignore: 'pid,hostname' |
||||
} |
||||
function createConsoleLogger() { |
||||
return { |
||||
info: (...args: any[]) => console.log('[INFO]', ...args), |
||||
error: (...args: any[]) => console.error('[ERROR]', ...args), |
||||
warn: (...args: any[]) => console.warn('[WARN]', ...args), |
||||
debug: (...args: any[]) => console.debug('[DEBUG]', ...args), |
||||
trace: (...args: any[]) => console.trace('[TRACE]', ...args), |
||||
fatal: (...args: any[]) => console.error('[FATAL]', ...args) |
||||
}; |
||||
} |
||||
|
||||
// Check if we're in a Node.js environment
|
||||
const isNode = typeof process !== 'undefined' && process.versions?.node; |
||||
|
||||
let logger: any; |
||||
|
||||
if (isNode) { |
||||
// Server-side: use pino
|
||||
// Use dynamic import to avoid bundling for browser
|
||||
const initPino = async () => { |
||||
try { |
||||
const pinoModule = await import('pino'); |
||||
const pino = pinoModule.default; |
||||
const logLevel = (typeof process !== 'undefined' && process.env?.LOG_LEVEL) || 'info'; |
||||
const isDev = typeof process !== 'undefined' && process.env?.NODE_ENV === 'development'; |
||||
|
||||
return pino({ |
||||
level: logLevel, |
||||
...(isDev && { |
||||
transport: { |
||||
target: 'pino-pretty', |
||||
options: { |
||||
colorize: true, |
||||
translateTime: 'HH:MM:ss Z', |
||||
ignore: 'pid,hostname' |
||||
} |
||||
} |
||||
}) |
||||
}); |
||||
} catch { |
||||
return createConsoleLogger(); |
||||
} |
||||
}) |
||||
}); |
||||
}; |
||||
|
||||
// Initialize with console logger first
|
||||
logger = createConsoleLogger(); |
||||
|
||||
// Upgrade to pino asynchronously (non-blocking)
|
||||
initPino().then(pinoLogger => { |
||||
// Replace the logger object
|
||||
Object.setPrototypeOf(logger, pinoLogger); |
||||
Object.assign(logger, pinoLogger); |
||||
}).catch(() => { |
||||
// Keep console logger if pino fails
|
||||
}); |
||||
} else { |
||||
// Browser-side: use console with similar API
|
||||
logger = createConsoleLogger(); |
||||
} |
||||
|
||||
export default logger; |
||||
|
||||
Loading…
Reference in new issue