Introduce granular role-based permissions with SuperAdmin env override, admin roles UI, and permission-gated API routes. Fix admin user Nostr metadata by batching relay profile fetches, normalizing npub pubkeys to hex, and adding reusable NostrAvatar/useNostrProfile components. Co-authored-by: Cursor <cursoragent@cursor.com>
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { useNostrProfile } from "@/hooks/useNostrProfile";
|
|
import type { NostrProfile } from "@/lib/nostr";
|
|
|
|
function computeInitials(profile: NostrProfile | null, fallback?: string): string {
|
|
const name = profile?.name || profile?.displayName;
|
|
if (name?.trim()) return name.trim().slice(0, 2).toUpperCase();
|
|
const fb = fallback?.trim();
|
|
if (fb) {
|
|
// For npub-style fallbacks skip the "npub1" prefix for nicer initials.
|
|
if (fb.startsWith("npub1") && fb.length >= 8) return fb.slice(5, 7).toUpperCase();
|
|
return fb.slice(0, 2).toUpperCase();
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
export interface NostrAvatarProps {
|
|
pubkey: string | null | undefined;
|
|
/** Rendered size in pixels (square). */
|
|
size?: number;
|
|
/** Text used to derive initials when no Nostr name/picture is available. */
|
|
fallbackText?: string;
|
|
className?: string;
|
|
}
|
|
|
|
// Self-contained avatar: fetches the user's Nostr metadata from relays and
|
|
// renders their profile picture, falling back to initials.
|
|
export function NostrAvatar({
|
|
pubkey,
|
|
size = 56,
|
|
fallbackText,
|
|
className = "",
|
|
}: NostrAvatarProps) {
|
|
const { profile, loading } = useNostrProfile(pubkey);
|
|
const displayName = profile?.name || profile?.displayName;
|
|
|
|
return (
|
|
<div
|
|
className={`shrink-0 rounded-full bg-surface-container-high flex items-center justify-center overflow-hidden text-on-surface ${className}`}
|
|
style={{ width: size, height: size }}
|
|
>
|
|
{loading ? (
|
|
<span className="text-on-surface/40 text-xs">…</span>
|
|
) : profile?.picture ? (
|
|
<Image
|
|
src={profile.picture}
|
|
alt={displayName ? `Avatar: ${displayName}` : "Nostr profile picture"}
|
|
width={size}
|
|
height={size}
|
|
className="object-cover w-full h-full"
|
|
unoptimized
|
|
/>
|
|
) : (
|
|
<span className="font-semibold text-sm" aria-hidden>
|
|
{computeInitials(profile, fallbackText)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|