mirror of
https://github.com/hoornet/vega.git
synced 2026-07-02 23:09:00 -07:00
Working feed: NDK + relay connection + live notes from Nostr
- Tailwind CSS + Zustand + NDK installed and configured - Sidebar with feed/relays/settings navigation - Global feed view with live notes from relays - Profile fetching with caching and deduplication - Relay connection with timeout handling - Note cards with avatar, name, timestamp, content - Dark theme, monospace, no-slop UI - Devtools enabled for debugging
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import { useEffect } from "react";
|
||||
import { useFeedStore } from "../../stores/feed";
|
||||
import { NoteCard } from "./NoteCard";
|
||||
|
||||
export function Feed() {
|
||||
const { notes, loading, connected, error, connect, loadFeed } = useFeedStore();
|
||||
|
||||
useEffect(() => {
|
||||
connect().then(() => loadFeed());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="border-b border-border px-4 py-2.5 flex items-center justify-between shrink-0">
|
||||
<h1 className="text-text text-sm font-medium tracking-wide">Global Feed</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
{connected && (
|
||||
<span className="text-success text-[11px] flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-success inline-block" />
|
||||
connected
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={loadFeed}
|
||||
disabled={loading}
|
||||
className="text-text-muted hover:text-text text-[11px] px-2 py-1 border border-border hover:border-text-dim transition-colors disabled:opacity-40"
|
||||
>
|
||||
{loading ? "loading…" : "refresh"}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Feed */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{error && (
|
||||
<div className="px-4 py-3 text-danger text-[12px] border-b border-border bg-danger/5">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && notes.length === 0 && (
|
||||
<div className="px-4 py-8 text-text-dim text-[12px] text-center">
|
||||
Connecting to relays…
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && notes.length === 0 && !error && (
|
||||
<div className="px-4 py-8 text-text-dim text-[12px] text-center">
|
||||
No notes yet.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notes.map((event) => (
|
||||
<NoteCard key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import { timeAgo, shortenPubkey } from "../../lib/utils";
|
||||
|
||||
interface NoteCardProps {
|
||||
event: NDKEvent;
|
||||
}
|
||||
|
||||
export function NoteCard({ event }: NoteCardProps) {
|
||||
const profile = useProfile(event.pubkey);
|
||||
const name = profile?.displayName || profile?.name || shortenPubkey(event.pubkey);
|
||||
const avatar = profile?.picture;
|
||||
const time = event.created_at ? timeAgo(event.created_at) : "";
|
||||
|
||||
return (
|
||||
<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">
|
||||
{avatar ? (
|
||||
<img
|
||||
src={avatar}
|
||||
alt=""
|
||||
className="w-9 h-9 rounded-sm object-cover bg-bg-raised"
|
||||
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">
|
||||
{name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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-dim text-[11px] shrink-0">{time}</span>
|
||||
</div>
|
||||
<div className="note-content text-text text-[13px] break-words whitespace-pre-wrap">
|
||||
{event.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { getNDK } from "../../lib/nostr";
|
||||
|
||||
export function RelaysView() {
|
||||
const ndk = getNDK();
|
||||
const relays = Array.from(ndk.pool?.relays?.values() ?? []);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<header className="border-b border-border px-4 py-2.5 shrink-0">
|
||||
<h1 className="text-text text-sm font-medium tracking-wide">Relays</h1>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{relays.length === 0 ? (
|
||||
<p className="text-text-dim text-[12px]">No relays configured.</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{relays.map((relay) => (
|
||||
<div
|
||||
key={relay.url}
|
||||
className="flex items-center gap-3 px-3 py-2 border border-border text-[12px]"
|
||||
>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full shrink-0 ${
|
||||
relay.connected ? "bg-success" : "bg-danger"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-text truncate flex-1 font-mono">{relay.url}</span>
|
||||
<span className="text-text-dim shrink-0">
|
||||
{relay.connected ? "connected" : "disconnected"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function SettingsView() {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<header className="border-b border-border px-4 py-2.5 shrink-0">
|
||||
<h1 className="text-text text-sm font-medium tracking-wide">Settings</h1>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<p className="text-text-dim text-[12px]">
|
||||
Settings will appear here — key management, relay config, Lightning wallet connection, appearance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { useFeedStore } from "../../stores/feed";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ id: "feed" as const, label: "feed", icon: "◈" },
|
||||
{ id: "relays" as const, label: "relays", icon: "⟐" },
|
||||
{ id: "settings" as const, label: "settings", icon: "⚙" },
|
||||
] as const;
|
||||
|
||||
export function Sidebar() {
|
||||
const { currentView, setView, sidebarCollapsed, toggleSidebar } = useUIStore();
|
||||
const { connected, notes } = useFeedStore();
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`h-full border-r border-border bg-bg flex flex-col transition-all duration-150 ${
|
||||
sidebarCollapsed ? "w-12" : "w-48"
|
||||
}`}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="border-b border-border px-3 py-2.5 flex items-center justify-between shrink-0">
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="text-text hover:text-accent transition-colors"
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<span className="text-sm font-bold">W</span>
|
||||
) : (
|
||||
<span className="text-sm font-bold tracking-widest">WRYSTR</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex-1 py-2">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setView(item.id)}
|
||||
className={`w-full text-left px-3 py-1.5 flex items-center gap-2 text-[12px] transition-colors ${
|
||||
currentView === item.id
|
||||
? "text-accent bg-accent/8"
|
||||
: "text-text-muted hover:text-text hover:bg-bg-hover"
|
||||
}`}
|
||||
>
|
||||
<span className="w-4 text-center text-[14px]">{item.icon}</span>
|
||||
{!sidebarCollapsed && <span>{item.label}</span>}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Status footer */}
|
||||
{!sidebarCollapsed && (
|
||||
<div className="border-t border-border px-3 py-2 text-[10px] text-text-dim">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${connected ? "bg-success" : "bg-danger"}`} />
|
||||
<span>{connected ? "online" : "offline"}</span>
|
||||
</div>
|
||||
<div className="mt-0.5">{notes.length} notes</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user