feat: roles/permissions system and Nostr profile display on admin users
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>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user