import { useState } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useParams, Link } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; import { Layout } from '@/components/Layout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Skeleton } from '@/components/ui/skeleton'; import { ZapButton } from '@/components/ZapButton'; import { ZapCrewDialog } from '@/components/ZapCrewDialog'; import { ClinkPayButton } from '@/components/ClinkPayButton'; import { CollectiveMembers } from '@/components/CollectiveMembers'; import { CrewNip05Badge } from '@/components/CrewNip05Badge'; import { CrewChat } from '@/components/CrewChat'; import { CrewAgentPanel } from '@/components/CrewAgentPanel'; import { CrewTreasuryCard } from '@/components/CrewTreasuryCard'; import { CrewLookingForCard } from '@/components/CrewLookingFor'; import { CrewRequestToJoin } from '@/components/CrewRequestToJoin'; import { CrewRepBadge } from '@/components/CrewBadge'; import { useCrewEvents } from '@/hooks/useCrewEvents'; import { EventCard } from '@/components/EventCard'; import { ConfirmMembershipCard } from '@/components/ConfirmMembershipCard'; import { LinkedLabCard } from '@/components/CrossLinkCards'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog'; import { useCollectiveMembers } from '@/hooks/useCollectiveMembers'; import { useCrewRole } from '@/hooks/useCrewRole'; import { useCrewModeration, useHideCrewPost } from '@/hooks/useCrewModeration'; import { CrewInviteDialog } from '@/components/CrewInviteDialog'; import { DeleteEntityDialog } from '@/components/DeleteEntityDialog'; import { useToast } from '@/hooks/useToast'; import { NoteContent } from '@/components/NoteContent'; import { useCrew } from '@/hooks/useCrews'; import { useCrewFeed } from '@/hooks/useCrewFeed'; import { useAuthor } from '@/hooks/useAuthor'; import { genUserName } from '@/lib/genUserName'; import { SKATE_CREW_KIND, SKATE_SPOT_KIND, CREW_VIBE_LABELS } from '@/lib/types'; import { sanitizeUrl, urlDomain } from '@/lib/sanitizeUrl'; import { cn } from '@/lib/utils'; import NotFound from './NotFound'; import { Users, MapPin, ArrowLeft, Calendar, Globe, Instagram, Edit, MessageSquare, Zap, ShieldCheck, EyeOff } from 'lucide-react'; import type { NostrEvent } from '@nostrify/nostrify'; function FeedItem({ event, canModerate, onHide, isHiding, }: { event: NostrEvent; canModerate?: boolean; onHide?: (event: NostrEvent) => void; isHiding?: boolean; }) { const author = useAuthor(event.pubkey); const metadata = author.data?.metadata; const name = metadata?.name ?? genUserName(event.pubkey); const npub = nip19.npubEncode(event.pubkey); return (
{name.slice(0, 2).toUpperCase()} {name} {new Date(event.created_at * 1000).toLocaleDateString()} {canModerate && onHide && ( Hide this post? As a crew moderator you can hide this post from the crew feed for everyone. The original note stays on Nostr — this only removes it from this crew's feed. Cancel onHide(event)} className="bg-red-600 hover:bg-red-700" > Hide post )}
); } export default function CrewDetailPage() { const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>(); const { data: crew, isLoading, error } = useCrew(identifier || '', pubkey); const author = useAuthor(crew?.pubkey); const { isFounder, isAdmin, role, canManage } = useCrewRole(crew); const { toast } = useToast(); const [agentOpen, setAgentOpen] = useState(false); useSeoMeta({ title: crew ? `${crew.name} - SkateSpots Crew` : 'Crew - SkateSpots', description: crew?.bio || 'A skate crew on SkateSpots.', }); // Feed = founder + listed members const feedAuthors = crew ? [crew.pubkey, ...crew.members] : []; const { data: feed, isLoading: feedLoading } = useCrewFeed(feedAuthors); // Crew-hosted events (jams, comps, meetups). const { data: crewEvents } = useCrewEvents(crew?.pubkey, crew?.identifier); // Moderation — founder + admins can hide feed posts (NIP-32 labels). const crewCoord = crew ? `${SKATE_CREW_KIND}:${crew.pubkey}:${crew.identifier}` : ''; const moderators = crew ? Array.from(new Set([crew.pubkey, ...crew.admins])) : []; const { data: hiddenIds } = useCrewModeration({ crewCoord, moderators, enabled: !!crew, }); const { mutateAsync: hidePost, isPending: isHiding } = useHideCrewPost(crewCoord); const handleHidePost = async (ev: NostrEvent) => { try { await hidePost({ postId: ev.id, postAuthor: ev.pubkey }); toast({ title: 'Post hidden', description: 'Removed from the crew feed for everyone.' }); } catch (err) { toast({ title: 'Failed to hide post', description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive', }); } }; // Confirmed members (for "Zap the Crew" — only pay people who vouched in) const { data: memberStatuses } = useCollectiveMembers({ kind: SKATE_CREW_KIND, founderPubkey: crew?.pubkey ?? '', identifier: crew?.identifier ?? '', claimedMembers: crew?.members ?? [], enabled: !!crew && (crew.members.length ?? 0) > 0, }); // Recipients: founder + confirmed members (dedup, founder always included) const crewZapRecipients = crew ? Array.from(new Set([ crew.pubkey, ...(memberStatuses ?? []).filter((m) => m.status === 'confirmed').map((m) => m.pubkey), ])) : []; if (isLoading) { return (
); } if (error || !crew) return ; const npub = nip19.npubEncode(crew.pubkey); const founderName = author.data?.metadata?.name ?? genUserName(crew.pubkey); const hasMembers = crew.members.length > 0; return (
Back to Crews
{/* Main */}
{/* Confirm membership prompt (listed pending members only) */} {hasMembers && ( )} {/* Hero */}
{crew.name.slice(0, 2).toUpperCase()}

{crew.name}

Crew
{crew.location && ( {crew.location} )} {crew.founded && ( Since {crew.founded} )}
{canManage && ( )} {isFounder && (
)} {isAdmin && !isFounder && ( You're an admin )} {role === 'member' && ( Member )} {role === 'none' && ( )}
{crew.bio && (

{crew.bio}

)} {crew.vibes.length > 0 && (
{crew.vibes.map((v) => ( {CREW_VIBE_LABELS[v] || v} ))}
)}
{/* Open call — looking for filmers/editors/members (Lab pipeline) */} {/* Crew treasury — transparent, zap-funded NIP-75 goal */} {crew.goalRef && } {/* Crew-hosted events (jams / comps / meetups) */} {((crewEvents && crewEvents.length > 0) || canManage) && ( Crew Events {canManage && ( )} {crewEvents && crewEvents.length > 0 ? ( crewEvents.slice(0, 5).map((ev) => ( )) ) : (

No crew events yet. Host a jam, comp or meetup as the crew — it connects your crew to the events calendar and lets people zap it.

)}
)} {/* Crew feed */} Crew Feed {feedLoading ? ( Array.from({ length: 3 }).map((_, i) => (
)) ) : (() => { // Moderators still see hidden posts (greyed via the badge on hide), // but for everyone the hidden set is filtered out. const visible = (feed ?? []).filter( (ev) => canManage || !hiddenIds?.has(ev.id), ); if (visible.length === 0) { return (

No recent posts from the crew yet.

); } return visible.map((ev) => (
{hiddenIds?.has(ev.id) && (

Hidden from feed

)}
)); })()}
{/* Crew Agent — AI guide, shown when the crew enabled it */} {crew.agentEnabled && ( )} {/* Crew Chat — private E2E-encrypted group chat for members */}
{/* Sidebar */}
{/* Founder / links / support */} Crew {founderName.slice(0, 2).toUpperCase()} Started by {founderName} {sanitizeUrl(crew.website) && ( {urlDomain(crew.website!)} )} {crew.instagram && ( @{crew.instagram.replace('@', '')} )} {crew.socials.map((s, i) => { const url = sanitizeUrl(s); return url ? ( {urlDomain(s)} ) : (
{s}
); })}
{/* Zap the whole crew — split across founder + confirmed members */} {crewZapRecipients.length > 1 && ( )} {crew.noffer && ( )}
{/* Members */} {hasMembers && ( )} {/* Linked Lab profiles — hire this crew */} {/* Home spots */} {crew.spotRefs.length > 0 && ( Home Spots {crew.spotRefs.map((ref) => { const [, spotPubkey, spotD] = ref.split(':'); return ( 🛹 View spot ); })} See on the map → )}
{/* Crew Agent chat panel */} {crew.agentEnabled && ( )}
); }