SkateSpots/src/components/CrewNip05Badge.tsx
shakespeare.diy 40e4a27565 Add crew NIP-05 (any-domain, founder-verified) + founder/delegated-admin governance
- SkateCrew: add nip05 + admins fields; parse `admin` role marker on p tags (admins = subset of members)
- CrewForm: NIP-05 input + per-member "Make admin" toggle; write nip05 tag + admin-marked p tags (works on create & edit via shared form)
- useCrewNip05: verify crew handle on ANY domain against the founder pubkey (verified/mismatch/unreachable), CORS-proxy fallback + caching
- CrewNip05Badge: green ✓ / red ⚠ / muted handle states with tooltips
- CollectiveMembers: show Admin chips, sort admins first, admin count in summary
- useCrewRole: centralized founder/admin/member/none roles (canManage, canEditProfile) — founder owns identity, admins get delegated powers with their own keys; egalitarian shared-editing intentionally unsupported
- CrewDetailPage: NIP-05 badge by crew name, viewer role badges (admin/member)
- NIP.md: document crew nip05 verification, admin role marker, and governance/conflict-resolution model

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-24 16:46:20 +07:00

91 lines
2.7 KiB
TypeScript

import { BadgeCheck, Loader2, AlertTriangle } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useCrewNip05 } from '@/hooks/useCrewNip05';
import { cn } from '@/lib/utils';
interface CrewNip05BadgeProps {
nip05?: string;
founderPubkey?: string;
className?: string;
}
/**
* Verified-handle badge for a crew's optional NIP-05.
*
* - `verified`: green check + the handle. The domain confirms the founder key.
* - `mismatch`: red warning — the domain points the handle at a different key.
* - `unreachable`: muted handle (shown but unverified) while the domain is down.
* - `none`: renders nothing.
*/
export function CrewNip05Badge({ nip05, founderPubkey, className }: CrewNip05BadgeProps) {
const { data, isLoading } = useCrewNip05(nip05, founderPubkey);
if (!nip05) return null;
const status = data?.status ?? (isLoading ? 'loading' : 'unreachable');
const handle = nip05.trim().toLowerCase();
if (status === 'mismatch') {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
'inline-flex items-center gap-1 rounded-full bg-red-500/15 px-2 py-0.5 text-xs font-bold text-red-400',
className,
)}
>
<AlertTriangle className="h-3 w-3" /> {handle}
</span>
</TooltipTrigger>
<TooltipContent>
This domain points the handle at a different Nostr key than the crew founder. Not verified.
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
if (status === 'verified') {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
'inline-flex items-center gap-1 rounded-full bg-teal-500/15 px-2 py-0.5 text-xs font-bold text-teal-300',
className,
)}
>
<BadgeCheck className="h-3 w-3" /> {handle}
</span>
</TooltipTrigger>
<TooltipContent>
Verified — the domain confirms this handle belongs to the crew's founder key.
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
// loading / unreachable — show the handle, muted.
return (
<span
className={cn(
'inline-flex items-center gap-1 rounded-full bg-white/5 px-2 py-0.5 text-xs font-medium text-gray-400',
className,
)}
>
{status === 'loading' ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : null}
{handle}
</span>
);
}