mirror of
https://github.com/hoornet/vega.git
synced 2026-05-29 19:59:27 -07:00
Add profile view with clickable names/avatars
- ProfileView shows avatar, bio, nip05, website, recent notes - Clicking any name or avatar navigates to their profile - Add fetchUserNotes to nostr lib (kind 1 by author) - Add openProfile action + selectedPubkey to UI store - Back button returns to feed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useRef } from "react";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { timeAgo, shortenPubkey } from "../../lib/utils";
|
||||
import { publishReaction, publishReply } from "../../lib/nostr";
|
||||
import { NoteContent } from "./NoteContent";
|
||||
@@ -18,6 +19,7 @@ export function NoteCard({ event }: NoteCardProps) {
|
||||
const time = event.created_at ? timeAgo(event.created_at) : "";
|
||||
|
||||
const { loggedIn } = useUserStore();
|
||||
const { openProfile } = useUIStore();
|
||||
const [liked, setLiked] = useState(false);
|
||||
const [liking, setLiking] = useState(false);
|
||||
const [showReply, setShowReply] = useState(false);
|
||||
@@ -68,19 +70,19 @@ export function NoteCard({ event }: NoteCardProps) {
|
||||
<article className="border-b border-border px-4 py-3 hover:bg-bg-hover transition-colors duration-100">
|
||||
<div className="flex gap-3">
|
||||
{/* Avatar */}
|
||||
<div className="shrink-0">
|
||||
<div className="shrink-0 cursor-pointer" onClick={() => openProfile(event.pubkey)}>
|
||||
{avatar ? (
|
||||
<img
|
||||
src={avatar}
|
||||
alt=""
|
||||
className="w-9 h-9 rounded-sm object-cover bg-bg-raised"
|
||||
className="w-9 h-9 rounded-sm object-cover bg-bg-raised hover:opacity-80 transition-opacity"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-9 h-9 rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-xs">
|
||||
<div className="w-9 h-9 rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-xs hover:border-accent/40 transition-colors">
|
||||
{name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
@@ -89,7 +91,10 @@ export function NoteCard({ event }: NoteCardProps) {
|
||||
{/* Content */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-2 mb-0.5">
|
||||
<span className="text-text font-medium truncate text-[13px]">{name}</span>
|
||||
<span
|
||||
className="text-text font-medium truncate text-[13px] cursor-pointer hover:text-accent transition-colors"
|
||||
onClick={() => openProfile(event.pubkey)}
|
||||
>{name}</span>
|
||||
{nip05 && (
|
||||
<span className="text-text-dim text-[10px] truncate max-w-40">{nip05}</span>
|
||||
)}
|
||||
|
||||
107
src/components/profile/ProfileView.tsx
Normal file
107
src/components/profile/ProfileView.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import { fetchUserNotes } from "../../lib/nostr";
|
||||
import { shortenPubkey } from "../../lib/utils";
|
||||
import { NoteCard } from "../feed/NoteCard";
|
||||
|
||||
export function ProfileView() {
|
||||
const { selectedPubkey, setView } = useUIStore();
|
||||
const pubkey = selectedPubkey!;
|
||||
const profile = useProfile(pubkey);
|
||||
|
||||
const [notes, setNotes] = useState<NDKEvent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const name = profile?.displayName || profile?.name || shortenPubkey(pubkey);
|
||||
const avatar = profile?.picture;
|
||||
const about = profile?.about;
|
||||
const nip05 = profile?.nip05;
|
||||
const website = profile?.website;
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetchUserNotes(pubkey).then((events) => {
|
||||
setNotes(events);
|
||||
setLoading(false);
|
||||
}).catch(() => setLoading(false));
|
||||
}, [pubkey]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="border-b border-border px-4 py-2.5 flex items-center gap-3 shrink-0">
|
||||
<button
|
||||
onClick={() => setView("feed")}
|
||||
className="text-text-dim hover:text-text text-[11px] transition-colors"
|
||||
>
|
||||
← back
|
||||
</button>
|
||||
<h1 className="text-text text-sm font-medium">Profile</h1>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Profile card */}
|
||||
<div className="border-b border-border px-4 py-4">
|
||||
<div className="flex gap-4 items-start">
|
||||
{avatar ? (
|
||||
<img
|
||||
src={avatar}
|
||||
alt=""
|
||||
className="w-14 h-14 rounded-sm object-cover bg-bg-raised shrink-0"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-14 h-14 rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-lg shrink-0">
|
||||
{name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="text-text font-medium text-[15px]">{name}</div>
|
||||
{nip05 && (
|
||||
<div className="text-text-dim text-[11px] mt-0.5">{nip05}</div>
|
||||
)}
|
||||
{website && (
|
||||
<a
|
||||
href={website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent text-[11px] hover:text-accent-hover mt-0.5 block"
|
||||
>
|
||||
{website.replace(/^https?:\/\//, "")}
|
||||
</a>
|
||||
)}
|
||||
{about && (
|
||||
<p className="text-text text-[12px] mt-2 leading-relaxed whitespace-pre-wrap">
|
||||
{about}
|
||||
</p>
|
||||
)}
|
||||
<div className="text-text-dim text-[10px] font-mono mt-2">
|
||||
{shortenPubkey(pubkey)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
{loading && (
|
||||
<div className="px-4 py-8 text-text-dim text-[12px] text-center">
|
||||
Loading notes…
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && notes.length === 0 && (
|
||||
<div className="px-4 py-8 text-text-dim text-[12px] text-center">
|
||||
No notes found.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notes.map((event) => (
|
||||
<NoteCard key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user