"use client"; import { useEffect, useState } from "react"; import Image from "next/image"; import { nip19 } from "nostr-tools"; import { api } from "@/lib/api"; import { cn, formatDate } from "@/lib/utils"; import { fetchNostrProfile, type NostrProfile } from "@/lib/nostr"; import { ShieldCheck, ShieldOff, UserPlus } from "lucide-react"; function hexToNpub(hex: string): string { try { return nip19.npubEncode(hex); } catch { return ""; } } function shortenNpub(npub: string): string { if (npub.length <= 26) return npub; return `${npub.slice(0, 14)}...${npub.slice(-10)}`; } function profileInitials(profile: NostrProfile | undefined, npub: string): string { const n = profile?.name || profile?.displayName; if (n?.trim()) return n.trim().slice(0, 2).toUpperCase(); if (npub.length >= 8) return npub.slice(5, 7).toUpperCase(); return "?"; } export default function UsersPage() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [promotePubkey, setPromotePubkey] = useState(""); const [promoting, setPromoting] = useState(false); const [hostname, setHostname] = useState(""); const [usernameDrafts, setUsernameDrafts] = useState>({}); const [savingPubkey, setSavingPubkey] = useState(null); const [nostrByPubkey, setNostrByPubkey] = useState>({}); const [nostrLoading, setNostrLoading] = useState(false); const [copiedPubkey, setCopiedPubkey] = useState(null); const loadUsers = async () => { try { const data = await api.getUsers(); setUsers(data); setUsernameDrafts( Object.fromEntries( data.map((u: { pubkey: string; username?: string | null }) => [u.pubkey, u.username ?? ""]) ) ); } catch (err: any) { setError(err.message); } finally { setLoading(false); } }; useEffect(() => { loadUsers(); }, []); useEffect(() => { setHostname(typeof window !== "undefined" ? window.location.hostname : ""); }, []); useEffect(() => { if (users.length === 0) { setNostrByPubkey({}); setNostrLoading(false); return; } let cancelled = false; setNostrLoading(true); setNostrByPubkey({}); (async () => { const entries = await Promise.all( users.map(async (u: { pubkey: string }) => { const profile = await fetchNostrProfile(u.pubkey); return [u.pubkey, profile] as const; }) ); if (!cancelled) { setNostrByPubkey(Object.fromEntries(entries)); setNostrLoading(false); } })(); return () => { cancelled = true; }; }, [users]); const handleCopyNpub = async (pubkey: string) => { const full = hexToNpub(pubkey); if (!full) return; try { await navigator.clipboard.writeText(full); setCopiedPubkey(pubkey); window.setTimeout(() => setCopiedPubkey((p) => (p === pubkey ? null : p)), 2000); } catch { // ignore } }; const handlePromote = async () => { if (!promotePubkey.trim()) return; setPromoting(true); setError(""); try { await api.promoteUser(promotePubkey); setPromotePubkey(""); await loadUsers(); } catch (err: any) { setError(err.message); } finally { setPromoting(false); } }; const handleDemote = async (pubkey: string) => { if (!confirm("Demote this user to regular user?")) return; setError(""); try { await api.demoteUser(pubkey); await loadUsers(); } catch (err: any) { setError(err.message); } }; const handlePromoteUser = async (pubkey: string) => { setError(""); try { await api.promoteUser(pubkey); await loadUsers(); } catch (err: any) { setError(err.message); } }; const handleSaveUsername = async (pubkey: string, currentUsername: string | null | undefined) => { const trimmed = (usernameDrafts[pubkey] ?? "").trim().toLowerCase(); if (!trimmed) return; setSavingPubkey(pubkey); setError(""); try { await api.updateUserUsername(pubkey, trimmed); await loadUsers(); } catch (err: any) { setError(err.message); setUsernameDrafts((prev) => ({ ...prev, [pubkey]: currentUsername ?? "" })); } finally { setSavingPubkey(null); } }; const handleCancelUsername = (pubkey: string, stored: string | null | undefined) => { setUsernameDrafts((prev) => ({ ...prev, [pubkey]: stored ?? "" })); }; if (loading) { return (
Loading users...
); } return (

User Management

{error &&

{error}

}

Promote User

setPromotePubkey(e.target.value)} className="bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 w-full focus:outline-none focus:ring-1 focus:ring-primary/40 flex-1" />
{users.length === 0 ? (

No users found.

) : ( users.map((user) => { const draft = usernameDrafts[user.pubkey] ?? ""; const stored = user.username ?? ""; const usernameDirty = draft.trim().toLowerCase() !== stored.toLowerCase(); const profile = nostrByPubkey[user.pubkey]; const fullNpub = hexToNpub(user.pubkey); const nostrDisplay = profile?.name || profile?.displayName; return (
{nostrLoading ? ( ) : profile?.picture ? ( {nostrDisplay ) : ( {profileInitials(profile, fullNpub)} )}

{nostrLoading ? ( ) : nostrDisplay ? ( nostrDisplay ) : ( No Nostr name )}

NIP-05 username

Reserved names from the site blocklist can be assigned here (users cannot claim them on the dashboard).

setUsernameDrafts((prev) => ({ ...prev, [user.pubkey]: e.target.value })) } disabled={savingPubkey === user.pubkey} placeholder="local-part" className="bg-surface-container-highest text-on-surface rounded-lg px-3 py-2 text-sm font-mono min-w-[8rem] max-w-full flex-1 focus:outline-none focus:ring-1 focus:ring-primary/40 disabled:opacity-50" /> @{hostname || "…"}
{draft.trim() && (

{draft.trim().toLowerCase()}@{hostname || "…"}

)}
{user.role} {user.createdAt && ( Joined {formatDate(user.createdAt)} )}
{user.role !== "ADMIN" && (
{user.role !== "MODERATOR" && ( )} {user.role === "MODERATOR" && ( )}
)}
); }) )}
); }